|
| 1 | +/** |
| 2 | + * Object identity — the canonical, Postgres-native answer to "what object is |
| 3 | + * this statement about?". |
| 4 | + * |
| 5 | + * Identity is the key used by dependency graphs, semantic diffing, and any |
| 6 | + * downstream naming scheme. It is a pure function of classifier facts — |
| 7 | + * grounded in the parser's node taxonomy (`CreateStmt`, `CreateTrigStmt`, |
| 8 | + * `IndexStmt`, ...), never in surface syntax like RangeVars. Rendering an |
| 9 | + * identity to a change path (e.g. a pgpm module layout) is deliberately NOT |
| 10 | + * defined here: paths are derived projections that belong to whichever |
| 11 | + * packaging layer consumes the identity, so nothing is ever attached to them. |
| 12 | + * |
| 13 | + * Identity tuple: `(kind, schema, name, table?)` — `table` scopes objects |
| 14 | + * that are only unique per table (triggers, policies, indexes, constraints, |
| 15 | + * seed data). Function overloads share an identity for now (signature |
| 16 | + * disambiguation is a planned refinement). |
| 17 | + */ |
| 18 | +import { StatementFacts } from './facts'; |
| 19 | + |
| 20 | +/** The kinds of objects an identity can describe. */ |
| 21 | +export type ObjectIdentityKind = |
| 22 | + | 'schema' |
| 23 | + | 'extension' |
| 24 | + | 'role' |
| 25 | + | 'table' |
| 26 | + | 'view' |
| 27 | + | 'sequence' |
| 28 | + | 'type' |
| 29 | + | 'function' |
| 30 | + | 'index' |
| 31 | + | 'trigger' |
| 32 | + | 'policy' |
| 33 | + | 'constraint' |
| 34 | + | 'seed_dml' |
| 35 | + | 'other'; |
| 36 | + |
| 37 | +/** |
| 38 | + * The identity of a database object. Identity is the diff/dependency key; |
| 39 | + * any path or name is only a downstream rendering of it. |
| 40 | + */ |
| 41 | +export interface ObjectIdentity { |
| 42 | + kind: ObjectIdentityKind; |
| 43 | + /** Owning schema (`null` for non-schema objects: roles, extensions). */ |
| 44 | + schema: string | null; |
| 45 | + /** Object name, unqualified (for table-scoped kinds: without the table). */ |
| 46 | + name: string; |
| 47 | + /** Owning table, for objects only unique per table (trigger/policy/index/constraint/seed). */ |
| 48 | + table?: string; |
| 49 | +} |
| 50 | + |
| 51 | +/** |
| 52 | + * Derive the identity of the object a statement primarily creates or |
| 53 | + * targets, or `null` when the statement creates nothing (grants, comments — |
| 54 | + * such statements ride with the change of the object they attach to). |
| 55 | + * |
| 56 | + * Table-scoped kinds are recovered from the classifier's table-qualified |
| 57 | + * names (`table.trigger`) and, for indexes and constraints, from the |
| 58 | + * targeted relation. |
| 59 | + */ |
| 60 | +export function identityOf(facts: StatementFacts): ObjectIdentity | null { |
| 61 | + if (facts.kind === 'extension' && facts.extension) { |
| 62 | + return { kind: 'extension', schema: null, name: facts.extension.name }; |
| 63 | + } |
| 64 | + |
| 65 | + const created = facts.creates[0]; |
| 66 | + if (!created) return null; |
| 67 | + |
| 68 | + switch (facts.kind) { |
| 69 | + case 'schema': |
| 70 | + return { kind: 'schema', schema: null, name: created.name }; |
| 71 | + case 'trigger': |
| 72 | + case 'policy': { |
| 73 | + const dot = created.name.indexOf('.'); |
| 74 | + if (dot > 0) { |
| 75 | + return { |
| 76 | + kind: facts.kind, |
| 77 | + schema: created.schema, |
| 78 | + name: created.name.slice(dot + 1), |
| 79 | + table: created.name.slice(0, dot) |
| 80 | + }; |
| 81 | + } |
| 82 | + return { kind: facts.kind, schema: created.schema, name: created.name }; |
| 83 | + } |
| 84 | + case 'index': { |
| 85 | + // IndexStmt records the index name in creates and the indexed relation |
| 86 | + // in references (same-schema RangeVar). |
| 87 | + const rel = facts.references.find(r => r.schema === created.schema) ?? facts.references[0]; |
| 88 | + return { |
| 89 | + kind: 'index', |
| 90 | + schema: created.schema, |
| 91 | + name: created.name, |
| 92 | + table: rel?.name |
| 93 | + }; |
| 94 | + } |
| 95 | + case 'fk_constraint': |
| 96 | + case 'constraint': |
| 97 | + case 'rls_enable': |
| 98 | + // ALTER TABLE statements target their table. |
| 99 | + return { kind: 'constraint', schema: created.schema, name: created.name, table: created.name }; |
| 100 | + case 'seed_dml': |
| 101 | + return { kind: 'seed_dml', schema: created.schema, name: created.name, table: created.name }; |
| 102 | + case 'table': |
| 103 | + // AlterTableStmt facts also classify as `table`-targeting; the created |
| 104 | + // name is the table either way. |
| 105 | + return { kind: 'table', schema: created.schema, name: created.name }; |
| 106 | + case 'view': |
| 107 | + case 'function': |
| 108 | + case 'type': |
| 109 | + return { kind: facts.kind, schema: created.schema, name: created.name }; |
| 110 | + default: |
| 111 | + if (facts.nodeTag === 'CreateSeqStmt') { |
| 112 | + return { kind: 'sequence', schema: created.schema, name: created.name }; |
| 113 | + } |
| 114 | + return { kind: 'other', schema: created.schema, name: created.name }; |
| 115 | + } |
| 116 | +} |
0 commit comments