AST-based SQL transformation, qualification, classification, and dependency-closure analysis for PostgreSQL. Works on plain SQL and inside PL/pgSQL function bodies (via plpgsql-parser hydration) — no regexes.
Looking for the PG13→17/18 version-upgrade AST transformer previously published under this name? It now lives at
@pgsql/transform-ast.
npm install @pgsql/transformThe parser runs on a WASM build of the real PostgreSQL parser; call loadModule() from plpgsql-parser once before using any synchronous API.
Rewrite schema names everywhere they can appear (DDL, DML, function bodies, trigger definitions, grants, policies, comments, type casts, string-embedded types inside PL/pgSQL, ...):
import { loadModule } from 'plpgsql-parser';
import { transformSql } from '@pgsql/transform';
await loadModule();
const mapping = new Map([['my-schema', 'my_schema']]);
const { sql } = transformSql(inputSql, mapping);import { classifyStatements } from '@pgsql/transform';
const facts = classifyStatements(sql);
// per statement: kind (schema|table|view|index|type|function|trigger|policy|grant|...),
// creates, references (incl. inside PL/pgSQL bodies), referencedSchemas, roles,
// fkTargets, securityRelevant, securityDefiner, dynamicSqlQualify unqualified object references against an inventory of known objects, with multi-schema routing support.
normalizeTree / cleanTree / validateRoundTrip — dependency-free AST normalization and mutation-aware parse→deparse→re-parse validation.
This package owns policy, not traversal. Every entry point above parses once,
then drives walk from @pgsql/traverse over the parsed script — statements and
hydrated PL/pgSQL bodies alike — with a visitor built here:
| Concern | Lives here |
|---|---|
| Which schema/role/extension a name maps to | SchemaRouter, RoleRouter, extension routes |
| What counts as a reference, a creation, a security-relevant statement | classifyStatements |
| When an unqualified name should be qualified | qualifyUnqualified |
| Whether the rewritten SQL still parses to the same tree | validateRoundTrip |
| How to reach every node of a SQL or PL/pgSQL AST | @pgsql/traverse |
So mapping logic never moves into the walker, and traversal logic never moves in
here. Some passes keep per-statement state (CTE names in qualifyUnqualified,
per-statement facts in classifyStatements) and drive the walkSqlAst /
walkPlpgsqlAst primitives directly with a visitor per statement.
Consequence for contributors: __fixtures__/output/ is the regression gate for
traversal changes made anywhere in the ecosystem. A walker change that alters
these golden files is a behavior change, not a refactor.
npm run fixtures— regenerate__fixtures__/output/golden files from__fixtures__/input/npm run scan-corpus <dir> [dir...]— audit node-type coverage over a SQL corpus