@pgsql/transform 18.14.1 → 18.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/esm/facts.js +5 -391
  2. package/facts.d.ts +5 -126
  3. package/facts.js +19 -392
  4. package/package.json +3 -2
package/esm/facts.js CHANGED
@@ -1,393 +1,7 @@
1
- import { walkSqlAst } from '@pgsql/traverse';
2
- import { parseSql, transformSync, walkPlpgsqlAst } from 'plpgsql-parser';
3
- const SECURITY_TAGS = new Set([
4
- 'CreatePolicyStmt',
5
- 'AlterPolicyStmt',
6
- 'GrantStmt',
7
- 'GrantRoleStmt',
8
- 'AlterDefaultPrivilegesStmt',
9
- 'SecLabelStmt',
10
- 'AlterOwnerStmt',
11
- 'CreateRoleStmt',
12
- 'AlterRoleStmt'
13
- ]);
14
- const KIND_BY_TAG = {
15
- CreateSchemaStmt: 'schema',
16
- CreateExtensionStmt: 'extension',
17
- CreateStmt: 'table',
18
- ViewStmt: 'view',
19
- IndexStmt: 'index',
20
- CompositeTypeStmt: 'type',
21
- CreateEnumStmt: 'type',
22
- CreateDomainStmt: 'type',
23
- CreateRangeStmt: 'type',
24
- DefineStmt: 'type',
25
- CreateFunctionStmt: 'function',
26
- CreateTrigStmt: 'trigger',
27
- CreateEventTrigStmt: 'trigger',
28
- CreatePolicyStmt: 'policy',
29
- AlterPolicyStmt: 'policy',
30
- GrantStmt: 'grant',
31
- GrantRoleStmt: 'grant',
32
- AlterDefaultPrivilegesStmt: 'grant',
33
- CommentStmt: 'comment',
34
- InsertStmt: 'seed_dml',
35
- UpdateStmt: 'seed_dml',
36
- DeleteStmt: 'seed_dml'
37
- };
38
- function qn(schema, name) {
39
- return { schema: schema ?? null, name };
40
- }
41
1
  /**
42
- * Read the `SCHEMA <name>` clause of a `CreateExtensionStmt` from its options
43
- * (`{ DefElem: { defname: 'schema', arg: { String: { sval } } } }`), or `null`
44
- * when the statement specifies no schema.
2
+ * The statement-classification facts layer now lives in `@pgsql/semantics`.
3
+ * It is re-exported here so existing `@pgsql/transform` consumers — and this
4
+ * package's own drivers (`qualify`, `restructure`, `naming`, `graph`) — keep
5
+ * importing the same symbols from the same paths.
45
6
  */
