@pgsql/transform 18.7.0 → 18.9.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.
package/esm/transform.js CHANGED
@@ -47,6 +47,36 @@ function createResult() {
47
47
  errors: [],
48
48
  };
49
49
  }
50
+ /**
51
+ * Routing claims: each routable site (a RangeVar, a qualified name list, a
52
+ * bare schema-name field) is resolved through the router at most once per
53
+ * transform result.
54
+ *
55
+ * The walker visits parents before children, so a statement-level handler
56
+ * with namespace context (e.g. `AlterFunctionStmt` routing its
57
+ * `ObjectWithArgs` as a `function`) claims the site before a generic child
58
+ * visitor (`ObjectWithArgs`, namespace `unknown`) reaches it — first (most
59
+ * contextual) visitor wins. Without claims a site is routed twice, which
60
+ * corrupts cyclic mappings (a→b, b→a swaps back) and applies the less precise
61
+ * namespace second.
62
+ */
63
+ const routingClaims = new WeakMap();
64
+ function claimSite(result, site, field = '*') {
65
+ let byNode = routingClaims.get(result);
66
+ if (!byNode) {
67
+ byNode = new WeakMap();
68
+ routingClaims.set(result, byNode);
69
+ }
70
+ let fields = byNode.get(site);
71
+ if (!fields) {
72
+ fields = new Set();
73
+ byNode.set(site, fields);
74
+ }
75
+ if (fields.has(field))
76
+ return false;
77
+ fields.add(field);
78
+ return true;
79
+ }
50
80
  /**
51
81
  * Transform a schema name if it exists in the mapping
52
82
  */
