22 * Unified SQL object graph — the semantic IR over parsed programs.
33 *
44 * Nodes are database objects (keyed by qualified name) with the statements
5- * that create them; edges are the references statements make to objects:
6- * plain references, late-bound PL/pgSQL body references, and foreign-key
7- * targets. Programs are named (typically one per pgpm change), so ownership
8- * queries answer change-level questions directly:
5+ * that create them; edges are the references statements make to objects,
6+ * typed with `@pgsql/transform`'s edge taxonomy (`hard | fk | late`).
7+ * Programs are named (typically one per pgpm change), so ownership queries
8+ * answer change-level questions directly:
99 *
1010 * - which objects live in a schema set (subsystem selection)
1111 * - which surviving statements still reach into a dropped object set
1414 * - which programs create nothing outside a dropped set (whole-change
1515 * pruning by ownership, not survivor counting)
1616 *
17+ * Each program's intra-program edges come straight from
18+ * `buildStatementGraph`; references with no in-program producer (external
19+ * dependencies, which the statement graph deliberately omits) are added with
20+ * the same kind classification, so the object graph is a superset projection
21+ * of the statement graph over the same facts.
22+ *
1723 * Pure and I/O-free; built from `SqlProgram`s parsed once elsewhere.
1824 */
19- import type { SqlProgram , StatementFacts } from '@pgpmjs/transform' ;
25+ import { buildStatementGraph , EdgeKind , SqlProgram , StatementFacts } from '@pgpmjs/transform' ;
2026
2127import { SqlObjectRef } from './refs' ;
2228
@@ -26,7 +32,13 @@ export interface StatementId {
2632 statement : number ;
2733}
2834
29- export type ObjectEdgeKind = 'reference' | 'body' | 'fk' ;
35+ /**
36+ * How an edge constrains ordering — `@pgsql/transform`'s taxonomy:
37+ * `hard` (must exist at CREATE time), `fk` (a hard edge from a foreign-key
38+ * target), `late` (reached only inside a PL/pgSQL body, resolved at call
39+ * time).
40+ */
41+ export type ObjectEdgeKind = EdgeKind ;
3042
3143/** One reference from a statement to an object. */
3244export interface ObjectEdge {
@@ -78,29 +90,60 @@ export function buildObjectGraph(
7890 const incoming = new Map < string , ObjectEdge [ ] > ( ) ;
7991
8092 const addEdge = ( edge : ObjectEdge ) : void => {
81- edges . push ( edge ) ;
8293 const list = incoming . get ( edge . to ) ;
83- if ( list ) list . push ( edge ) ;
84- else incoming . set ( edge . to , [ edge ] ) ;
94+ if ( list ) {
95+ if ( list . some (
96+ e => e . kind === edge . kind &&
97+ e . from . program === edge . from . program &&
98+ e . from . statement === edge . from . statement
99+ ) ) return ;
100+ list . push ( edge ) ;
101+ } else {
102+ incoming . set ( edge . to , [ edge ] ) ;
103+ }
104+ edges . push ( edge ) ;
85105 } ;
86106
87107 for ( const [ name , program ] of programMap ) {
88- program . statements . forEach ( ( stmt , i ) => {
108+ const facts = program . statements . map ( s => s . facts ) ;
109+ const statementGraph = buildStatementGraph ( facts ) ;
110+
111+ facts . forEach ( ( stmt , i ) => {
89112 const id : StatementId = { program : name , statement : i } ;
90- for ( const c of stmt . facts . creates ) {
113+ for ( const c of stmt . creates ) {
91114 const key = objectKey ( c ) ;
92115 const node = objects . get ( key ) ;
93116 if ( node ) node . createdBy . push ( id ) ;
94117 else objects . set ( key , { ref : { schema : c . schema , name : c . name } , createdBy : [ id ] } ) ;
95118 }
96- for ( const r of stmt . facts . references ) {
97- addEdge ( { from : id , to : objectKey ( r ) , kind : 'reference' } ) ;
98- }
99- for ( const r of stmt . facts . bodyReferences ) {
100- addEdge ( { from : id , to : objectKey ( r ) , kind : 'body' } ) ;
119+ } ) ;
120+
121+ // Intra-program edges, exactly as the statement graph typed them.
122+ for ( const e of statementGraph . edges ) {
123+ addEdge ( { from : { program : name , statement : e . from } , to : objectKey ( e . via ) , kind : e . kind } ) ;
124+ }
125+
126+ // References with no in-program producer are external dependencies: the
127+ // statement graph induces no edge for them, but the object graph keeps
128+ // them (that is what contract/cascade queries measure). Same kind
129+ // classification: fk beats late beats hard.
130+ facts . forEach ( ( stmt , i ) => {
131+ const id : StatementId = { program : name , statement : i } ;
132+ const bodyOnly = new Set ( stmt . bodyReferences . map ( objectKey ) ) ;
133+ const fkKeys = new Set ( stmt . fkTargets . map ( objectKey ) ) ;
134+ const seen = new Set < string > ( ) ;
135+ for ( const r of stmt . references ) {
136+ const key = objectKey ( r ) ;
137+ seen . add ( key ) ;
138+ if ( statementGraph . producers . has ( `${ r . schema ?? '' } .${ r . name } ` ) ) continue ;
139+ const kind : EdgeKind = fkKeys . has ( key ) ? 'fk' : bodyOnly . has ( key ) ? 'late' : 'hard' ;
140+ addEdge ( { from : id , to : key , kind } ) ;
101141 }
102- for ( const t of stmt . facts . fkTargets ) {
103- addEdge ( { from : id , to : objectKey ( t ) , kind : 'fk' } ) ;
142+ for ( const t of stmt . fkTargets ) {
143+ const key = objectKey ( t ) ;
144+ if ( seen . has ( key ) ) continue ;
145+ if ( statementGraph . producers . has ( `${ t . schema ?? '' } .${ t . name } ` ) ) continue ;
146+ addEdge ( { from : id , to : key , kind : 'fk' } ) ;
104147 }
105148 } ) ;
106149 }
0 commit comments