← Default view

Atlas / Text-to-SQL Hardening / Query Validation

Five-Layer SQL Validation Pipeline

survey · 20 citations · 8 min read

// TL;DR Regex blocklists drop from 98% to 60% accuracy against LLM-generated queries. Intent errors masquerade as working code — most broken queries execute cleanly and return plausible wrong data. Chain five deterministic layers: statement-type allowlist → AST structural check → semantic schema binding → policy injection → read-only DB connection. All validation must run after LLM generation, in code — never delegated to a second LLM call. [1][3]

Why Single-Layer Validation Fails

60%
regex detection accuracy against LLM-generated queries — down from 98%
50K+
production queries studied — most broken ones execute cleanly and return plausible wrong data
6
distinct failure classes — each requires a different layer to catch it

The Validation Pipeline

0
Intent ClassificationPRE-SQL

Before generating SQL, classify whether the user's message warrants a database query. Off-topic, ambiguous, or schema-mismatched inputs refused early — before any SQL tokens are emitted. [3]

off-topic queries schema-mismatched inputs ambiguous intent
LatentRefusal: hidden-activation probe, 88.5% F1, ~2ms overhead [4] · "Query Carefully" parse-time unanswerable detection [18]
1
Statement-Type AllowlistDEFAULT DENY

Parse the generated SQL and enforce a default-deny policy on statement types. Regex is insufficient — use a real AST parser. [5]

DROPTRUNCATEALTER GRANT / REVOKEINSERT / UPDATE / DELETE multi-statement (;)EXEC / dynamic SQL SELECT ✓
sqlglot ⭐ 9.3k · 31 dialects · AST top-level statement type check [6]
2
AST Structural ValidationAST PARSE

Traverse the parse tree against what this user's role is authorized to see. Covers SELECT, WHERE, JOIN ON, subqueries, and CTEs — catches hallucinated fields immediately. [7]

unauthorized tableshallucinated columns PII in projections / CTEsfinancial column exposure
sqlglot sqlparse pg_query sqlparse is non-validating — will miss edge-case constructs [9]
3
Semantic Schema Binding + Compile GateSEMANTIC

Catalog-aware validator resolves every reference against live DB metadata. Engine-native dry-run (PARSEONLY / EXPLAIN) catches dialect-specific syntax, permission grants, and insane cost estimates. [10]

UNKNOWN_COLUMNAMBIGUOUS_COLUMN TYPE_MISMATCHMISSING_REQUIRED_FILTER CARTESIAN_JOINRESTRICTED_COLUMN
SET PARSEONLY ON EXPLAIN returns allow / deny / warn / repair codes with structured error info [11]
4
Policy InjectionAST REWRITE

Do not trust the LLM to consistently include required filters. Mechanically rewrite the parsed AST — policies evaluated against the AST, never raw prompt text. A regex-matched tenant_id in the user message is not an enforced predicate. [7]

missing tenant_id filterunbounded time windowsmissing status='active'
WHERE tenant_id = :ctx.tenant ✓ AND created_at >= :ctx.start_date ✓
sqlglot transform() survives alias and CTE resolution [6]
5
Database-Level EnforcementDB BACKSTOP

Backstop if any prior layer has a bug. Physical write isolation — not a policy setting. Cannot be bypassed by application code. [12]

GRANT SELECT only ✓ [12] PostgreSQL / SQL Server RLS ✓ [17] read replica (physical isolation) ✓ [3] LIMIT 1000 result cap ✓ [3]
⚠ RLS has exploitable CVEs: CVE-2024-10976 (bypass below subqueries), CVE-2025-8713 (statistics leakage) — defense-in-depth, not primary control [13]

Failure Mode → Layer Matrix