46
- function extensionSchemaOption(options) {
47
- if (!Array.isArray(options))
48
- return null;
49
- for (const opt of options) {
50
- const def = opt?.DefElem;
51
- if (def?.defname === 'schema') {
52
- const sval = def.arg?.String?.sval;
53
- return typeof sval === 'string' ? sval : null;
54
- }
55
- }
56
- return null;
57
- }
58
- function nameListToQualified(names) {
59
- if (!Array.isArray(names) || names.length === 0)
60
- return null;
61
- const parts = names
62
- .map((n) => n?.String?.sval)
63
- .filter((s) => typeof s === 'string');
64
- if (parts.length === 0)
65
- return null;
66
- if (parts.length === 1)
67
- return qn(null, parts[0]);
68
- return qn(parts[parts.length - 2], parts[parts.length - 1]);
69
- }
70
- function isCatalogSchema(schema) {
71
- return schema === 'pg_catalog' || schema === 'information_schema';
72
- }
73
- function pushRef(refs, ref) {
74
- if (!ref || !ref.schema || isCatalogSchema(ref.schema))
75
- return;
76
- if (refs.some(r => r.schema === ref.schema && r.name === ref.name))
77
- return;
78
- refs.push(ref);
79
- }
80
- function collectRoles(node, roles) {
81
- const push = (role) => {
82
- if (typeof role === 'string' && role.length > 0 && !roles.includes(role)) {
83
- roles.push(role);
84
- }
85
- };
86
- if (Array.isArray(node?.grantees)) {
87
- for (const g of node.grantees)
88
- push(g?.RoleSpec?.rolename);
89
- }
90
- if (Array.isArray(node?.roles)) {
91
- for (const r of node.roles)
92
- push(r?.RoleSpec?.rolename);
93
- }
94
- push(node?.newowner?.rolename);
95
- push(node?.role?.rolename);
96
- }
97
- /**
98
- * Create a read-only visitor that accumulates references, roles and FK
99
- * targets into the provided facts object.
100
- */
101
- function createFactsVisitor(facts, bodyRefs) {
102
- const push = (ref) => {
103
- pushRef(facts.references, ref);
104
- if (bodyRefs)
105
- pushRef(bodyRefs, ref);
106
- };
107
- return {
108
- RangeVar: (path) => {
109
- const node = path.node;
110
- if (node.schemaname) {
111
- push(qn(node.schemaname, node.relname));
112
- }
113
- },
114
- FuncCall: (path) => {
115
- push(nameListToQualified(path.node.funcname));
116
- },
117
- TypeName: (path) => {
118
- push(nameListToQualified(path.node.names));
119
- },
120
- ColumnRef: (path) => {
121
- // schema.table.column references carry cross-schema information
122
- const fields = path.node.fields;
123
- if (Array.isArray(fields) && fields.length >= 3) {
124
- const parts = fields
125
- .map((f) => f?.String?.sval)
126
- .filter((s) => typeof s === 'string');
127
- if (parts.length >= 3) {
128
- push(qn(parts[0], parts[1]));
129
- }
130
- }
131
- },
132
- Constraint: (path) => {
133
- const node = path.node;
134
- if (node.contype === 'CONSTR_FOREIGN' && node.pktable) {
135
- const target = qn(node.pktable.schemaname ?? null, node.pktable.relname);
136
- if (!facts.fkTargets.some(t => t.schema === target.schema && t.name === target.name)) {
137
- facts.fkTargets.push(target);
138
- }
139
- if (target.schema)
140
- pushRef(facts.references, target);
141
- }
142
- }
143
- };
144
- }
145
- function classifyOne(nodeTag, node) {
146
- const facts = {
147
- kind: KIND_BY_TAG[nodeTag] ?? 'other',
148
- nodeTag,
149
- creates: [],
150
- references: [],
151
- referencedSchemas: [],
152
- roles: [],
153
- fkTargets: [],
154
- bodyReferences: [],
155
- securityRelevant: SECURITY_TAGS.has(nodeTag),
156
- securityDefiner: false,
157
- dynamicSql: false,
158
- span: { start: 0, len: 0 }
159
- };
160
- switch (nodeTag) {
161
- case 'CreateSchemaStmt':
162
- facts.creates.push(qn(null, node.schemaname));
163
- break;
164
- case 'CreateExtensionStmt':
165
- facts.extension = {
166
- name: node.extname,
167
- schema: extensionSchemaOption(node.options),
168
- action: 'create',
169
- ifNotExists: node.if_not_exists === true
170
- };
171
- break;
172
- case 'CreateStmt':
173
- case 'ViewStmt': {
174
- const rel = nodeTag === 'ViewStmt' ? node.view : node.relation;
175
- if (rel)
176
- facts.creates.push(qn(rel.schemaname ?? null, rel.relname));
177
- break;
178
- }
179
- case 'IndexStmt':
180
- if (node.relation) {
181
- facts.creates.push(qn(node.relation.schemaname ?? null, node.idxname ?? node.relation.relname));
182
- }
183
- break;
184
- case 'CreateFunctionStmt': {
185
- const name = nameListToQualified(node.funcname);
186
- if (name)
187
- facts.creates.push(name);
188
- for (const opt of node.options ?? []) {
189
- const def = opt?.DefElem;
190
- if (def?.defname === 'security' && def?.arg?.Boolean?.boolval === true) {
191
- facts.securityDefiner = true;
192
- }
193
- }
194
- break;
195
- }
196
- case 'CreateTrigStmt':
197
- if (node.relation) {
198
- // Trigger names are only unique per table; qualify with the table.
199
- facts.creates.push(qn(node.relation.schemaname ?? null, `${node.relation.relname}.${node.trigname}`));
200
- }
201
- pushRef(facts.references, nameListToQualified(node.funcname));
202
- break;
203
- case 'CreatePolicyStmt':
204
- case 'AlterPolicyStmt':
205
- if (node.table) {
206
- // Policy names are only unique per table; qualify with the table.
207
- facts.creates.push(qn(node.table.schemaname ?? null, `${node.table.relname}.${node.policy_name}`));
208
- // The guarded table lives on `node.table`; the generic RangeVar walker
209
- // does not descend into it, so capture the reference explicitly.
210
- if (node.table.schemaname) {
211
- pushRef(facts.references, qn(node.table.schemaname, node.table.relname));
212
- }
213
- }
214
- break;
215
- case 'CreateSeqStmt':
216
- if (node.sequence) {
217
- facts.creates.push(qn(node.sequence.schemaname ?? null, node.sequence.relname));
218
- }
219
- break;
220
- case 'CompositeTypeStmt':
221
- if (node.typevar) {
222
- facts.creates.push(qn(node.typevar.schemaname ?? null, node.typevar.relname));
223
- }
224
- break;
225
- case 'CreateEnumStmt':
226
- case 'CreateRangeStmt': {
227
- const name = nameListToQualified(node.typeName);
228
- if (name)
229
- facts.creates.push(name);
230
- break;
231
- }
232
- case 'CreateDomainStmt': {
233
- // `typeName` on a domain is the base type; the name is `domainname`.
234
- const name = nameListToQualified(node.domainname);
235
- if (name)
236
- facts.creates.push(name);
237
- break;
238
- }
239
- case 'AlterObjectSchemaStmt':
240
- // ALTER EXTENSION <name> SET SCHEMA <newschema>. Other object types
241
- // (TABLE/TYPE/FUNCTION ... SET SCHEMA) keep their default classification.
242
- if (node.objectType === 'OBJECT_EXTENSION') {
243
- const name = node.object?.String?.sval;
244
- if (typeof name === 'string') {
245
- facts.kind = 'extension';
246
- facts.extension = {
247
- name,
248
- schema: typeof node.newschema === 'string' ? node.newschema : null,
249
- action: 'set_schema'
250
- };
251
- }
252
- }
253
- break;
254
- case 'DropStmt':
255
- // DROP EXTENSION <name> [, ...]. Extension names are bare String nodes.
256
- // Other DROP object types keep their default classification. When
257
- // several extensions are dropped in one statement the first names the
258
- // fact; the full list stays available via the raw node.
259
- if (node.removeType === 'OBJECT_EXTENSION' && Array.isArray(node.objects)) {
260
- const name = node.objects[0]?.String?.sval;
261
- if (typeof name === 'string') {
262
- facts.kind = 'extension';
263
- facts.extension = { name, schema: null, action: 'drop' };
264
- }
265
- }
266
- break;
267
- case 'AlterTableStmt': {
268
- if (node.relation) {
269
- facts.creates.push(qn(node.relation.schemaname ?? null, node.relation.relname));
270
- }
271
- const cmds = node.cmds ?? [];
272
- for (const cmd of cmds) {
273
- const subtype = cmd?.AlterTableCmd?.subtype;
274
- if (subtype === 'AT_EnableRowSecurity' || subtype === 'AT_ForceRowSecurity' ||
275
- subtype === 'AT_DisableRowSecurity' || subtype === 'AT_NoForceRowSecurity') {
276
- facts.kind = 'rls_enable';
277
- facts.securityRelevant = true;
278
- }
279
- else if (subtype === 'AT_AddConstraint') {
280
- const contype = cmd?.AlterTableCmd?.def?.Constraint?.contype;
281
- facts.kind = contype === 'CONSTR_FOREIGN' ? 'fk_constraint' : 'constraint';
282
- }
283
- else if (subtype === 'AT_ChangeOwner') {
284
- facts.securityRelevant = true;
285
- const owner = cmd?.AlterTableCmd?.newowner?.rolename;
286
- if (owner && !facts.roles.includes(owner))
287
- facts.roles.push(owner);
288
- }
289
- }
290
- break;
291
- }
292
- case 'InsertStmt':
293
- case 'UpdateStmt':
294
- case 'DeleteStmt':
295
- if (node.relation) {
296
- facts.creates.push(qn(node.relation.schemaname ?? null, node.relation.relname));
297
- }
298
- break;
299
- default:
300
- break;
301
- }
302
- collectRoles(node, facts.roles);
303
- return facts;
304
- }
305
- /** Read a `CreateFunctionStmt` DefElem option's scalar/list value. */
306
- function functionOption(node, defname) {
307
- for (const opt of node.options ?? []) {
308
- if (opt?.DefElem?.defname === defname)
309
- return opt.DefElem.arg;
310
- }
311
- return undefined;
312
- }
313
- /**
314
- * Collect references from a `LANGUAGE sql` function body supplied as a string
315
- * literal (`AS $$ ... $$`). That body is an opaque String node the AST walker
316
- * never parses, so — mirroring the schema transformer's body rewrite — parse
317
- * it standalone and walk each statement with the facts visitor. The standard
318
- * `BEGIN ATOMIC` / `RETURN` `sql_body` form is already part of the AST and is
319
- * covered by the outer walk, so only the string form needs this.
320
- */
321
- function collectSqlBodyReferences(node, facts) {
322
- const language = functionOption(node, 'language')?.String?.sval;
323
- if (typeof language !== 'string' || language.toLowerCase() !== 'sql')
324
- return;
325
- const asArg = functionOption(node, 'as');
326
- const items = asArg?.List?.items ?? [];
327
- const body = items[0]?.String?.sval;
328
- if (typeof body !== 'string')
329
- return;
330
- try {
331
- const stmts = parseSql(body)?.stmts ?? [];
332
- const visitor = createFactsVisitor(facts, facts.bodyReferences);
333
- for (const stmt of stmts) {
334
- if (stmt?.stmt)
335
- walkSqlAst(stmt.stmt, visitor);
336
- }
337
- }
338
- catch {
339
- // A non-parseable body (C symbol name, etc.) contributes no references.
340
- }
341
- }
342
- /**
343
- * Classify each top-level statement in a SQL script into {@link StatementFacts}.
344
- *
345
- * Uses the same parser stack as the schema transformer (pgsql AST walk plus
346
- * hydrated PL/pgSQL body walk), so references inside function bodies are
347
- * included. The input SQL is never modified.
348
- */
349
- export function classifyStatements(sql) {
350
- const allFacts = [];
351
- transformSync(sql, (ctx) => {
352
- const stmts = ctx.sql?.stmts ?? [];
353
- for (const stmt of stmts) {
354
- const stmtNode = stmt?.stmt;
355
- const nodeTag = stmtNode ? Object.keys(stmtNode)[0] : 'other';
356
- const node = stmtNode?.[nodeTag] ?? {};
357
- const facts = classifyOne(nodeTag, node);
358
- const start = stmt?.stmt_location ?? 0;
359
- facts.span = { start, len: stmt?.stmt_len ?? Math.max(0, sql.length - start) };
360
- if (stmtNode) {
361
- facts.stmt = stmtNode;
362
- walkSqlAst(stmtNode, createFactsVisitor(facts));
363
- }
364
- if (nodeTag === 'CreateFunctionStmt') {
365
- collectSqlBodyReferences(node, facts);
366
- }
367
- allFacts.push(facts);
368
- }
369
- for (const fn of ctx.functions ?? []) {
370
- const facts = allFacts[fn.stmtIndex];
371
- if (!facts || !fn.plpgsql?.hydrated)
372
- continue;
373
- // Refs already found outside the body (signature types, defaults)
374
- // constrain deploy order and are not body-only.
375
- const outer = new Set(facts.references.map(r => `${r.schema ?? '?'}.${r.name}`));
376
- walkPlpgsqlAst(fn.plpgsql.hydrated, {
377
- PLpgSQL_stmt_dynexecute: () => { facts.dynamicSql = true; },
378
- PLpgSQL_stmt_dynfors: () => { facts.dynamicSql = true; }
379
- }, {
380
- walkSqlExpressions: true,
381
- sqlVisitor: createFactsVisitor(facts, facts.bodyReferences)
382
- });
383
- facts.bodyReferences = facts.bodyReferences.filter(r => !outer.has(`${r.schema ?? '?'}.${r.name}`));
384
- }
385
- }, { hydrate: true });
386
- for (const facts of allFacts) {
387
- const selfRef = (r) => facts.creates.some(c => c.schema === r.schema && c.name === r.name);
388
- facts.references = facts.references.filter(r => !selfRef(r));
389
- facts.bodyReferences = facts.bodyReferences.filter(r => !selfRef(r));
390
- facts.referencedSchemas = [...new Set(facts.references.map(r => r.schema).filter(Boolean))];
391
- }
392
- return allFacts;
393
- }
7
+ export * from '@pgsql/semantics';
package/facts.d.ts CHANGED
@@ -1,128 +1,7 @@
1
1
  /**
2
- * A (possibly schema-qualified) object name extracted from a statement.
2
+ * The statement-classification facts layer now lives in `@pgsql/semantics`.
3
+ * It is re-exported here so existing `@pgsql/transform` consumers — and this
4
+ * package's own drivers (`qualify`, `restructure`, `naming`, `graph`) — keep
5
+ * importing the same symbols from the same paths.
3
6
  */