@@ -75,6 +105,8 @@ export function shouldTransformSchema(schemaName, schemaMapping) {
75
105
  export function transformNameList(names, schemaMapping, result, ns = 'unknown') {
76
106
  if (!names || names.length < 2)
77
107
  return;
108
+ if (!claimSite(result, names))
109
+ return;
78
110
  const router = asRouter(schemaMapping);
79
111
  // For schema-qualified names, the first element is the schema.
80
112
  const first = names[0];
@@ -110,6 +142,8 @@ export function transformSchemaNameField(container, field, schemaMapping, result
110
142
  const schemaName = container?.[field];
111
143
  if (typeof schemaName !== 'string')
112
144
  return;
145
+ if (!claimSite(result, container, field))
146
+ return;
113
147
  // A bare schema name is an operation on the schema itself: only the
114
148
  // schema-level default applies.
115
149
  const newName = asRouter(schemaMapping).resolve(schemaName, undefined, 'schema');
@@ -121,11 +155,16 @@ export function transformSchemaNameField(container, field, schemaMapping, result
121
155
  }
122
156
  /**
123
157
  * Transform a RangeVar-like relation object (has schemaname/relname fields).
124
- * Many statement nodes embed a relation that the walker does NOT auto-recurse into.
158
+ * The walker auto-recurses into embedded relations (concrete `RangeVar`
159
+ * fields are tag-synthesized), so the generic `RangeVar` visitor covers
160
+ * every occurrence; statement handlers call this directly only when they
161
+ * carry extra context, and the claim guard keeps each relation routed once.
125
162
  */
126
163
  export function transformRelation(relation, schemaMapping, result) {
127
164
  if (!relation?.schemaname)
128
165
  return;
166
+ if (!claimSite(result, relation))
167
+ return;
129
168
  const oldName = relation.schemaname;
130
169
  // A RangeVar names a relation (table/view/sequence/matview); route by the
131
170
  // relation name so object-level routes can send it to its own schema.
@@ -185,6 +224,8 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
185
224
  const schemaName = container?.[key];
186
225
  if (typeof schemaName !== 'string')
187
226
  return;
227
+ if (!claimSite(result, container, key))
228
+ return;
188
229
  const newName = router.resolve(schemaName, undefined, 'schema');
189
230
  if (newName && newName !== schemaName) {
190
231
  result.schemasFound.add(schemaName);
@@ -242,15 +283,6 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
242
283
  const node = path.node;
243
284
  transformNameList(node.funcname, router, result, 'function');
244
285
  },
245
- // Transform CallStmt (CALL schema.procedure(...)).
246
- // Note: The walker does NOT auto-recurse into CallStmt.funccall,
247
- // so the FuncCall visitor above never sees it.
248
- CallStmt: (path) => {
249
- const node = path.node;
250
- if (node.funccall?.funcname) {
251
- transformNameList(node.funccall.funcname, router, result, 'function');
252
- }
253
- },
254
286
  // Transform TypeName nodes (type references with schema-qualified names)
255
287
  TypeName: (path) => {
256
288
  const node = path.node;
@@ -308,8 +340,9 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
308
340
  }
309
341
  },
310
342
  // Transform DropStmt (DROP SCHEMA name; DROP TABLE/INDEX/POLICY/TRIGGER/
311
- // TYPE/FUNCTION schema.obj). The walker does NOT auto-recurse into
312
- // DropStmt.objects.
343
+ // TYPE/FUNCTION schema.obj) with the namespace derived from removeType,
344
+ // so object routes apply; claims stop the generic List/ObjectWithArgs
345
+ // visitors from re-routing the same names with namespace `unknown`.
313
346
  DropStmt: (path) => {
314
347
  const node = path.node;
315
348
  if (node.removeType !== 'OBJECT_SCHEMA' && Array.isArray(node.objects)) {
@@ -343,79 +376,10 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
343
376
  }
344
377
  }
345
378
  },
346
- // Transform InsertStmt (INSERT INTO schema.table)
347
- // Note: The walker does NOT auto-recurse into InsertStmt.relation.
348
- InsertStmt: (path) => {
349
- transformRelation(path.node.relation, schemaMapping, result);
350
- },
351
- // Transform CreateStmt (CREATE TABLE schema.table)
352
- // Note: The walker does NOT auto-recurse into CreateStmt.relation.
353
- CreateStmt: (path) => {
354
- transformRelation(path.node.relation, schemaMapping, result);
355
- },
356
- // Transform UpdateStmt (UPDATE schema.table SET ...)
357
- // Note: The walker does NOT auto-recurse into UpdateStmt.relation.
358
- UpdateStmt: (path) => {
359
- transformRelation(path.node.relation, schemaMapping, result);
360
- },
361
- // Transform DeleteStmt (DELETE FROM schema.table)
362
- // Note: The walker does NOT auto-recurse into DeleteStmt.relation.
363
- DeleteStmt: (path) => {
364
- transformRelation(path.node.relation, schemaMapping, result);
365
- },
366
- // Transform MergeStmt (MERGE INTO schema.table USING ...)
367
- // Note: The walker does NOT auto-recurse into MergeStmt.relation.
368
- MergeStmt: (path) => {
369
- transformRelation(path.node.relation, schemaMapping, result);
370
- },
371
- // Transform IndexStmt (CREATE INDEX ... ON schema.table)
372
- // Note: The walker does NOT auto-recurse into IndexStmt.relation.
373
- IndexStmt: (path) => {
374
- transformRelation(path.node.relation, schemaMapping, result);
375
- },
376
- // Transform Constraint (REFERENCES schema.table for foreign keys)
377
- // Note: The walker does NOT auto-recurse into Constraint.pktable.
378
- Constraint: (path) => {
379
- const node = path.node;
380
- if (node.contype === 'CONSTR_FOREIGN' && node.pktable) {
381
- transformRelation(node.pktable, schemaMapping, result);
382
- }
383
- },
384
- // Transform CompositeTypeStmt (CREATE TYPE schema.name AS (...))
385
- // Note: The walker does NOT auto-recurse into CompositeTypeStmt.typevar.
386
- CompositeTypeStmt: (path) => {
387
- transformRelation(path.node.typevar, schemaMapping, result);
388
- },
389
- // Transform ColumnDef (column definitions with schema-qualified types)
390
- // Note: The walker does NOT auto-recurse into ColumnDef.typeName.
391
- ColumnDef: (path) => {
392
- const node = path.node;
393
- if (node.typeName?.names) {
394
- transformNameList(node.typeName.names, schemaMapping, result, 'type');
395
- }
396
- },
397
- // Transform ViewStmt (CREATE VIEW schema.viewname AS ...)
398
- // Note: The walker does NOT auto-recurse into ViewStmt.view.
399
- ViewStmt: (path) => {
400
- transformRelation(path.node.view, schemaMapping, result);
401
- },
402
- // Transform AlterTableStmt (ALTER TABLE schema.table ...)
403
- // Note: The walker does NOT auto-recurse into AlterTableStmt.relation.
404
- AlterTableStmt: (path) => {
405
- transformRelation(path.node.relation, schemaMapping, result);
406
- },
407
- // Transform CreateSeqStmt (CREATE SEQUENCE schema.seqname)
408
- // Note: The walker does NOT auto-recurse into CreateSeqStmt.sequence.
409
- CreateSeqStmt: (path) => {
410
- transformRelation(path.node.sequence, schemaMapping, result);
411
- },
412
- // Transform AlterSeqStmt (ALTER SEQUENCE schema.seqname [OWNED BY schema.table.col])
413
- // Note: The walker does NOT auto-recurse into AlterSeqStmt.sequence.
414
- // Also handles OWNED BY clause where the schema name is in DefElem.arg.List.items.
379
+ // Transform AlterSeqStmt OWNED BY (schema name in DefElem.arg.List.items;
380
+ // a bare name list no generic visitor covers)
415
381
  AlterSeqStmt: (path) => {
416
382
  const node = path.node;
417
- transformRelation(node.sequence, schemaMapping, result);
418
- // Handle OWNED BY option: schema name is in the name list
419
383
  if (node.options) {
420
384
  for (const opt of node.options) {
421
385
  if (opt?.DefElem?.defname === 'owned_by' && opt.DefElem?.arg?.List?.items) {
@@ -424,67 +388,12 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
424
388
  }
425
389
  }
426
390
  },
427
- // Transform CreatePolicyStmt (CREATE POLICY ... ON schema.table)
428
- // Note: The walker does NOT auto-recurse into CreatePolicyStmt.table.
429
- CreatePolicyStmt: (path) => {
430
- transformRelation(path.node.table, schemaMapping, result);
431
- },
432
- // Transform RuleStmt (CREATE RULE ... ON schema.table)
433
- // Note: The walker does NOT auto-recurse into RuleStmt.relation.
434
- RuleStmt: (path) => {
435
- transformRelation(path.node.relation, schemaMapping, result);
436
- },
437
- // Transform CopyStmt (COPY schema.table ...)
438
- // Note: The walker does NOT auto-recurse into CopyStmt.relation.
439
- CopyStmt: (path) => {
440
- transformRelation(path.node.relation, schemaMapping, result);
441
- },
442
- // Transform ClusterStmt (CLUSTER schema.table USING index)
443
- // Note: The walker does NOT auto-recurse into ClusterStmt.relation.
444
- ClusterStmt: (path) => {
445
- transformRelation(path.node.relation, schemaMapping, result);
446
- },
447
- // Transform VacuumRelation (VACUUM/ANALYZE schema.table)
448
- // Note: The walker visits VacuumRelation but does NOT recurse into its relation.
449
- VacuumRelation: (path) => {
450
- transformRelation(path.node.relation, schemaMapping, result);
451
- },
452
- // Transform RefreshMatViewStmt (REFRESH MATERIALIZED VIEW schema.view)
453
- // Note: The walker does NOT auto-recurse into RefreshMatViewStmt.relation.
454
- RefreshMatViewStmt: (path) => {
455
- transformRelation(path.node.relation, schemaMapping, result);
456
- },
457
- // Transform CreateTableAsStmt (CREATE MATERIALIZED VIEW schema.viewname AS ...)
458
- // Note: The walker does NOT auto-recurse into CreateTableAsStmt.into.rel.
459
- CreateTableAsStmt: (path) => {
460
- transformRelation(path.node.into?.rel, schemaMapping, result);
461
- },
462
- // Transform TypeCast nodes (type casts like NULL::schema.type)
463
- // Note: The walker visits TypeCast but does NOT recurse into the TypeName node.
464
- TypeCast: (path) => {
465
- const node = path.node;
466
- if (node.typeName?.names) {
467
- transformNameList(node.typeName.names, schemaMapping, result, 'type');
468
- }
469
- },
470
- // Transform CreateFunctionStmt (CREATE FUNCTION schema.funcname)
471
- // Also handles RETURNS [SETOF] schema.type via returnType.names
391
+ // Transform CreateFunctionStmt (CREATE FUNCTION schema.funcname).
392
+ // funcname is a bare name list (no generic visitor); return and parameter
393
+ // types are TypeName nodes the walker reaches on its own.
472
394
  CreateFunctionStmt: (path) => {
473
395
  const node = path.node;
474
396
  transformNameList(node.funcname, schemaMapping, result, 'function');
475
- // Transform the return type (e.g., RETURNS SETOF schema.tablename)
476
- if (node.returnType?.names) {
477
- transformNameList(node.returnType.names, schemaMapping, result, 'type');
478
- }
479
- // Transform parameter types (the walker does NOT auto-recurse into
480
- // FunctionParameter.argType TypeName nodes)
481
- if (Array.isArray(node.parameters)) {
482
- for (const param of node.parameters) {
483
- if (param?.FunctionParameter?.argType?.names) {
484
- transformNameList(param.FunctionParameter.argType.names, schemaMapping, result, 'type');
485
- }
486
- }
487
- }
488
397
  // Transform schema-qualified references inside the function body
489
398
  // (AS $$...$$ is a String under the 'as' DefElem, which the walker
490
399
  // does not parse as SQL). LANGUAGE plpgsql bodies are additionally
@@ -511,12 +420,11 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
511
420
  }
512
421
  },
513
422
  // Transform CreateTrigStmt (CREATE TRIGGER ... EXECUTE PROCEDURE schema.func)
514
- // Note: The walker does NOT auto-recurse into CreateTrigStmt.relation,
515
- // so we must explicitly transform the RangeVar here.
423
+ // funcname is a bare name list; the ON relation is a RangeVar the walker
424
+ // reaches on its own.
516
425
  CreateTrigStmt: (path) => {
517
426
  const node = path.node;
518
427
  transformNameList(node.funcname, schemaMapping, result, 'function');
519
- transformRelation(node.relation, schemaMapping, result);
520
428
  },
521
429
  // Transform CommentStmt (COMMENT ON TABLE/COLUMN/FUNCTION schema.obj,
522
430
  // COMMENT ON SCHEMA schema)
@@ -527,13 +435,14 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
527
435
  // For functions, object is { ObjectWithArgs: { objname: [...] } }
528
436
  // For schemas, object is a bare { String: { sval } }
529
437
  if (node.object) {
438
+ const ns = namespaceOfObjectType(node.objtype);
530
439
  // Handle List-wrapped objects (COMMENT ON TABLE schema.tbl, COMMENT ON COLUMN schema.tbl.col)
531
440
  if (node.object?.List?.items) {
532
- transformNameList(node.object.List.items, schemaMapping, result);
441
+ transformNameList(node.object.List.items, schemaMapping, result, ns);
533
442
  }
534
443
  // Handle ObjectWithArgs-style objects (COMMENT ON FUNCTION schema.func(args))
535
444
  else if (node.object?.ObjectWithArgs?.objname) {
536
- transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result);
445
+ transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result, ns);
537
446
  }
538
447
  // Handle bare schema names (COMMENT ON SCHEMA schema)
539
448
  else if (node.object?.String && node.objtype === 'OBJECT_SCHEMA') {
@@ -590,41 +499,41 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
590
499
  const node = path.node;
591
500
  transformNameList(node.objname, schemaMapping, result);
592
501
  },
593
- // Transform AlterObjectSchemaStmt (ALTER ... SET SCHEMA newschema)
594
- // The walker does NOT auto-recurse into AlterObjectSchemaStmt.relation
595
- // or .object, so we must transform relation, object names, and newschema.
502
+ // Transform AlterObjectSchemaStmt (ALTER ... SET SCHEMA newschema):
503
+ // object name lists and the destination schema. The relation form is a
504
+ // RangeVar the walker reaches on its own.
596
505
  AlterObjectSchemaStmt: (path) => {
597
506
  const node = path.node;
598
- transformRelation(node.relation, schemaMapping, result);
507
+ const ns = namespaceOfObjectType(node.objectType);
599
508
  // Non-relation objects (ALTER TYPE/FUNCTION ... SET SCHEMA)
600
509
  if (node.object?.List?.items) {
601
- transformNameList(node.object.List.items, schemaMapping, result);
510
+ transformNameList(node.object.List.items, schemaMapping, result, ns);
602
511
  }
603
512
  else if (node.object?.ObjectWithArgs?.objname) {
604
- transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result);
513
+ transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result, ns);
605
514
  }
