Read-only-first principle
The core rule is simple: onlySELECT statements are permitted. BoardPulse AI will never write to, modify, or delete data in your source database. If the AI model generates a query that starts with anything other than SELECT, the request is rejected before execution with an HTTP 422 response.
This is enforced at two levels:
- Keyword check — the normalized SQL is scanned for any forbidden keyword
- Statement type check — the query must begin with
SELECT
Forbidden keywords
The following SQL keywords are blocked regardless of context. If any of them appear in a generated query, the request fails immediately:How validation works
BoardPulse AI uses SQLGlot to parse and validate SQL before execution. SQLGlot performs dialect-aware AST parsing, which is more reliable than simple string matching. The validation sequence for every query:1
Parse
SQLGlot parses the raw SQL string into an AST. If the SQL is syntactically invalid, a
ParseError is raised and the request returns HTTP 422.2
Single-statement check
Only one SQL statement is allowed per request. Multi-statement queries (e.g.,
SELECT 1; DROP TABLE users) are rejected immediately.3
SELECT-only check
The normalized query must start with
SELECT. CTEs are accepted since they resolve to a SELECT.4
Forbidden keyword scan
The full normalized SQL is checked for any keyword in
FORBIDDEN_KEYWORDS.5
Table allowlist check
SQLGlot walks the AST to extract every table reference. Any table not in your approved list causes a rejection that names the offending tables.
6
Normalized SQL returned
If all checks pass, the normalized (pretty-printed) SQL is returned and forwarded to the execution layer.
Table allowlisting
Only tables you explicitly approve can appear in queries. The allowlist is configured with theDEFAULT_SOURCE_INCLUDE_TABLES environment variable:
The table names in
DEFAULT_SOURCE_INCLUDE_TABLES are matched case-insensitively. You do not need to worry about capitalization.Execution safety limit
After validation, the query is wrapped with a configurableLIMIT before it runs. This prevents accidental full-table scans from returning millions of rows. The executed_sql field in the response always reflects the final query — including this limit — so you can see exactly what ran.