4
- export interface QualifiedName {
5
- schema: string | null;
6
- name: string;
7
- }
8
- /**
9
- * Coarse statement classification used for tier/package sorting.
10
- */
11
- export type StatementKind = 'schema' | 'extension' | 'table' | 'view' | 'index' | 'type' | 'function' | 'trigger' | 'policy' | 'grant' | 'rls_enable' | 'fk_constraint' | 'constraint' | 'comment' | 'seed_dml' | 'other';
12
- /**
13
- * The action a statement performs on a PostgreSQL extension.
14
- *
15
- * - `create` — `CREATE EXTENSION` (`CreateExtensionStmt`).
16
- * - `set_schema` — `ALTER EXTENSION ... SET SCHEMA` (`AlterObjectSchemaStmt`
17
- * with `objectType: OBJECT_EXTENSION`); only succeeds for relocatable
18
- * extensions, so it is surfaced as its own action.
19
- * - `drop` — `DROP EXTENSION` (`DropStmt` with `removeType: OBJECT_EXTENSION`).
20
- */
21
- export type ExtensionAction = 'create' | 'set_schema' | 'drop';
22
- /**
23
- * Facts about an extension-level statement. Unlike ordinary objects, an
24
- * extension is installed into exactly one schema and its member objects are
25
- * renamed with it, so the relevant fact is the extension name plus the schema
26
- * it is (being) placed in.
27
- */
28
- export interface ExtensionFact {
29
- /** The extension name (`CREATE EXTENSION <name>`). */
30
- name: string;
31
- /**
32
- * The schema the statement places the extension in, or `null` when none is
33
- * specified (`CREATE EXTENSION <name>` with no `SCHEMA` clause installs into
34
- * the current default — typically `public` or the extension's fixed schema).
35
- * `DROP EXTENSION` carries no schema.
36
- */
37
- schema: string | null;
38
- /** Which extension operation this statement performs. */
39
- action: ExtensionAction;
40
- /** `CREATE EXTENSION IF NOT EXISTS`. */
41
- ifNotExists?: boolean;
42
- }
43
- /**
44
- * AST-derived facts about a single top-level SQL statement.
45
- *
46
- * Read-only: classification never modifies the statement. Facts are the
47
- * substrate for classifier-driven slicing (schema / functionality / security
48
- * tiers) — replacing path/name-glob decisions with computed properties like
49
- * "this trigger function references billing".
50
- */
51
- export interface StatementFacts {
52
- /** Coarse category of the statement. */
53
- kind: StatementKind;
54
- /** The raw parser node tag (e.g. `CreateStmt`, `CreatePolicyStmt`). */
55
- nodeTag: string;
56
- /** Objects this statement creates or directly targets. */
57
- creates: QualifiedName[];
58
- /**
59
- * Schema-qualified objects this statement references — tables, functions
60
- * and types reached anywhere in the statement, including PL/pgSQL bodies.
61
- * Unqualified references are omitted (they resolve via search_path and
62
- * carry no cross-schema information).
63
- */
64
- references: QualifiedName[];
65
- /**
66
- * The subset of `references` reached only inside a PL/pgSQL body. These
67
- * are late-binding: Postgres resolves them at call time, not at CREATE
68
- * time, so they do not constrain deploy order (and legitimately form
69
- * recursion cycles between functions).
70
- */
71
- bodyReferences: QualifiedName[];
72
- /** Distinct schemas reached by `references`. */
73
- referencedSchemas: string[];
74
- /** Role names granted to, owning, or bound by this statement. */
75
- roles: string[];
76
- /**
77
- * For extension-level statements (`kind: 'extension'`): the extension being
78
- * created, relocated, or dropped. Absent for every other statement.
79
- */
80
- extension?: ExtensionFact;
81
- /** Foreign-key target tables (from column/table FK constraints). */
82
- fkTargets: QualifiedName[];
83
- /**
84
- * Whether the statement is part of the security surface: policies, grants,
85
- * RLS enable/force, security labels, ownership.
86
- */
87
- securityRelevant: boolean;
88
- /** For functions: declared with SECURITY DEFINER. */
89
- securityDefiner: boolean;
90
- /**
91
- * For functions: the body executes dynamic SQL (EXECUTE / EXECUTE ... USING
92
- * / FOR ... IN EXECUTE). Analogous to `eval` — references inside the
93
- * dynamic string are invisible to the AST, so edges from this statement
94
- * are incomplete and slicing should treat it conservatively.
95
- */
96
- dynamicSql: boolean;
97
- /**
98
- * The statement's source span in the classified script, as reported by the
99
- * parser: `start` is the byte offset of the statement's first token, `len`
100
- * runs to the end of the statement (the parser excludes the trailing `;`;
101
- * for the final statement the span extends to the end of the script).
102
- * `sql.slice(span.start, span.start + span.len)` is the statement's
103
- * verbatim source, so consumers can carry original text alongside the
104
- * facts without a second parse.
105
- */
106
- span: StatementSpan;
107
- /**
108
- * The raw parsed statement node (`{ CreateStmt: {...} }` etc.), exactly as
109
- * the parser produced it. Facts are still read-only — the node is carried
110
- * so consumers like `revertFor`/`verifyFor` can derive inverse or
111
- * existence-check statements without a second parse. Absent only for
112
- * facts constructed by hand.
113
- */
114
- stmt?: Record<string, any>;
115
- }
116
- /** A statement's location in the source script (byte offsets). */
117
- export interface StatementSpan {
118
- start: number;
119
- len: number;
120
- }
121
- /**
122
- * Classify each top-level statement in a SQL script into {@link StatementFacts}.
123
- *
124
- * Uses the same parser stack as the schema transformer (pgsql AST walk plus
125
- * hydrated PL/pgSQL body walk), so references inside function bodies are
126
- * included. The input SQL is never modified.
127
- */
128
- export declare function classifyStatements(sql: string): StatementFacts[];
7
+ export * from '@pgsql/semantics';
package/facts.js CHANGED
@@ -1,396 +1,23 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.classifyStatements = classifyStatements;
4
- const traverse_1 = require("@pgsql/traverse");
5
- const plpgsql_parser_1 = require("plpgsql-parser");
6
- const SECURITY_TAGS = new Set([
7
- 'CreatePolicyStmt',
8
- 'AlterPolicyStmt',
9
- 'GrantStmt',
10
- 'GrantRoleStmt',
11
- 'AlterDefaultPrivilegesStmt',
12
- 'SecLabelStmt',
13
- 'AlterOwnerStmt',
14
- 'CreateRoleStmt',
15
- 'AlterRoleStmt'
16
- ]);
17
- const KIND_BY_TAG = {
18
- CreateSchemaStmt: 'schema',
19
- CreateExtensionStmt: 'extension',
20
- CreateStmt: 'table',
21
- ViewStmt: 'view',
22
- IndexStmt: 'index',
23
- CompositeTypeStmt: 'type',
24
- CreateEnumStmt: 'type',
25
- CreateDomainStmt: 'type',
26
- CreateRangeStmt: 'type',
27
- DefineStmt: 'type',
28
- CreateFunctionStmt: 'function',
29
- CreateTrigStmt: 'trigger',
30
- CreateEventTrigStmt: 'trigger',
31
- CreatePolicyStmt: 'policy',
32
- AlterPolicyStmt: 'policy',
33
- GrantStmt: 'grant',
34
- GrantRoleStmt: 'grant',
35
- AlterDefaultPrivilegesStmt: 'grant',
36
- CommentStmt: 'comment',
37
- InsertStmt: 'seed_dml',
38
- UpdateStmt: 'seed_dml',
39
- DeleteStmt: 'seed_dml'
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
40
15
  };
