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

> Source: <https://dev.to/nur-zaman/using-an-ast-to-validate-ai-generated-postgresql-before-it-runs-1o47>
> Published: 2026-08-25 09:35:14+00:00

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](https://github.com/nur-zaman/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:

``` js
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:

``` js
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.