606
515
  else if (node.object?.TypeName?.names) {
607
- transformNameList(node.object.TypeName.names, schemaMapping, result);
516
+ transformNameList(node.object.TypeName.names, schemaMapping, result, ns);
608
517
  }
609
518
  // Transform the newschema (destination schema)
610
519
  transformSchemaNameField(node, 'newschema', schemaMapping, result);
611
520
  },
612
521
  // Transform RenameStmt (ALTER SCHEMA old RENAME TO new;
613
- // ALTER TABLE schema.tbl RENAME ...). The walker does NOT auto-recurse
614
- // into RenameStmt.relation or .object.
522
+ // ALTER TABLE schema.tbl RENAME ...). The relation form is a RangeVar
523
+ // the walker reaches on its own.
615
524
  RenameStmt: (path) => {
616
525
  const node = path.node;
617
- transformRelation(node.relation, schemaMapping, result);
618
526
  if (node.renameType === 'OBJECT_SCHEMA') {
619
527
  // subname is the old schema name; newname is the target
620
528
  transformSchemaNameField(node, 'subname', schemaMapping, result);
621
529
  transformSchemaNameField(node, 'newname', schemaMapping, result);
622
530
  }
531
+ const ns = namespaceOfObjectType(node.renameType);
623
532
  if (node.object?.List?.items) {
624
- transformNameList(node.object.List.items, schemaMapping, result);
533
+ transformNameList(node.object.List.items, schemaMapping, result, ns);
625
534
  }
626
535
  else if (node.object?.ObjectWithArgs?.objname) {
627
- transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result);
536
+ transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result, ns);
628
537
  }
