> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwedai.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# SQL engine

> QWED's SQL Engine validates queries for injection attacks, destructive operations, schema compliance, and syntax errors before execution in production.

> [!NOTE]
>
> **Updated in v7.0.0 (breaking).** `SQLVerifier.verify_sql()` now returns a [`DiagnosticResult`](https://docs.qwedai.tech/advanced/diagnostics) instead of an ad-hoc dict, and a proven-malicious query is reported as `VERIFIED` (truth) with a separate admission decision (policy). See the [changelog](https://docs.qwedai.tech/changelog.md#v7-0-0-—-full-diagnosticresult-engine-conformance) for migration details.

SQL query validation and injection detection.

## Overview

The SQL Engine validates queries for:

* SQL injection patterns
* Destructive operations
* Schema compliance
* Syntax correctness

Verification is static AST analysis (SQLGlot). The engine never connects to a database.

## The `DiagnosticResult` contract

`verify_sql()` returns a `DiagnosticResult` with a status, an agent-safe `agent_message`, structured `developer_fields`, and a `proof_ref` bound to the query AST:

| Outcome | `status` | `developer_fields` | `proof_ref` |
| - | - | - | - |
| Safe query | `VERIFIED` | `is_valid: true` | Present |
| Malicious query | `VERIFIED` (as-malicious) | `is_valid: false`, `malicious_classification: true` | Present |
| Table outside `schema_ddl` | `BLOCKED` | `is_valid: false`, `constraint_id: "sql_verifier.schema_violation"` | `null` |
| Complexity limit exceeded | `BLOCKED` | `constraint_id: "sql_verifier.complexity_limit_exceeded"` | `null` |
| DDL schema parse failure | `BLOCKED` | `constraint_id: "sql_verifier.schema_parse_error"` | `null` |
| Query parse error | `BLOCKED` | `constraint_id: "sql_verifier.parse_error"` | `null` |
| Internal error | `BLOCKED` | `constraint_id: "sql_verifier.execution_error"` | `null` |

Proving that a query is malicious is a successful proof, so a malicious query is `VERIFIED` — not `BLOCKED`. `BLOCKED` is reserved for cases where verification itself could not complete, and blocked results never carry a `proof_ref`. If the DDL schema fails to parse, the incomplete analysis is `BLOCKED` even when the query itself looks malicious (`schema_parse_error` takes precedence).

`agent_message` never leaks detection rules, rule IDs, or raw parser output. Rule-level detail lives in `developer_fields`.

### Admission is a separate decision

Do not gate execution on `status` alone. `POST /verify/sql` returns an explicit `admission` field (`ADMIT` or `BLOCKED`) alongside the verdict. A malicious query is `VERIFIED` at the truth layer but `BLOCKED` at the admission layer. Gate on `admission` (or `developer_fields.is_valid`), never on `status == "VERIFIED"`.

## Usage

```python theme={null}
result = client.verify_sql(
    query="SELECT * FROM users WHERE id = 1",
    schema_ddl="CREATE TABLE users (id INT, name TEXT)"
)
print(result.status)                            # "VERIFIED"
print(result.developer_fields["is_valid"])      # True
print(result.proof_ref)                         # "sha256:..."
```

## Schema compliance

When you pass `schema_ddl`, every table the query reads or writes must be declared in that DDL. A reference to an undeclared table is an admission failure: the result is `BLOCKED` with `is_valid: false` and `constraint_id: "sql_verifier.schema_violation"`. Names the query defines itself, such as CTEs, are not treated as tables. Without `schema_ddl`, the engine does not check table names.

With `schema_ddl`, the engine also expands `SELECT *` (qualified or not, such as `SELECT u.*`) against the declared columns. If the star covers a sensitive column, the query is not valid (`is_valid: false`) with a `CRITICAL` `sensitive_column_access` issue. Select the non-sensitive columns you need instead of `*`. Without `schema_ddl`, the engine can't tell which columns a star covers.

```python theme={null}
import httpx

try:
    result = client.verify_sql(
        query="SELECT * FROM users",
        schema_ddl="CREATE TABLE users (id INT, name TEXT, password_hash TEXT)",
    )
    print(result.developer_fields["is_valid"])   # False
    # issues: [{"type": "sensitive_column_access", "severity": "CRITICAL", ...}]

    # Select only the columns you need
    result = client.verify_sql(
        query="SELECT id, name FROM users",
        schema_ddl="CREATE TABLE users (id INT, name TEXT, password_hash TEXT)",
    )
    print(result.developer_fields["is_valid"])   # True
except httpx.HTTPError as exc:
    # Request failed (network error or non-2xx). Fail closed: do not run the SQL.
    raise RuntimeError(f"SQL verification unavailable: {exc}") from exc
```

```python theme={null}
import httpx

try:
    result = client.verify_sql(
        query="SELECT * FROM payments",
        schema_ddl="CREATE TABLE users (id INT, name TEXT)",
    )
except httpx.HTTPError as exc:
    # Request failed (network error or non-2xx). Fail closed: do not run the SQL.
    raise RuntimeError(f"SQL verification unavailable: {exc}") from exc

print(result.status)                               # "BLOCKED"
print(result.developer_fields["is_valid"])         # False
print(result.developer_fields["constraint_id"])    # "sql_verifier.schema_violation"
```

> [!NOTE]
>
> Before v7.2.3, a query on a table outside the schema was admitted with a warning. See [GHSA-q5g6-6cw2-cjm3](https://github.com/QWED-AI/qwed-verification/security/advisories/GHSA-q5g6-6cw2-cjm3).

## MySQL and MariaDB executable comments

MySQL and MariaDB run SQL placed inside executable comments (`/*! ... */`, `/*!NNNNN ... */`, and `/*M! ... */`). The SQL engine expands these comments before analysis, so injection, destructive-operation, and schema checks apply to the SQL inside them. The engine expands them whatever `dialect` you declare.

`developer_fields.executable_comments_expanded` is `true` when the query contained an executable comment.

```python theme={null}
import httpx

# The tautology hidden in the comment is analysed as SQL
try:
    result = client.verify_sql("SELECT * FROM users WHERE id = 1 /*!50000 OR 1=1 */")
except httpx.HTTPError as exc:
    # Request failed (network error or non-2xx). Fail closed: do not run the SQL.
    raise RuntimeError(f"SQL verification unavailable: {exc}") from exc
print(result.developer_fields["executable_comments_expanded"])  # True
print(result.developer_fields["is_valid"])                      # False
```

## Injection detection

```python theme={null}
# SQL injection pattern — proven malicious
result = client.verify_sql("SELECT * FROM users; DROP TABLE users; --")
print(result.status)                                            # "VERIFIED" (as-malicious)
print(result.developer_fields["is_valid"])                      # False
print(result.developer_fields["malicious_classification"])      # True
print(result.developer_fields["issues"])
# [{"severity": "CRITICAL", "issue_type": "injection", ...}]
```

## Detected patterns

| Pattern | Risk | Example |
| - | - | - |
| Comment injection | Critical | `; --` |
| OR injection | Critical | `' OR '1'='1` |
| UNION injection | Critical | `UNION SELECT` |
| Chained DROP | Critical | `; DROP TABLE` |

Tautologies are detected by value, not by matching fixed text shapes. The engine constant-folds every column-free comparison, so `OR 1=1`, `OR 'a'='a'`, `OR 1=1.0`, `OR 2>1`, and arithmetic forms are all reported as `injection_tautology`. A `WHERE` or `HAVING` clause whose whole predicate folds to true, such as `WHERE 1`, is flagged too.

> [!NOTE]
>
> Changed in v9.0.0 ([GHSA-jvhg-jw63-gr5g](https://github.com/QWED-AI/qwed-verification/security/advisories/GHSA-jvhg-jw63-gr5g))

Constant folding also covers `IN`, `IS`, `LIKE` and `ILIKE`, `BETWEEN`, `/`, `%`, `||`, `COALESCE`, `CAST`, `ABS`, and `EXISTS` over a `SELECT` with no `FROM`. `AND` and `OR` short-circuit on a constant operand, and a column compared with itself, such as `id = id`, is treated as always true (it is true for every non-`NULL` value). Filters such as `id = 5 OR 1 IN (1)`, `NOT (id = 5 AND 1 = 0)` and `id = 5 OR id = id` are now tautologies: the query has `is_valid: false` and `malicious_classification: true`, and is not admitted.

A filter predicate that reads no column but can't be worked out may still be always true, so the query is not admitted. The result is `BLOCKED` with `is_valid: false`, an issue of type `unevaluated_constant_predicate`, and `constraint_id: "sql_verifier.unevaluated_predicate"`. When the same query also has a proven tautology, the tautology decides the result: `VERIFIED` with `constraint_id: "sql_verifier.malicious"` and `malicious_classification: true`. It doesn't set `malicious_classification`, because nothing proves malice. Compare against a column or remove the constant condition; check that removing it doesn't change which rows the query returns.

### Stacked queries

A query that holds two or more statements, such as `SELECT 1; DROP TABLE users`, is reported as `stacked_queries`. The query is not valid, sets `malicious_classification: true`, and is not admitted.

The engine counts statements from the parsed query. A `;` inside a string literal, a dollar-quoted string, or a comment is not a statement separator, so these queries are checked like any other single statement:

```sql theme={null}
SELECT * FROM users WHERE name = 'a;b'
```

```sql theme={null}
SELECT $$a;b$$
```

```sql theme={null}
SELECT * FROM users /* ; */
```

```sql theme={null}
SELECT * FROM users; -- note
```

Where a database could split the text differently from the parser, the engine stays conservative and counts every `;` before the end of the query as a separator. That applies when the query contains:

* A backslash anywhere. Escape rules depend on server settings, such as MySQL's `NO_BACKSLASH_ESCAPES`.
* `--` not followed by an ASCII whitespace character. MySQL starts a comment only then.
* A `/*` inside a block comment.
* A carriage return not followed by a line feed.

A query that does not parse also counts every `;`. The engine applies these checks both to the query as given and with MySQL executable comments expanded.

> [!NOTE]
>
> Before v9.0.0, any `;` before the last character marked the query as `stacked_queries`, so a `;` in a string or a trailing comment was refused.

## Subqueries and complexity limits

The engine checks queries with parenthesised subqueries, such as `IN (SELECT ...)`, scalar subqueries, and subqueries in `FROM`, like any other query. A valid one is `VERIFIED` with `is_valid: true`.

`developer_fields.complexity.subquery_depth` reports how deeply the deepest `SELECT` is nested inside other `SELECT`, `UPDATE`, `DELETE`, `INSERT`, or `MERGE` statements. A plain query, and a `UNION` of two plain `SELECT` statements, report `0`. A CTE counts as one level. When `subquery_depth` exceeds `max_subquery_depth` (default `3`), the result is `BLOCKED` with `constraint_id: "sql_verifier.complexity_limit_exceeded"`.

```python theme={null}
import httpx

try:
    result = client.verify_sql(
        query="SELECT name FROM users WHERE id IN (SELECT user_id FROM orders)",
        schema_ddl=(
            "CREATE TABLE users (id INT, name TEXT);"
            "CREATE TABLE orders (id INT, user_id INT)"
        ),
    )
except httpx.HTTPError as exc:
    # Request failed (network error or non-2xx). Fail closed: do not run the SQL.
    raise RuntimeError(f"SQL verification unavailable: {exc}") from exc

fields = result.result["developer_fields"]                 # the SDK keeps the full response in result.result
print(result.status)                                       # "VERIFIED"
print(fields["is_valid"])                                  # True
print(fields["complexity"]["subquery_depth"])               # 1
```

> [!NOTE]
>
> Before v9.0.0, every query with a parenthesised subquery was `BLOCKED` with `sql_verifier.execution_error`, and the subquery depth limit never ran for those queries. A `UNION` of two plain `SELECT` statements reported a `subquery_depth` of `1`.

## Destructive operations

```python theme={null}
# Destructive query — verified, not valid, not admissible
result = client.verify_sql("DELETE FROM users")
print(result.developer_fields["is_valid"])   # False
print(result.developer_fields["issues"])
# [{"issue_type": "destructive_delete", "severity": "CRITICAL", ...}]
```

| Operation | Severity |
| - | - |
| DROP | Critical |
| DELETE | High |
| TRUNCATE | High |
| UPDATE | High |
| INSERT | High |
| ALTER | High |
| CREATE | High |
| MERGE | High |

### Dangerous functions

> [!NOTE]
>
> New in v8.0.0 ([GHSA-45jv-2j44-4c2q](https://github.com/QWED-AI/qwed-verification/security/advisories/GHSA-45jv-2j44-4c2q))

The SQL engine denies calls to functions that read or write server files, load code, or reach the OS or another server. The list is `DANGEROUS_SQL_FUNCTIONS`. It covers every dialect, whatever `dialect` you declare. A call anywhere in a statement, including inside a `SELECT`, is a `CRITICAL` issue with type `dangerous_function`. The query is not valid, sets `malicious_classification: true`, and is not admitted.

| Dialect | Examples |
| - | - |
| SQLite | `load_extension`, `readfile`, `writefile` |
| PostgreSQL | `pg_read_file`, `pg_ls_dir`, `lo_import`, `lo_export`, `dblink`, `set_config` |
| MySQL / MariaDB | `load_file`, `sys_exec`, `sys_eval` |
| SQL Server | `xp_cmdshell`, `xp_dirtree`, `sp_oacreate`, `openrowset` |
| Oracle | `utl_file`, `utl_http`, `dbms_java`, `dbms_scheduler` |

```python theme={null}
import httpx

try:
    result = client.verify_sql("SELECT load_extension('/tmp/evil.so')")
except httpx.HTTPError as exc:
    # Request failed (network error or non-2xx). Fail closed: do not run the SQL.
    raise RuntimeError(f"SQL verification unavailable: {exc}") from exc
if result.status == "UNVERIFIABLE":
    # The server could not be reached or decide; there is no verdict to read.
    raise RuntimeError(f"SQL not verified: {result.error}")
print(result.is_verified)                   # False: not admitted
print(result.result["issues"])
# [{"severity": "CRITICAL", "type": "dangerous_function", ...}]
```

> [!NOTE]
>
> Before v8.0.0, `SQLVerifier` admitted calls such as `load_extension()`, `pg_read_file()`, and `xp_cmdshell()`. `QWEDClient.verify_sql` also answered from the local `SQLGuard` instead of the server. It now always calls `POST /verify/sql` and returns `UNVERIFIABLE` when the server can't be reached. See [SQLGuard](https://docs.qwedai.tech/sdks/guards.md#sqlguard) for the local pre-filter.

### Statements are classified across the whole query tree

The engine inspects every node of the parsed query, not just the top-level statement. A data-modifying statement hidden in a CTE or subquery is reported as `destructive_command`, even though the outer statement is a `SELECT`:

```python theme={null}
import httpx

# DML inside a CTE: verified, not valid, not admissible
try:
    result = client.verify_sql("WITH d AS (DELETE FROM users RETURNING *) SELECT * FROM d")
except httpx.HTTPError as exc:
    # Request failed (network error or non-2xx). Fail closed: do not run the SQL.
    raise RuntimeError(f"SQL verification unavailable: {exc}") from exc
print(result.developer_fields["is_valid"])   # False
# issues: [{"type": "destructive_command", "severity": "CRITICAL", ...}]
```

Administrative and side-effecting statement families are reported as `admin_command` wherever they appear (see the table below). Beyond that, only read-only query types are admitted at the top level: `SELECT`, `UNION`, `INTERSECT`, `EXCEPT`, and their `WITH` and parenthesized forms. Any other top-level statement is reported as `non_read_only_statement` with `is_valid: false`, so the engine fails closed on statement types it doesn't recognize.

### Administrative commands

The SQL engine also blocks administrative SQL commands by default, wherever they appear in the query:

| Command | Risk |
| - | - |
| GRANT | Critical |
| REVOKE | Critical |
| SET | Medium |
| TRANSACTION | Medium |

These statement families are also reported as `CRITICAL` `admin_command` issues: `COPY` (including `COPY ... FROM PROGRAM`), `ATTACH`, `DETACH`, `PRAGMA`, `USE`, `LOAD DATA`, `ANALYZE`, `CACHE`, `UNCACHE`, `VACUUM`, `EXPORT`, `IMPORT`, `CALL`, `EXECUTE`, and any statement the SQL parser doesn't model.

> [!NOTE]
>
> Before v7.3.0, the engine checked only the top-level statement type and matched tautologies by text shape, and `SELECT *` bypassed the sensitive-column check. See [GHSA-6p94-4f75-g2wq](https://github.com/QWED-AI/qwed-verification/security/advisories/GHSA-6p94-4f75-g2wq) and [GHSA-6cpx-8jrq-r7h3](https://github.com/QWED-AI/qwed-verification/security/advisories/GHSA-6cpx-8jrq-r7h3).

> [!NOTE]
>
> A resource-limit violation is a `CRITICAL` admission failure but not evidence of malicious intent. Only true-malice issue types set `malicious_classification: true`.

## Supported dialects

* PostgreSQL
* MySQL
* SQLite
* SQL Server
* BigQuery

```python theme={null}
result = client.verify_sql(query, schema, dialect="postgresql")
```


## Related topics

- [Code engine](https://docs.qwedai.tech/engines/code.md)
- [Logic engine — Z3 SAT/SMT verification with QWED-Logic DSL](https://docs.qwedai.tech/engines/logic.md)
- [Verification engine tiers: Proof, Policy, Advisory](https://docs.qwedai.tech/engines/overview.md)
- [LLM verification with formal methods](https://docs.qwedai.tech/advanced/llm-verification.md)
- [QWED-Agent specification v1.1](https://docs.qwedai.tech/specs/agent.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.