Failure ClassError CodeCaught By
Schema hallucination (column/table not in DB)UNKNOWN_COLUMNLayer 3 — Semantic
Wrong join path (fabricated FK relationships)CARTESIAN_JOINLayer 3 — Semantic
Missing required filter (tenant_id, date range)MISSING_REQUIRED_FILTERLayer 3 — Semantic + Layer 4 — Policy
Wrong table selection (payments vs revenue_recognition)WRONG_SCHEMA_OBJECTLayer 3 — Semantic
Unsafe statement type (DELETE / DROP from ambiguous intent)FORBIDDEN_STMT_TYPELayer 1 — Allowlist
Syntax errors (unbalanced parens, invalid keywords)PARSE_ERRORLayer 2 — AST + Layer 3 — Compile Gate
PII column in SELECT, WHERE, CTE projectionRESTRICTED_COLUMNLayer 2 — AST
Off-topic / schema-mismatched user inputINTENT_MISMATCHLayer 0 — Intent

Implementation Reference — Layers 1 & 2 (sqlglot)

import sqlglot ALLOWED_TABLES = {"orders", "products", "customers"} FORBIDDEN_TYPES = { sqlglot.exp.Drop, sqlglot.exp.Delete, sqlglot.exp.Update, sqlglot.exp.Insert, } def validate(sql: str) -> tuple[bool, str]: parsed = sqlglot.parse_one(sql) if type(parsed) in FORBIDDEN_TYPES: return False, f"Forbidden: {type(parsed).__name__}" tables = {t.name.lower() for t in parsed.find_all(sqlglot.exp.Table)} if not tables.issubset(ALLOWED_TABLES): blocked = tables - ALLOWED_TABLES return False, f"Unauthorized tables: {blocked}" return True, "ok" # Layer 3 — Semantic dry-run (engine-native) # PostgreSQL: EXPLAIN SELECT ... → validates column refs, plans cost [10] # SQL Server: SET PARSEONLY ON; SELECT → parse phase only, no execution [11] # Both catch dialect-specific errors and permission gaps custom parsers miss.

Tools

sqlglot GitHub repository

github.com/tobymao/sqlglot — 31-dialect AST parser, mutation API, type inference · ⭐ 9.3k [6]

LibraryASTDialectsStarsBest For
sqlglot Full31 ⭐ 9.3k Traversal, mutation, type inference — layers 1–4 [6]
sqlparse PartialDB-agnostic ⭐ 3.9k Lightweight tokenization; non-validating [9]
pg_query Full (PG)PostgreSQL — Exact PG parse tree via libpg_query [19]
sqlfluff NoLinting only — SQL style / dialect linting, not safety gates

Emerging Threats

⚠ ToxicSQL Backdoor CRITICAL
79.41%
attack success rate from poisoning just 0.44% of training data. Stealthy semantic + character-level triggers generate predefined malicious SQL on target inputs while behaving normally on clean queries. [14] Published at SIGMOD 2026. [15]

Implication: fine-tuned models are not inherently safer. Application-layer validation remains mandatory regardless of model provenance or fine-tuning source.
↺ Repair Loop Escape MODERATE
2–3 max
repair attempts before escalating to human review. Returning validation errors to the LLM can produce queries that remove safety filters to avoid the error. [8]

Re-validate every repaired query with identical strictness. Treat policy violations as separate error class — return to application logic, not to the LLM.

Evidence Logging (SOC 2 / HIPAA)

Every validation decision — accepted or rejected — must produce a cryptographically-signed, timestamped record. Log the LLM prompt alongside the SQL so backdoor attacks can be traced to their trigger inputs. [5][16]
→ user identity + role context → natural language input → generated SQL (pre-injection) → policy decisions + violation codes → final executed SQL → result row count + latency → which layers accepted / rejected → repair attempt history

Implementation Ladder

▶ Crawl
  • Expose 2–3 specific business queries
  • Hard-code allowed tables/columns
  • Manually review SQL in staging for 2 weeks
▶▶ Walk
  • Role-scoped schema subset
  • Automated AST validation
  • Semantic binding enabled
  • Audit logging active
▶▶▶ Run
  • Roll out across all roles
  • SQL Compile Gate added
  • Anomaly alerts on audit stream
Do not skip Crawl. A narrow, manually-reviewed allow-set is far safer than a broad schema with "should-be-fine" LLM constraints. The use cases you don't anticipate are the ones that will expose you. [3][20]