629
538
  },
630
539
  // Transform AlterFunctionStmt (ALTER FUNCTION schema.fn(...) SET ...)
@@ -637,46 +546,26 @@ export function createSqlVisitor(schemaMapping, result, visitorOptions) {
637
546
  // Transform AlterOwnerStmt (ALTER ... OWNER TO role)
638
547
  AlterOwnerStmt: (path) => {
639
548
  const node = path.node;
640
- transformRelation(node.relation, schemaMapping, result);
549
+ const ns = namespaceOfObjectType(node.objectType);
641
550
  if (node.object?.List?.items) {
642
- transformNameList(node.object.List.items, schemaMapping, result);
551
+ transformNameList(node.object.List.items, schemaMapping, result, ns);
643
552
  }
644
553
  else if (node.object?.ObjectWithArgs?.objname) {
645
- transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result);
554
+ transformNameList(node.object.ObjectWithArgs.objname, schemaMapping, result, ns);
646
555
  }
647
556
  else if (node.object?.String && node.objectType === 'OBJECT_SCHEMA') {
648
557
  transformSchemaNameField(node.object.String, 'sval', schemaMapping, result);
649
558
  }
650
559
  },
651
- // Transform CreateCastStmt (CREATE CAST (src AS tgt) WITH FUNCTION fn)
560
+ // Transform CreateCastStmt (CREATE CAST (src AS tgt) WITH FUNCTION fn).
561
+ // The function name gets its `function` namespace here; the cast's
562
+ // source/target/argument types are TypeName nodes the walker reaches on
563
+ // its own.
652
564
  CreateCastStmt: (path) => {
653
565
  const node = path.node;
654
- if (node.sourcetype?.names) {
655
- transformNameList(node.sourcetype.names, schemaMapping, result, 'type');
656
- }
657
- if (node.targettype?.names) {
658
- transformNameList(node.targettype.names, schemaMapping, result, 'type');
659
- }
660
566
  if (node.func?.objname) {
661
567
  transformNameList(node.func.objname, schemaMapping, result, 'function');
662
568
  }
663
- if (Array.isArray(node.func?.objargs)) {
664
- for (const arg of node.func.objargs) {
665
- if (arg?.TypeName?.names) {
666
- transformNameList(arg.TypeName.names, schemaMapping, result, 'type');
667
- }
668
- else if (arg?.names) {
669
- transformNameList(arg.names, schemaMapping, result, 'type');
670
- }
671
- }
672
- }
673
- if (Array.isArray(node.func?.objfuncargs)) {
674
- for (const param of node.func.objfuncargs) {
675
- if (param?.FunctionParameter?.argType?.names) {
676
- transformNameList(param.FunctionParameter.argType.names, schemaMapping, result, 'type');
677
- }
678
- }
679
- }
680
569
  },
