SQL Semantic Layer — Stop Bad Agent Queries

A YAML file declares every table, column, join and function an agent may use. Queries outside the model are rejected before they reach the warehouse.

offline no DB connection sqlglot PEP 723

Live Demo simplified JS

against the e-commerce fixture model below
Result will appear here.

Violation Codes

undeclared_table
FROM refunds, pg_catalog.pg_user, "Orders" (quoted = case-sensitive in Postgres)
undeclared_column
orders.discount_cents; also through CTEs, subqueries, ORDER BY
undeclared_join
JOIN on an undeclared relationship; CROSS JOIN; ON declared OR 1=1
pii_column
Any read of a pii:true column, including in WHERE or through an alias
select_star
SELECT *, t.* — COUNT(*) is fine
undeclared_function
pg_read_file(), pg_sleep(), dblink() — any function not listed in the model
write_statement
INSERT/UPDATE/DELETE/DDL; also the second statement after a semicolon
parse_error
SQL that doesn't parse in the declared dialect

Fixture Model — e-commerce warehouse

Policy: read-only · no SELECT * · PII blocked

TableColumnTypeNotes
customersidintegerPK
emailtextPII ✗
full_nametextPII ✗
countrytextISO-3166
created_attimestamp
ordersidinteger
customer_idintegerFK→customers
statustextplaced|shipped|cancelled
total_centsintegertax incl.
placed_attimestamp
order_itemsidinteger
order_idintegerFK→orders
product_idintegerFK→products
quantityinteger
unit_price_centsinteger
productsidintegerPK
skutext
nametext
categorytext

Allowed joins: orders.customer_id = customers.id · order_items.order_id = orders.id · order_items.product_id = products.id

CLI — full sqlglot validation

The demo above is a simplified JS validator. The real tool uses sqlglot — catches undeclared columns through CTEs, OR-injected join conditions, dialect-specific functions, and more.

uv run main.py check --model model.yaml \ --sql "SELECT email FROM customers" # exit 1, JSON reasons uv run main.py batch agent_queries.jsonl --json # audit a full JSONL query log uv run main.py context > DATA_MODEL.md # paste into AGENTS.md / CLAUDE.md uv run main.py --selfcheck # 30 fixtures · 9 live SQLite executions

Wire check in front of your agent's SQL tool. Exit 0 = query runs. Exit 1 = violation list returned to the agent so it can rewrite.

What the CLI catches that this demo doesn't:

  • Undeclared columns through CTEs and subquery aliases (q20, q29)
  • Joins on wrong column pairs between declared tables (q10, q22)
  • OR-injection in JOIN ON clauses (q28)
  • Quoted identifier case-sensitivity: "Orders" ≠ orders
  • Any dialect (Postgres, MySQL, BigQuery, DuckDB, …)