41
- function qn(schema, name) {
42
- return { schema: schema ?? null, name };
43
- }
44
- /**
45
- * Read the `SCHEMA <name>` clause of a `CreateExtensionStmt` from its options
46
- * (`{ DefElem: { defname: 'schema', arg: { String: { sval } } } }`), or `null`
47
- * when the statement specifies no schema.
48
- */
49
- function extensionSchemaOption(options) {
50
- if (!Array.isArray(options))
51
- return null;
52
- for (const opt of options) {
53
- const def = opt?.DefElem;
54
- if (def?.defname === 'schema') {
55
- const sval = def.arg?.String?.sval;
56
- return typeof sval === 'string' ? sval : null;
57
- }
58
- }
59
- return null;
60
- }
61
- function nameListToQualified(names) {
62
- if (!Array.isArray(names) || names.length === 0)
63
- return null;
64
- const parts = names
65
- .map((n) => n?.String?.sval)
66
- .filter((s) => typeof s === 'string');
67
- if (parts.length === 0)
68
- return null;
69
- if (parts.length === 1)
70
- return qn(null, parts[0]);
71
- return qn(parts[parts.length - 2], parts[parts.length - 1]);
72
- }
73
- function isCatalogSchema(schema) {
74
- return schema === 'pg_catalog' || schema === 'information_schema';
75
- }
76
- function pushRef(refs, ref) {
77
- if (!ref || !ref.schema || isCatalogSchema(ref.schema))
78
- return;
79
- if (refs.some(r => r.schema === ref.schema && r.name === ref.name))
80
- return;
81
- refs.push(ref);
82
- }
83
- function collectRoles(node, roles) {
84
- const push = (role) => {
85
- if (typeof role === 'string' && role.length > 0 && !roles.includes(role)) {
86
- roles.push(role);
87
- }
88
- };
89
- if (Array.isArray(node?.grantees)) {
90
- for (const g of node.grantees)
91
- push(g?.RoleSpec?.rolename);
92
- }
93
- if (Array.isArray(node?.roles)) {
94
- for (const r of node.roles)
95
- push(r?.RoleSpec?.rolename);
96
- }
97
- push(node?.newowner?.rolename);
98
- push(node?.role?.rolename);
99
- }
100
- /**
101
- * Create a read-only visitor that accumulates references, roles and FK
102
- * targets into the provided facts object.
103
- */
104
- function createFactsVisitor(facts, bodyRefs) {
105
- const push = (ref) => {
106
- pushRef(facts.references, ref);
107
- if (bodyRefs)
108
- pushRef(bodyRefs, ref);
109
- };
110
- return {
111
- RangeVar: (path) => {
112
- const node = path.node;
113
- if (node.schemaname) {
114
- push(qn(node.schemaname, node.relname));
115
- }
116
- },
117
- FuncCall: (path) => {
118
- push(nameListToQualified(path.node.funcname));
119
- },
120
- TypeName: (path) => {
121
- push(nameListToQualified(path.node.names));
122
- },
123
- ColumnRef: (path) => {
124
- // schema.table.column references carry cross-schema information
125
- const fields = path.node.fields;
126
- if (Array.isArray(fields) && fields.length >= 3) {
127
- const parts = fields
128
- .map((f) => f?.String?.sval)
129
- .filter((s) => typeof s === 'string');
130
- if (parts.length >= 3) {
131
- push(qn(parts[0], parts[1]));
132
- }
133
- }
134
- },
135
- Constraint: (path) => {
136
- const node = path.node;
137
- if (node.contype === 'CONSTR_FOREIGN' && node.pktable) {
138
- const target = qn(node.pktable.schemaname ?? null, node.pktable.relname);
139
- if (!facts.fkTargets.some(t => t.schema === target.schema && t.name === target.name)) {
140
- facts.fkTargets.push(target);
141
- }
142
- if (target.schema)
143
- pushRef(facts.references, target);
144
- }
145
- }
146
- };
147
- }
148
- function classifyOne(nodeTag, node) {
149
- const facts = {
150
- kind: KIND_BY_TAG[nodeTag] ?? 'other',
151
- nodeTag,
152
- creates: [],
153
- references: [],
154
- referencedSchemas: [],
155
- roles: [],
156
- fkTargets: [],
157
- bodyReferences: [],
158
- securityRelevant: SECURITY_TAGS.has(nodeTag),
159
- securityDefiner: false,
160
- dynamicSql: false,
161
- span: { start: 0, len: 0 }
162
- };
163
- switch (nodeTag) {
164
- case 'CreateSchemaStmt':
165
- facts.creates.push(qn(null, node.schemaname));
166
- break;
167
- case 'CreateExtensionStmt':
168
- facts.extension = {
169
- name: node.extname,
170
- schema: extensionSchemaOption(node.options),
171
- action: 'create',
172
- ifNotExists: node.if_not_exists === true
173
- };
174
- break;
175
- case 'CreateStmt':
176
- case 'ViewStmt': {
177
- const rel = nodeTag === 'ViewStmt' ? node.view : node.relation;
178
- if (rel)
179
- facts.creates.push(qn(rel.schemaname ?? null, rel.relname));
180
- break;
181
- }
182
- case 'IndexStmt':
183
- if (node.relation) {
184
- facts.creates.push(qn(node.relation.schemaname ?? null, node.idxname ?? node.relation.relname));
185
- }
186
- break;
187
- case 'CreateFunctionStmt': {
188
- const name = nameListToQualified(node.funcname);
189
- if (name)
190
- facts.creates.push(name);
191
- for (const opt of node.options ?? []) {
192
- const def = opt?.DefElem;
193
- if (def?.defname === 'security' && def?.arg?.Boolean?.boolval === true) {
194
- facts.securityDefiner = true;
195
- }
196
- }
197
- break;
198
- }
199
- case 'CreateTrigStmt':
200
- if (node.relation) {
201
- // Trigger names are only unique per table; qualify with the table.
202
- facts.creates.push(qn(node.relation.schemaname ?? null, `${node.relation.relname}.${node.trigname}`));
203
- }
204
- pushRef(facts.references, nameListToQualified(node.funcname));
205
- break;
206
- case 'CreatePolicyStmt':
207
- case 'AlterPolicyStmt':
208
- if (node.table) {
209
- // Policy names are only unique per table; qualify with the table.
210
- facts.creates.push(qn(node.table.schemaname ?? null, `${node.table.relname}.${node.policy_name}`));
211
- // The guarded table lives on `node.table`; the generic RangeVar walker
212
- // does not descend into it, so capture the reference explicitly.
213
- if (node.table.schemaname) {
214
- pushRef(facts.references, qn(node.table.schemaname, node.table.relname));
215
- }
216
- }
217
- break;
218
- case 'CreateSeqStmt':
219
- if (node.sequence) {
220
- facts.creates.push(qn(node.sequence.schemaname ?? null, node.sequence.relname));
221
- }
222
- break;
223
- case 'CompositeTypeStmt':
224
- if (node.typevar) {
225
- facts.creates.push(qn(node.typevar.schemaname ?? null, node.typevar.relname));
226
- }
227
- break;
228
- case 'CreateEnumStmt':
229
- case 'CreateRangeStmt': {
230
- const name = nameListToQualified(node.typeName);
231
- if (name)
232
- facts.creates.push(name);
233
- break;
234
- }
235
- case 'CreateDomainStmt': {
236
- // `typeName` on a domain is the base type; the name is `domainname`.
237
- const name = nameListToQualified(node.domainname);
238
- if (name)
239
- facts.creates.push(name);
240
- break;
241
- }
242
- case 'AlterObjectSchemaStmt':
243
- // ALTER EXTENSION <name> SET SCHEMA <newschema>. Other object types
244
- // (TABLE/TYPE/FUNCTION ... SET SCHEMA) keep their default classification.
245
- if (node.objectType === 'OBJECT_EXTENSION') {
246
- const name = node.object?.String?.sval;
247
- if (typeof name === 'string') {
248
- facts.kind = 'extension';
249
- facts.extension = {
250
- name,
251
- schema: typeof node.newschema === 'string' ? node.newschema : null,
252
- action: 'set_schema'
253
- };
254
- }
255
- }
256
- break;
257
- case 'DropStmt':
258
- // DROP EXTENSION <name> [, ...]. Extension names are bare String nodes.
259
- // Other DROP object types keep their default classification. When
260
- // several extensions are dropped in one statement the first names the
261
- // fact; the full list stays available via the raw node.
262
- if (node.removeType === 'OBJECT_EXTENSION' && Array.isArray(node.objects)) {
263
- const name = node.objects[0]?.String?.sval;
264
- if (typeof name === 'string') {
265
- facts.kind = 'extension';
266
- facts.extension = { name, schema: null, action: 'drop' };
267
- }
268
- }
269
- break;
270
- case 'AlterTableStmt': {
271
- if (node.relation) {
272
- facts.creates.push(qn(node.relation.schemaname ?? null, node.relation.relname));
273
- }
274
- const cmds = node.cmds ?? [];
275
- for (const cmd of cmds) {
276
- const subtype = cmd?.AlterTableCmd?.subtype;
277
- if (subtype === 'AT_EnableRowSecurity' || subtype === 'AT_ForceRowSecurity' ||
278
- subtype === 'AT_DisableRowSecurity' || subtype === 'AT_NoForceRowSecurity') {
279
- facts.kind = 'rls_enable';
280
- facts.securityRelevant = true;
281
- }
282
- else if (subtype === 'AT_AddConstraint') {
283
- const contype = cmd?.AlterTableCmd?.def?.Constraint?.contype;
284
- facts.kind = contype === 'CONSTR_FOREIGN' ? 'fk_constraint' : 'constraint';
285
- }
286
- else if (subtype === 'AT_ChangeOwner') {
287
- facts.securityRelevant = true;
288
- const owner = cmd?.AlterTableCmd?.newowner?.rolename;
289
- if (owner && !facts.roles.includes(owner))
290
- facts.roles.push(owner);
291
- }
292
- }
293
- break;
294
- }
295
- case 'InsertStmt':
296
- case 'UpdateStmt':
297
- case 'DeleteStmt':
298
- if (node.relation) {
299
- facts.creates.push(qn(node.relation.schemaname ?? null, node.relation.relname));
300
- }
301
- break;
302
- default:
303
- break;
304
- }
305
- collectRoles(node, facts.roles);
306
- return facts;
307
- }
308
- /** Read a `CreateFunctionStmt` DefElem option's scalar/list value. */
309
- function functionOption(node, defname) {
310
- for (const opt of node.options ?? []) {
311
- if (opt?.DefElem?.defname === defname)
312
- return opt.DefElem.arg;
313
- }
314
- return undefined;
315
- }
316
- /**
317
- * Collect references from a `LANGUAGE sql` function body supplied as a string
318
- * literal (`AS $$ ... $$`). That body is an opaque String node the AST walker
319
- * never parses, so — mirroring the schema transformer's body rewrite — parse
320
- * it standalone and walk each statement with the facts visitor. The standard
321
- * `BEGIN ATOMIC` / `RETURN` `sql_body` form is already part of the AST and is
322
- * covered by the outer walk, so only the string form needs this.
323
- */
324
- function collectSqlBodyReferences(node, facts) {
325
- const language = functionOption(node, 'language')?.String?.sval;
326
- if (typeof language !== 'string' || language.toLowerCase() !== 'sql')
327
- return;
328
- const asArg = functionOption(node, 'as');
329
- const items = asArg?.List?.items ?? [];
330
- const body = items[0]?.String?.sval;
331
- if (typeof body !== 'string')
332
- return;
333
- try {
334
- const stmts = (0, plpgsql_parser_1.parseSql)(body)?.stmts ?? [];
335
- const visitor = createFactsVisitor(facts, facts.bodyReferences);
336
- for (const stmt of stmts) {
337
- if (stmt?.stmt)
338
- (0, traverse_1.walkSqlAst)(stmt.stmt, visitor);
339
- }
340
- }
341
- catch {
342
- // A non-parseable body (C symbol name, etc.) contributes no references.
343
- }
344
- }
16
+ Object.defineProperty(exports, "__esModule", { value: true });
345
17
  /**
346
- * Classify each top-level statement in a SQL script into {@link StatementFacts}.
347
- *
348
- * Uses the same parser stack as the schema transformer (pgsql AST walk plus
349
- * hydrated PL/pgSQL body walk), so references inside function bodies are
350
- * included. The input SQL is never modified.
18
+ * The statement-classification facts layer now lives in `@pgsql/semantics`.
19
+ * It is re-exported here so existing `@pgsql/transform` consumers — and this
20
+ * package's own drivers (`qualify`, `restructure`, `naming`, `graph`) — keep
21
+ * importing the same symbols from the same paths.
351
22
  */
