cd /news/developer-tools/using-an-ast-to-validate-ai-generate… · home topics developer-tools article
[ARTICLE · art-109989] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Using an AST to validate AI-generated PostgreSQL before it runs

Nur Zaman has developed sql-guard, a TypeScript package that validates AI-generated PostgreSQL queries using an abstract syntax tree (AST) before execution. The tool enforces allowlists for tables and functions, rejects multi-statement queries by default, and returns structured violations to prevent unsafe database operations.

read3 min views4 publishedAug 25, 2026

If an LLM is generating PostgreSQL in your application, there is one moment worth treating separately: after the model returns SQL, but before your code calls db.query()

.

Prompt rules are useful. They can make the model more likely to produce the sort of query you want. They do not decide which tables the application is allowed to read, whether multiple statements are acceptable, or whether a function call should run.

I have been working on sql-guard, a TypeScript package for that gap. It parses PostgreSQL into an abstract syntax tree (AST), checks the tree against an explicit policy, and rejects anything it cannot validate confidently.

SQL is structured. A query may have joins, subqueries, aliases, unions, and common table expressions (CTEs). Checking raw text can catch an obvious keyword, but it cannot reliably answer what the query actually does.

For example:

SELECT * FROM public.users;

SELECT 1; DELETE FROM public.users;

WITH removed AS (
  DELETE FROM public.users
  RETURNING id
)
SELECT * FROM removed;

All three examples contain SELECT

, but they are not equivalent. The second has two statements. The third uses a data-modifying CTE. A validator needs to understand the query structure rather than look for a few strings.

An AST makes that possible. It lets the validator inspect statement types, source tables, function calls, and nested expressions. It also means an alias or CTE name cannot conceal the base table being read.

sql-guard

is built around allowlists. You state what a particular feature may use, and the validator checks the generated SQL against that list.

Here is a small policy for an assistant that can look at users and orders:

import { validate } from 'sql-guard';

const policy = {
  allowedTables: ['public.users', 'public.orders'],
  allowedFunctions: ['count', 'lower'],
};

const result = validate(
  'SELECT lower(u.email) FROM public.users AS u',
  policy,
);

if (!result.ok) {
  console.error(result.errorCode, result.violations);
  // Do not execute the SQL.
}

// Execute only after result.ok is true.

The default policy allows one SELECT

statement and no function calls. Tables and functions have to be allowed explicitly. Multi-statement input is disabled by default.

That makes the initial policy intentionally strict. A request for public.secret_users

, information_schema.tables

, or pg_catalog.pg_read_file(...)

is denied unless the policy names it.

Unqualified names such as SELECT * FROM users

can be ambiguous. By default, sql-guard

expects schema-qualified names. You can choose a simple default schema for a single-schema application:

const policy = {
  defaultSchema: 'public',
  allowedTables: ['users', 'orders'],
};

Or you can provide a resolver when the application needs more control over how names map to schema-qualified tables.

Functions need the same care. An unqualified lower(...)

and pg_catalog.current_database(...)

are separate policy entries. This lets a feature allow exactly the form it intends to use.

The package returns structured violations and an error code rather than just true

or false

. An application can log a rejected query without attempting to execute it. If a rejected query should interrupt the request immediately, assertSafeSql()

throws a SqlValidationError

with the same details.

This check is only about query shape. It does not sanitize values, execute SQL, evaluate row-level security, decide column-level permissions, or notice runtime schema changes.

The surrounding controls still matter:

If any one of those controls fails, the others should limit the damage. The SQL validator is one small boundary in that chain.

The package currently supports PostgreSQL and requires Node.js 18 or later.

── more in #developer-tools 4 stories · sorted by recency
── more on @nur zaman 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/using-an-ast-to-vali…] indexed:0 read:3min 2026-08-25 ·