Open-source runtime verification for AI, licensed under Apache-2.0.
QWED
Contact

Type to search pages, engines and advisories.

    Looking for API details?Search the docs

    Checking AI-generated SQL before it runs

    A model that writes SQL from a question will sometimes write a query that deletes rows, carries an injection pattern or reads a column it should not. QWED parses the query with SQLGlot before it runs and returns a decision: only a query with no finding that denies is admitted. Your application then runs it or holds it.

    What can go wrong

    Text-to-SQL is useful because a person can ask a question in plain words. The risk is that the model is as confident about a harmful query as about a correct one. The mistakes that matter most are structural, which is why a parser can catch them:

    • It changes data. A question about orders comes back as an UPDATE or a DELETE.
    • It carries an injection pattern. A WHERE clause ends with OR 1 = 1, or a second statement follows a semicolon.
    • It reads what it should not. A join pulls in a password, salary or card column.
    • It runs away. Ten joins and nested subqueries on a production database.

    What QWED checks

    The SQL engine parses the query into a syntax tree with SQLGlot and inspects the tree. It never connects to your database. A finding that denies stops the query from being admitted; a finding that warns is reported and the query can still be admitted.

    What triggers itEffectFinding
    Changes and admin
    DELETE, UPDATE, INSERT, DROP, ALTER, TRUNCATE, CREATE, MERGE, anywhere in the query, including inside a CTE or subqueryDeniesdestructive_command
    GRANT, REVOKE, SET, USE, transactions, COPY, ATTACH, DETACH, PRAGMA, LOAD DATA, EXPORT, CALL, EXECUTE and statements SQLGlot does not model, anywhere in the queryDeniesadmin_command
    Any other statement that is not a read-only query (SELECT, UNION, INTERSECT, EXCEPT, WITH)Deniesnon_read_only_statement
    Injection patterns
    A comparison that is always true, such as 1 = 1, 1 = 1.0 or 2 > 1, or a filter that is always true, such as WHERE 1Deniesinjection_tautology
    OR with a side that is always true, such as OR TRUE, OR 1 or OR NOT FALSEDeniesinjection_or_true
    A second statement after a semicolonDeniesstacked_queries
    UNIONWarnsunion_query
    A comment that starts with two hyphensWarnscomment_detected
    Sensitive data
    Reading a blocked column, by default password, secret, api_key, token, ssn, salary, credit_card, bank_account, balance, private_key and similarDeniessensitive_column_access
    SELECT * or t.* over a table whose declared schema has a blocked column (needs a schema)Deniessensitive_column_access
    Schema, when you pass one
    A table that is not in the schema you gaveDeniesschema_table_missing
    Size
    More than 10 tables, 5 joins, 3 levels of subquery or an estimated cost over 1,000Deniescomplexity_*
    More than 50 columns or 20 conditionsWarnscomplexity_*

    MySQL and MariaDB executable comments (/*! ... */) are expanded before the checks run, so a pattern hidden inside one is still seen.

    What comes back

    Every check returns a result and a decision, as on the rest of the platform. For SQL there are three outcomes:

    • Proven correct. VERIFIED · ADMIT

      A query that parses and has no finding that denies, for example a plain SELECT on tables in your schema.

    • Proven wrong. VERIFIED · DENY

      A destructive or admin statement, an injection pattern or a blocked column. The result lists each finding as evidence.

    • Can’t be checked. BLOCKED · DENY

      The query does not parse, the schema does not parse, a table is outside the schema, the statement is not a read-only query, or the query is over the size limits.

    A verified result carries a proof_ref: a SHA-256 commitment to the parsed query, the decision and the settings it was checked with, so a later reader can tell exactly what was checked. It is a record, not a proof that the query answers the question.

    Try it

    In Python, with the core package (pip install qwed). This is the example from the README:

    python
    from qwed_new.core.sql_verifier import SQLVerifierfrom qwed_new.core.diagnostics import admission_decisionresult = SQLVerifier().verify_sql(    "DELETE FROM users WHERE id = 1 OR 1 = 1",    schema_ddl="CREATE TABLE users (id INT)",)print(result.status.value, admission_decision(result).value)# VERIFIED BLOCKEDprint([issue["type"] for issue in result.developer_fields["issues"]])# ['destructive_command', 'injection_tautology']

    BLOCKED in the second value is the admission decision; in a Verification Context record the same decision is written DENY.

    Over the self-hosted API, send the query with the schema it should run against. The API requires schema_ddl and accepts a dialect (SQLite when you leave it out):

    request
    POST /verify/sql{  "query": "SELECT id, name FROM users WHERE id = 42",  "schema_ddl": "CREATE TABLE users (id INT, name TEXT)",  "dialect": "postgres"}

    Where it sits

    1. The model writes a query from the user's question.
    2. Your application sends the query and your schema to QWED.
    3. On VERIFIED · ADMIT, your application runs the query, ideally with a database role that can only read.
    4. On anything else, your application does not run it: it shows the findings, asks the model to try again, or holds it for a person.

    QWED returns the decision; your application enforces it. Keep the database's own permissions in place as well: QWED checks the query's shape, not what the connection is allowed to do.

    Settings

    In the library, SQLVerifier takes:

    • blocked_columns: more column names to deny. They are added to the defaults; the defaults cannot be removed.
    • complexity_limits: your own limits for tables, joins, subquery depth, columns, conditions and cost. check_complexity=False skips them.
    • allow_destructive=True: admits data changes for jobs that need them. Admin commands such as GRANT are still denied.
    • dialect: passed to SQLGlot, for example postgres, mysql or sqlite.

    The API uses the defaults; only the dialect can be set per request.

    What it does not check

    • Whether it is the right query. A valid SELECT that answers a different question is still admitted. QWED does not check intent.
    • Columns against the schema. The schema check covers tables only. An unknown column is not flagged, and a schema-qualified name such as public.users is matched on the table name.
    • Anything inside the database. No connection is made, so row counts, permissions, row-level policies and the data itself are out of scope.
    • Tables and stars, when no schema is given. In the library, the schema is optional; without it, table names are not checked and SELECT * cannot be expanded, so a star over a table with a blocked column is not caught. The API always requires a schema.
    • Warnings. UNION and -- comments are reported but do not deny on their own.

    Results are only as sound as SQLGlot and QWED's own rules. Everything QWED does not verify

    Track record

    Problems in this engine are published, not hidden. Each of these was a false accept: a query the engine returned as VERIFIED · ADMIT when it should have been denied. Each is fixed, and tests cover it.

    • GHSA-6p94-4f75-g2wq, fixed in qwed 7.3.0. A data change hidden in a CTE (WITH d AS (DELETE FROM t RETURNING *) SELECT * FROM d) was admitted because only the outermost statement was classified, and statement families such as COPY, ATTACH and PRAGMA were in no deny list. The whole query is now checked, and only read-only statements are admitted.
    • GHSA-6cpx-8jrq-r7h3, fixed in qwed 7.3.0. SELECT * FROM users was admitted even when the schema declared a password column, and always-true filters such as OR 1 or 1 = 1.0 slipped past a check that matched fixed shapes. Stars are now expanded against the schema, and filters are tested by value.
    • GHSA-q5g6-6cw2-cjm3, fixed in qwed 7.2.3. A pattern inside a MySQL executable comment (WHERE id = 1 /*!50000 OR 1=1 */) was admitted, and a table outside the declared schema produced only a warning. Both now deny.

    These checks are a screening step, not a replacement for a database role that can only read. The full track record

    Other parts of QWED look at SQL from different sides: QWED Security flags SQL built by string concatenation in your code, and Open Responses can block destructive statements in an agent's tool-call arguments.