681
570
  // Transform CreateEventTrigStmt (CREATE EVENT TRIGGER ... EXECUTE FUNCTION schema.fn())
682
571
  CreateEventTrigStmt: (path) => {
package/facts.d.ts CHANGED
@@ -94,6 +94,21 @@ export interface StatementFacts {
94
94
  * are incomplete and slicing should treat it conservatively.
95
95
  */
96
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
+ /** A statement's location in the source script (byte offsets). */
109
+ export interface StatementSpan {
110
+ start: number;
111
+ len: number;
97
112
  }
98
113
  /**
99
114
  * Classify each top-level statement in a SQL script into {@link StatementFacts}.
package/facts.js CHANGED
@@ -157,7 +157,8 @@ function classifyOne(nodeTag, node) {
157
157
  bodyReferences: [],
158
158
  securityRelevant: SECURITY_TAGS.has(nodeTag),
159
159
  securityDefiner: false,
160
- dynamicSql: false
160
+ dynamicSql: false,
161
+ span: { start: 0, len: 0 }
161
162
  };
162
163
  switch (nodeTag) {
163
164
  case 'CreateSchemaStmt':
@@ -351,6 +352,8 @@ function classifyStatements(sql) {
351
352
  const nodeTag = stmtNode ? Object.keys(stmtNode)[0] : 'other';
352
353
  const node = stmtNode?.[nodeTag] ?? {};
353
354
  const facts = classifyOne(nodeTag, node);
355
+ const start = stmt?.stmt_location ?? 0;
356
+ facts.span = { start, len: stmt?.stmt_len ?? Math.max(0, sql.length - start) };
354
357
  if (stmtNode) {
355
358
  (0, traverse_1.walk)(stmtNode, createFactsVisitor(facts));
356
359
  }
package/graph.d.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Statement-level dependency graph over classified SQL.
3
+ *
4
+ * {@link classifyStatements} reduces each top-level statement to
5
+ * {@link StatementFacts} — what it creates, what it references, which of
6
+ * those references live only inside PL/pgSQL bodies. This module turns a
7
+ * sequence of facts into a typed dependency graph and computes the orderings
8
+ * the granularity passes (consolidate / atomize) need:
9
+ *
10
+ * - **typed edges**: `hard` edges (the referenced object must exist at
11
+ * CREATE time), `fk` edges (foreign-key targets — hard, but singled out
12
+ * because they are the edges FK inlining must respect), and `late` edges
13
+ * (PL/pgSQL body references, resolved at call time — they never constrain
14
+ * deploy order and may legitimately form cycles).
15
+ * - **SCC condensation**: strongly connected components over hard+fk edges.
16
+ * A well-formed DDL script condenses to singleton components; any larger
17
+ * component is a genuine ordering cycle (e.g. mutually referencing FKs)
18
+ * that granularity passes must leave in atomic form.
19
+ * - **topological order** of the condensation, stable with respect to the
20
+ * original statement order (ties keep source order), so re-emission is
21
+ * deterministic and minimally surprising.
22
+ */
23
+ import { QualifiedName, StatementFacts } from './facts';
24
+ /**
25
+ * How a dependency edge constrains ordering.
26
+ *
27
+ * - `hard` — name must resolve when the dependent statement executes.
28
+ * - `fk` — a hard edge arising from a foreign-key target; distinguished so
29
+ * folding passes can decide whether an FK may be inlined into its table.
30
+ * - `late` — reached only inside a PL/pgSQL body; resolved at call time,
31
+ * so it does not constrain deploy order.
32
+ */
33
+ export type EdgeKind = 'hard' | 'fk' | 'late';
34
+ /** A directed dependency: statement `from` depends on statement `to`. */
35
+ export interface StatementEdge {
36
+ from: number;
37
+ to: number;
38
+ kind: EdgeKind;
39
+ /** The referenced object that induced this edge. */
40
+ via: QualifiedName;
41
+ }
42
+ /** A node in the statement graph: one top-level statement. */
43
+ export interface StatementNode {
44
+ /** Index of the statement in the classified script. */
45
+ index: number;
46
+ facts: StatementFacts;
47
+ /** Outgoing edges (this statement's dependencies). */
48
+ out: StatementEdge[];
49
+ /** Incoming edges (statements that depend on this one). */
50
+ in: StatementEdge[];
51
+ }
52
+ /** The statement-level dependency graph for one SQL script. */
53
+ export interface StatementGraph {
54
+ nodes: StatementNode[];
55
+ edges: StatementEdge[];
56
+ /**
57
+ * `schema.name` → indices of the statements that create that object.
58
+ * Trigger and policy names are table-qualified (`table.trigger`), matching
59
+ * {@link StatementFacts.creates}.
60
+ */
61
+ producers: Map<string, number[]>;
62
+ /**
63
+ * Strongly connected components over `hard` + `fk` edges, in topological
64
+ * order of the condensation. Singleton components are the common case;
65
+ * larger ones are genuine DDL ordering cycles.
66
+ */
67
+ components: number[][];
68
+ /**
69
+ * A stable topological order of all statements: components in condensation
70
+ * order, members of a component in source order. Ties between independent
71
+ * components keep source order.
72
+ */
73
+ order: number[];
74
+ }
75
+ /**
76
+ * Build the typed statement dependency graph for a classified script.
77
+ *
78
+ * Edges only exist between statements of the same script: a reference with
79
+ * no in-script producer is an external dependency and induces no edge (the
80
+ * caller decides what to do with those — pgpm expresses them as
81
+ * cross-package requires).
82
+ */
83
+ export declare function buildStatementGraph(facts: StatementFacts[]): StatementGraph;