352
- function classifyStatements(sql) {
353
- const allFacts = [];
354
- (0, plpgsql_parser_1.transformSync)(sql, (ctx) => {
355
- const stmts = ctx.sql?.stmts ?? [];
356
- for (const stmt of stmts) {
357
- const stmtNode = stmt?.stmt;
358
- const nodeTag = stmtNode ? Object.keys(stmtNode)[0] : 'other';
359
- const node = stmtNode?.[nodeTag] ?? {};
360
- const facts = classifyOne(nodeTag, node);
361
- const start = stmt?.stmt_location ?? 0;
362
- facts.span = { start, len: stmt?.stmt_len ?? Math.max(0, sql.length - start) };
363
- if (stmtNode) {
364
- facts.stmt = stmtNode;
365
- (0, traverse_1.walkSqlAst)(stmtNode, createFactsVisitor(facts));
366
- }
367
- if (nodeTag === 'CreateFunctionStmt') {
368
- collectSqlBodyReferences(node, facts);
369
- }
370
- allFacts.push(facts);
371
- }
372
- for (const fn of ctx.functions ?? []) {
373
- const facts = allFacts[fn.stmtIndex];
374
- if (!facts || !fn.plpgsql?.hydrated)
375
- continue;
376
- // Refs already found outside the body (signature types, defaults)
377
- // constrain deploy order and are not body-only.
378
- const outer = new Set(facts.references.map(r => `${r.schema ?? '?'}.${r.name}`));
379
- (0, plpgsql_parser_1.walkPlpgsqlAst)(fn.plpgsql.hydrated, {
380
- PLpgSQL_stmt_dynexecute: () => { facts.dynamicSql = true; },
381
- PLpgSQL_stmt_dynfors: () => { facts.dynamicSql = true; }
382
- }, {
383
- walkSqlExpressions: true,
384
- sqlVisitor: createFactsVisitor(facts, facts.bodyReferences)
385
- });
386
- facts.bodyReferences = facts.bodyReferences.filter(r => !outer.has(`${r.schema ?? '?'}.${r.name}`));
387
- }
388
- }, { hydrate: true });
389
- for (const facts of allFacts) {
390
- const selfRef = (r) => facts.creates.some(c => c.schema === r.schema && c.name === r.name);
391
- facts.references = facts.references.filter(r => !selfRef(r));
392
- facts.bodyReferences = facts.bodyReferences.filter(r => !selfRef(r));
393
- facts.referencedSchemas = [...new Set(facts.references.map(r => r.schema).filter(Boolean))];
394
- }
395
- return allFacts;
396
- }
23
+ __exportStar(require("@pgsql/semantics"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pgsql/transform",
3
- "version": "18.14.1",
3
+ "version": "18.15.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "AST-based SQL transformation, qualification, classification and closure analysis for PostgreSQL",
6
6
  "main": "index.js",
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@pgsql/quotes": "18.2.0",
36
+ "@pgsql/semantics": "18.1.0",
36
37
  "@pgsql/traverse": "18.7.1",
37
38
  "plpgsql-parser": "18.5.1"
38
39
  },
@@ -49,5 +50,5 @@
49
50
  "classify",
50
51
  "plpgsql"
51
52
  ],
52
- "gitHead": "4c8ecf62465ed608f068ef127d2b9f4a23844f6f"
53
+ "gitHead": "0fba1b6faf8067bfd5f1591656c2a90f77c280b0"
53
54
  }