@prisma-next/sql-schema-ir 0.14.0 → 0.15.0-dev.10

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.
@@ -0,0 +1,683 @@
1
+ import { IRNodeBase, freezeNode } from "@prisma-next/framework-components/ir";
2
+ import { blindCast } from "@prisma-next/utils/casts";
3
+ import { canonicalStringify } from "@prisma-next/utils/canonical-stringify";
4
+ import { ifDefined } from "@prisma-next/utils/defined";
5
+ //#region src/ir/schema-node-kinds.ts
6
+ /**
7
+ * The `nodeKind` discriminant for each relational schema-diff leaf node.
8
+ * Each node carries a unique value; the differ pairs siblings by `id`, and
9
+ * these kinds distinguish the node types that appear as `PostgresTableSchemaNode`
10
+ * children (columns, primary key, foreign keys, uniques, indexes, checks) from
11
+ * each other and from the RLS policy/role kinds a target defines separately.
12
+ */
13
+ const RelationalSchemaNodeKind = {
14
+ schema: "sql-schema",
15
+ table: "sql-table",
16
+ column: "sql-column",
17
+ columnDefault: "sql-column-default",
18
+ primaryKey: "sql-primary-key",
19
+ foreignKey: "sql-foreign-key",
20
+ unique: "sql-unique",
21
+ index: "sql-index",
22
+ check: "sql-check-constraint"
23
+ };
24
+ /**
25
+ * The one real map from a relational `nodeKind` to the framework-neutral
26
+ * {@link DiffSubjectGranularity} its diff issues carry — the SQL family's
27
+ * post-diff filters (issue category, strict-mode gating) and the framework
28
+ * aggregate's unclaimed-elements sweep key on the granularity, never on the
29
+ * `nodeKind` spelling and never on anything stamped on the node. Resolution
30
+ * is by `nodeKind` equality against this map, not a suffix string match.
31
+ * Target-specific node kinds (Postgres namespace/table/policy/role) are
32
+ * outside this family layer's vocabulary and map their own kinds directly.
33
+ */
34
+ const RELATIONAL_NODE_GRANULARITY = {
35
+ [RelationalSchemaNodeKind.schema]: "structural",
36
+ [RelationalSchemaNodeKind.table]: "entity",
37
+ [RelationalSchemaNodeKind.column]: "field",
38
+ [RelationalSchemaNodeKind.columnDefault]: "auxiliary",
39
+ [RelationalSchemaNodeKind.primaryKey]: "auxiliary",
40
+ [RelationalSchemaNodeKind.foreignKey]: "auxiliary",
41
+ [RelationalSchemaNodeKind.unique]: "auxiliary",
42
+ [RelationalSchemaNodeKind.index]: "auxiliary",
43
+ [RelationalSchemaNodeKind.check]: "auxiliary"
44
+ };
45
+ function isRelationalSchemaNodeKind(nodeKind) {
46
+ return Object.hasOwn(RELATIONAL_NODE_GRANULARITY, nodeKind);
47
+ }
48
+ /**
49
+ * Looks up the subject granularity for a relational `nodeKind`. Throws for a
50
+ * `nodeKind` outside this map (a target-specific kind, e.g. Postgres's
51
+ * namespace/table/policy/role) — those map their own kinds directly rather
52
+ * than through this family-level map.
53
+ */
54
+ function relationalNodeGranularity(nodeKind) {
55
+ if (!isRelationalSchemaNodeKind(nodeKind)) throw new Error(`relationalNodeGranularity: unrecognized relational node kind "${nodeKind}"`);
56
+ return RELATIONAL_NODE_GRANULARITY[nodeKind];
57
+ }
58
+ /**
59
+ * The one real map from a relational `nodeKind` to its storage `entityKind`
60
+ * — the same vocabulary the contract storage's `entries` dictionary keys use
61
+ * (`elementCoordinates` walks it). Only the whole-table kind has an entity of
62
+ * its own; every other relational node (a column, an index, …) is nested
63
+ * under one and maps to nothing here.
64
+ */
65
+ const RELATIONAL_NODE_ENTITY_KIND = { [RelationalSchemaNodeKind.table]: "table" };
66
+ /**
67
+ * Looks up the storage `entityKind` for a relational `nodeKind` — sibling of
68
+ * {@link relationalNodeGranularity}, resolved by `nodeKind` equality against
69
+ * {@link RELATIONAL_NODE_ENTITY_KIND}. `undefined` for a target-specific kind
70
+ * (targets map their own kinds directly) or a relational kind with no entity
71
+ * of its own.
72
+ */
73
+ function relationalNodeEntityKind(nodeKind) {
74
+ return isRelationalSchemaNodeKind(nodeKind) ? RELATIONAL_NODE_ENTITY_KIND[nodeKind] : void 0;
75
+ }
76
+ //#endregion
77
+ //#region src/ir/sql-schema-ir-node.ts
78
+ /**
79
+ * SQL Schema IR node base. Carries the family-level
80
+ * `kind = 'sql-schema-ir'` discriminator and inherits the framework's
81
+ * `freezeNode` affordance.
82
+ *
83
+ * SQL Schema IR represents the actual database state as discovered by
84
+ * introspection (the parallel to SQL Contract IR, which represents the
85
+ * desired state).
86
+ *
87
+ * The discriminator is installed as a non-enumerable own property,
88
+ * matching the SqlNode pattern. This keeps `JSON.stringify(node)`
89
+ * canonical (no `kind` field), keeps `toEqual({...})` test assertions
90
+ * against pre-lift flat shapes passing, and keeps `node.kind` readable
91
+ * for dispatch.
92
+ *
93
+ * Both `kind` and `nodeKind` are required: every concrete leaf is a node
94
+ * the generic differ can pair and compare, so every leaf must declare which
95
+ * node it is. `nodeKind` has no default here — every direct subclass sets its
96
+ * own literal value (the relational leaves via `RelationalSchemaNodeKind`,
97
+ * target concretions via their own vocabulary, e.g. `PostgresSchemaNodeKind`).
98
+ */
99
+ var SqlSchemaIRNode = class extends IRNodeBase {
100
+ constructor() {
101
+ super();
102
+ Object.defineProperty(this, "kind", {
103
+ value: "sql-schema-ir",
104
+ writable: false,
105
+ enumerable: false,
106
+ configurable: false
107
+ });
108
+ }
109
+ };
110
+ /**
111
+ * Asserts `node` matches `predicate`, narrowing its type to `T`. The one
112
+ * shared implementation every node class's `static assert` and `isEqualTo`
113
+ * reach for, instead of each hand-writing its own throw: the message names
114
+ * the class the caller expected.
115
+ */
116
+ function assertNode(node, className, predicate) {
117
+ if (node === void 0 || !predicate(node)) throw new Error(`Expected a ${className} but got nodeKind=${node?.nodeKind ?? "undefined"}`);
118
+ }
119
+ /**
120
+ * Defines a non-enumerable own property, the same treatment `kind` gets
121
+ * above: a derivation-time render-support field stays out of
122
+ * `JSON.stringify`, `toEqual({...})` structural assertions, and spreads,
123
+ * while remaining directly readable (`node.field`) for the one consumer
124
+ * that resolves it at plan time. A no-op when `value` is `undefined` — the
125
+ * property is simply absent, matching every other optional field on these
126
+ * nodes.
127
+ */
128
+ function defineNonEnumerable(target, key, value) {
129
+ if (value === void 0) return;
130
+ Object.defineProperty(target, key, {
131
+ value,
132
+ enumerable: false,
133
+ writable: false,
134
+ configurable: false
135
+ });
136
+ }
137
+ //#endregion
138
+ //#region src/ir/primary-key.ts
139
+ /**
140
+ * Primary-key Schema IR node. Mirrors the Contract IR `PrimaryKey`
141
+ * shape (same `columns` + optional `name`) so verification can compare
142
+ * intent and actual structurally. Defined here independently to avoid
143
+ * a sql-schema-ir -> sql-contract dependency.
144
+ *
145
+ * Implements `DiffableNode` so a primary key is directly a table's diff-tree
146
+ * child. `id` is a fixed sentinel — a table has at most one primary key, so
147
+ * there is never a sibling to disambiguate against. `isEqualTo` compares the
148
+ * column tuple; the PK's own `name` is a database-assigned label with no
149
+ * semantic weight, so it is not compared (mirrors the policy node's
150
+ * name-insensitivity to non-identifying detail).
151
+ */
152
+ var PrimaryKey = class PrimaryKey extends SqlSchemaIRNode {
153
+ nodeKind = RelationalSchemaNodeKind.primaryKey;
154
+ columns;
155
+ constructor(input) {
156
+ super();
157
+ this.columns = input.columns;
158
+ if (input.name !== void 0) this.name = input.name;
159
+ defineNonEnumerable(this, "dependsOn", input.dependsOn);
160
+ freezeNode(this);
161
+ }
162
+ get id() {
163
+ return "primary-key";
164
+ }
165
+ children() {
166
+ return [];
167
+ }
168
+ static is(node) {
169
+ return node.nodeKind === RelationalSchemaNodeKind.primaryKey;
170
+ }
171
+ isEqualTo(other) {
172
+ const node = blindCast(other);
173
+ assertNode(node, "PrimaryKey", PrimaryKey.is);
174
+ return this.columns.length === node.columns.length && this.columns.every((c, i) => c === node.columns[i]);
175
+ }
176
+ };
177
+ //#endregion
178
+ //#region src/ir/sql-check-constraint-ir.ts
179
+ /**
180
+ * Schema IR node for a table-level check constraint that restricts a
181
+ * column to a set of permitted values (an enum-style `IN (...)` check).
182
+ *
183
+ * Carries the **resolved values** rather than a raw SQL predicate so
184
+ * callers can compare value-sets without parsing SQL.
185
+ *
186
+ * Implements `DiffableNode` so a check constraint is directly a table's
187
+ * diff-tree child: `id` is the constraint name. `isEqualTo` compares
188
+ * `column` and the permitted-value set (order-insensitive — the database
189
+ * does not guarantee `IN (...)` ordering).
190
+ */
191
+ var SqlCheckConstraintIR = class SqlCheckConstraintIR extends SqlSchemaIRNode {
192
+ nodeKind = RelationalSchemaNodeKind.check;
193
+ name;
194
+ column;
195
+ permittedValues;
196
+ constructor(input) {
197
+ super();
198
+ this.name = input.name;
199
+ this.column = input.column;
200
+ this.permittedValues = Object.freeze([...input.permittedValues]);
201
+ freezeNode(this);
202
+ }
203
+ get id() {
204
+ return `check:${this.name}`;
205
+ }
206
+ children() {
207
+ return [];
208
+ }
209
+ static is(node) {
210
+ return node.nodeKind === RelationalSchemaNodeKind.check;
211
+ }
212
+ /**
213
+ * Compares the permitted-value sets only (unordered), matching the
214
+ * relational walk's `verifyCheckConstraints`: two checks pairing by name
215
+ * compare their value sets — the `column` field is descriptive, not part
216
+ * of the comparison, so a name-paired check with equal values verifies
217
+ * regardless of which column carries it.
218
+ */
219
+ isEqualTo(other) {
220
+ const node = blindCast(other);
221
+ assertNode(node, "SqlCheckConstraintIR", SqlCheckConstraintIR.is);
222
+ const thisValues = new Set(this.permittedValues);
223
+ const otherValues = new Set(node.permittedValues);
224
+ if (thisValues.size !== otherValues.size) return false;
225
+ return [...thisValues].every((v) => otherValues.has(v));
226
+ }
227
+ };
228
+ //#endregion
229
+ //#region src/ir/resolved-default-equality.ts
230
+ /**
231
+ * Structural equality for two resolved column defaults, ported from the
232
+ * relational walk's `columnDefaultsEqual` normalized branch: kinds must
233
+ * match; literal values are normalized (Date and temporal-typed strings to
234
+ * ISO instants) then compared canonically (JSON objects match their
235
+ * canonical string form); function expressions compare case- and
236
+ * whitespace-insensitively.
237
+ *
238
+ * `nativeType` provides the temporal-normalization context (the actual
239
+ * side's resolved native type in a diff comparison).
240
+ */
241
+ function resolvedDefaultsEqual(expected, actual, nativeType) {
242
+ if (expected.kind !== actual.kind) return false;
243
+ if (expected.kind === "literal" && actual.kind === "literal") return literalValuesEqual(normalizeLiteralValue(expected.value, nativeType), normalizeLiteralValue(actual.value, nativeType));
244
+ if (expected.kind === "function" && actual.kind === "function") return normalizeFunctionExpression(expected.expression) === normalizeFunctionExpression(actual.expression);
245
+ return false;
246
+ }
247
+ function normalizeFunctionExpression(expression) {
248
+ return expression.toLowerCase().replace(/\s+/g, "");
249
+ }
250
+ function isTemporalNativeType(nativeType) {
251
+ if (!nativeType) return false;
252
+ const normalized = nativeType.toLowerCase();
253
+ return normalized.includes("timestamp") || normalized === "date";
254
+ }
255
+ function normalizeLiteralValue(value, nativeType) {
256
+ if (value instanceof Date) return value.toISOString();
257
+ if (typeof value === "string" && isTemporalNativeType(nativeType)) {
258
+ const parsed = new Date(value);
259
+ if (!Number.isNaN(parsed.getTime())) return parsed.toISOString();
260
+ }
261
+ return value;
262
+ }
263
+ function literalValuesEqual(a, b) {
264
+ if (a === b) return true;
265
+ if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) return canonicalStringify(a) === canonicalStringify(b);
266
+ if (typeof a === "object" && a !== null && typeof b === "string") try {
267
+ return canonicalStringify(a) === canonicalStringify(JSON.parse(b));
268
+ } catch {
269
+ return false;
270
+ }
271
+ if (typeof a === "string" && typeof b === "object" && b !== null) try {
272
+ return canonicalStringify(JSON.parse(a)) === canonicalStringify(b);
273
+ } catch {
274
+ return false;
275
+ }
276
+ return false;
277
+ }
278
+ //#endregion
279
+ //#region src/ir/sql-column-default-ir.ts
280
+ /**
281
+ * Schema-diff node for a column's default value. The default is the one
282
+ * column attribute with an extra/missing/drift lifecycle of its own, so it
283
+ * is a child node of the column rather than a compared attribute: an
284
+ * undeclared live default surfaces as `not-expected`, a declared default the
285
+ * database lacks as `not-found`, and a divergent value as `not-equal` — the
286
+ * three reasons cover the legacy `extra_default` / `default_missing` /
287
+ * `default_mismatch` vocabulary with no attribute inspection.
288
+ *
289
+ * `id` is a fixed sentinel — a column has at most one default. Built
290
+ * transiently by `SqlColumnIR.children()` from the column's own fields;
291
+ * never constructed by derivation or introspection directly.
292
+ */
293
+ var SqlColumnDefaultIR = class SqlColumnDefaultIR extends SqlSchemaIRNode {
294
+ nodeKind = RelationalSchemaNodeKind.columnDefault;
295
+ constructor(input) {
296
+ super();
297
+ if (input.resolved !== void 0) this.resolved = input.resolved;
298
+ if (input.raw !== void 0) this.raw = input.raw;
299
+ if (input.nativeTypeContext !== void 0) this.nativeTypeContext = input.nativeTypeContext;
300
+ defineNonEnumerable(this, "many", input.many);
301
+ freezeNode(this);
302
+ }
303
+ get id() {
304
+ return "default";
305
+ }
306
+ children() {
307
+ return [];
308
+ }
309
+ static is(node) {
310
+ return node.nodeKind === RelationalSchemaNodeKind.columnDefault;
311
+ }
312
+ /**
313
+ * Structured comparison with `this` as the expected side: both sides
314
+ * resolved compare per the relational walk's `columnDefaultsEqual`
315
+ * semantics; a declared expected default against an unparseable actual
316
+ * (raw present, no resolved parse) is a mismatch; two raw-only nodes fall
317
+ * back to raw string equality.
318
+ */
319
+ isEqualTo(other) {
320
+ const node = blindCast(other);
321
+ assertNode(node, "SqlColumnDefaultIR", SqlColumnDefaultIR.is);
322
+ if (this.resolved !== void 0 && node.resolved !== void 0) return resolvedDefaultsEqual(this.resolved, node.resolved, node.nativeTypeContext ?? this.nativeTypeContext);
323
+ if (this.resolved !== void 0 || node.resolved !== void 0) return false;
324
+ return this.raw === node.raw;
325
+ }
326
+ };
327
+ //#endregion
328
+ //#region src/ir/sql-column-ir.ts
329
+ /**
330
+ * Schema IR node for a single column on a table, as observed by
331
+ * introspection. Unlike the Contract IR `StorageColumn`, this carries
332
+ * the column's `name` (Schema IR columns are returned as arrays from
333
+ * introspection queries; the parent table re-keys them into a record
334
+ * for downstream consumers).
335
+ *
336
+ * Implements `DiffableNode` so a column is directly a table's diff-tree
337
+ * child: `id` is the column name (unique among a table's columns); `isEqualTo`
338
+ * compares this column's own attributes only — never children, since a
339
+ * column is a leaf. When both sides carry `resolvedNativeType` (stamped at
340
+ * derivation/introspection), the comparison uses the resolved values —
341
+ * resolved native type, nullability, and structured default equality per
342
+ * the relational walk's `columnDefaultsEqual` semantics, with `this` as the
343
+ * expected side. Otherwise it falls back to comparing raw fields.
344
+ */
345
+ var SqlColumnIR = class SqlColumnIR extends SqlSchemaIRNode {
346
+ nodeKind = RelationalSchemaNodeKind.column;
347
+ name;
348
+ nativeType;
349
+ nullable;
350
+ constructor(input) {
351
+ super();
352
+ this.name = input.name;
353
+ this.nativeType = input.nativeType;
354
+ this.nullable = input.nullable;
355
+ if (input.default !== void 0) this.default = input.default;
356
+ if (input.annotations !== void 0) this.annotations = input.annotations;
357
+ if (input.many !== void 0) this.many = input.many;
358
+ if (input.resolvedNativeType !== void 0) this.resolvedNativeType = input.resolvedNativeType;
359
+ if (input.resolvedDefault !== void 0) this.resolvedDefault = input.resolvedDefault;
360
+ defineNonEnumerable(this, "codecRef", input.codecRef);
361
+ defineNonEnumerable(this, "codecBaseNativeType", input.codecBaseNativeType);
362
+ defineNonEnumerable(this, "codecNamedType", input.codecNamedType);
363
+ freezeNode(this);
364
+ }
365
+ get id() {
366
+ return `column:${this.name}`;
367
+ }
368
+ /**
369
+ * The column's default, when declared/present, is the column's one child
370
+ * node — it has an extra/missing/drift lifecycle of its own, so the differ
371
+ * recurses to it rather than `isEqualTo` comparing it. Built transiently
372
+ * from this column's own fields.
373
+ */
374
+ children() {
375
+ if (this.resolvedDefault === void 0 && this.default === void 0) return [];
376
+ return [new SqlColumnDefaultIR({
377
+ ...ifDefined("resolved", this.resolvedDefault),
378
+ ...ifDefined("raw", this.default),
379
+ ...ifDefined("nativeTypeContext", this.resolvedNativeType),
380
+ ...ifDefined("many", this.many ?? this.codecRef?.many)
381
+ })];
382
+ }
383
+ static is(node) {
384
+ return node.nodeKind === RelationalSchemaNodeKind.column;
385
+ }
386
+ /**
387
+ * Compares the column's own attributes only — the default lives on the
388
+ * child node. When both sides carry `resolvedNativeType`, the resolved
389
+ * value governs (array-ness rides on its `[]` suffix); otherwise raw
390
+ * fields compare.
391
+ */
392
+ isEqualTo(other) {
393
+ const node = blindCast(other);
394
+ assertNode(node, "SqlColumnIR", SqlColumnIR.is);
395
+ if (this.resolvedNativeType !== void 0 && node.resolvedNativeType !== void 0) return this.resolvedNativeType === node.resolvedNativeType && this.nullable === node.nullable;
396
+ return this.nativeType === node.nativeType && this.nullable === node.nullable && Boolean(this.many) === Boolean(node.many);
397
+ }
398
+ };
399
+ //#endregion
400
+ //#region src/ir/sql-foreign-key-ir.ts
401
+ /**
402
+ * Schema IR node for a foreign-key constraint as observed by
403
+ * introspection. The `referencedTable` / `referencedColumns` field
404
+ * names match the introspection vocabulary (`pg_constraint.confkey`,
405
+ * etc.) and intentionally differ from the Contract IR's nested
406
+ * `references: { table, columns }` shape so that the verifier's
407
+ * structural comparison stays explicit about which side it's reading.
408
+ *
409
+ * Implements `DiffableNode` so a foreign key is directly a table's diff-tree
410
+ * child. Foreign keys are frequently unnamed (introspection may not carry a
411
+ * constraint name, and the contract side never invents one), so `id` is
412
+ * derived from the referencing/referenced coordinates rather than `name` —
413
+ * the same tuple that makes two FK constraints the same constraint. This
414
+ * also serves as the comparison key: two FKs with the same coordinates are
415
+ * paired by the differ, and `isEqualTo` then compares the remaining
416
+ * attribute — the referential actions.
417
+ */
418
+ var SqlForeignKeyIR = class SqlForeignKeyIR extends SqlSchemaIRNode {
419
+ nodeKind = RelationalSchemaNodeKind.foreignKey;
420
+ columns;
421
+ referencedTable;
422
+ referencedColumns;
423
+ constructor(input) {
424
+ super();
425
+ this.columns = input.columns;
426
+ this.referencedTable = input.referencedTable;
427
+ this.referencedColumns = input.referencedColumns;
428
+ if (input.referencedSchema !== void 0) this.referencedSchema = input.referencedSchema;
429
+ if (input.name !== void 0) this.name = input.name;
430
+ if (input.onDelete !== void 0) this.onDelete = input.onDelete;
431
+ if (input.onUpdate !== void 0) this.onUpdate = input.onUpdate;
432
+ if (input.annotations !== void 0) this.annotations = input.annotations;
433
+ const resolvedReferencedNamespace = input.resolvedReferencedNamespace ?? input.referencedSchema;
434
+ if (resolvedReferencedNamespace !== void 0) this.resolvedReferencedNamespace = resolvedReferencedNamespace;
435
+ defineNonEnumerable(this, "dependsOn", input.dependsOn);
436
+ freezeNode(this);
437
+ }
438
+ get id() {
439
+ const referencedNamespace = this.resolvedReferencedNamespace ?? "";
440
+ return `foreign-key:${this.columns.join(",")}->${referencedNamespace}.${this.referencedTable}(${this.referencedColumns.join(",")})`;
441
+ }
442
+ children() {
443
+ return [];
444
+ }
445
+ static is(node) {
446
+ return node.nodeKind === RelationalSchemaNodeKind.foreignKey;
447
+ }
448
+ /**
449
+ * Referential-action comparison with `this` as the expected side, matching
450
+ * the relational walk's `getReferentialActionMismatches`: `noAction` is the
451
+ * database default and equivalent to an undeclared action, and drift is
452
+ * flagged only when the expected side declares a (normalized) action.
453
+ */
454
+ isEqualTo(other) {
455
+ const node = blindCast(other);
456
+ assertNode(node, "SqlForeignKeyIR", SqlForeignKeyIR.is);
457
+ return referentialActionMatches(this.onDelete, node.onDelete) && referentialActionMatches(this.onUpdate, node.onUpdate);
458
+ }
459
+ };
460
+ function referentialActionMatches(expected, actual) {
461
+ const normalizedExpected = expected === "noAction" ? void 0 : expected;
462
+ if (normalizedExpected === void 0) return true;
463
+ return normalizedExpected === (actual === "noAction" ? void 0 : actual);
464
+ }
465
+ //#endregion
466
+ //#region src/ir/sql-index-ir.ts
467
+ /**
468
+ * Schema IR node for a secondary index as observed by introspection.
469
+ * Unlike the Contract IR `Index`, the Schema IR carries an explicit
470
+ * `unique` field — introspection sees the underlying index regardless
471
+ * of whether the user expressed it as `@@index` or `@@unique`, and the
472
+ * verifier needs to distinguish them when comparing to the Contract.
473
+ *
474
+ * Implements `DiffableNode` so an index is directly a table's diff-tree
475
+ * child. Indexes are frequently unnamed, so `id` is derived from the column
476
+ * tuple — the same tuple that makes two indexes the same index, so it
477
+ * doubles as the pairing key. `isEqualTo` is symmetric structural equality
478
+ * on the remaining attributes: `unique`, `type`, and `options`. A unique
479
+ * index and a non-unique index on the same columns are different objects
480
+ * and are not equal — there is no "stronger satisfies weaker".
481
+ *
482
+ * Deliberately column-tuple-only (not `unique`): a contract `@@index`
483
+ * (non-unique) must still pair against a live unique index of the same
484
+ * columns so the differ can report the mismatch as an incompatible index
485
+ * change rather than a spurious missing+extra pair — see
486
+ * `planner.unique-index-structural.test.ts`. The corollary is that two
487
+ * indexes legitimately coexisting on one table with the *same* column tuple
488
+ * (e.g. a unique index and a redundant plain index Postgres has no problem
489
+ * hosting side by side) collide on this id; the postgres control adapter's
490
+ * introspection keeps only one such index per table+column-tuple rather
491
+ * than handing the differ two same-tree siblings it cannot represent.
492
+ */
493
+ var SqlIndexIR = class SqlIndexIR extends SqlSchemaIRNode {
494
+ nodeKind = RelationalSchemaNodeKind.index;
495
+ columns;
496
+ unique;
497
+ constructor(input) {
498
+ super();
499
+ this.columns = input.columns;
500
+ this.unique = input.unique;
501
+ if (input.name !== void 0) this.name = input.name;
502
+ if (input.type !== void 0) this.type = input.type;
503
+ if (input.options !== void 0) this.options = input.options;
504
+ if (input.annotations !== void 0) this.annotations = input.annotations;
505
+ defineNonEnumerable(this, "dependsOn", input.dependsOn);
506
+ freezeNode(this);
507
+ }
508
+ get id() {
509
+ return `index:${this.columns.join(",")}`;
510
+ }
511
+ children() {
512
+ return [];
513
+ }
514
+ static is(node) {
515
+ return node.nodeKind === RelationalSchemaNodeKind.index;
516
+ }
517
+ /**
518
+ * Symmetric structural equality: two paired index nodes are equal iff their
519
+ * `unique` flag, `type`, and (loosely-compared) `options` all match. There
520
+ * is no satisfaction — a unique index does not equal a non-unique index.
521
+ * `options` compares loosely (introspection stringifies reloptions); `type`
522
+ * compares strictly after the introspection-side btree→undefined
523
+ * normalization done at construction.
524
+ */
525
+ isEqualTo(other) {
526
+ const node = blindCast(other);
527
+ assertNode(node, "SqlIndexIR", SqlIndexIR.is);
528
+ return this.unique === node.unique && this.type === node.type && indexOptionsLooselyEqual(this.options, node.options);
529
+ }
530
+ };
531
+ /**
532
+ * Option-bag equality ported from the relational walk: same key set, values
533
+ * compared via `String()` coercion — Postgres introspection returns
534
+ * reloptions values as raw strings (`'70'`, `'false'`) while contract option
535
+ * leaves are typed (number, boolean, string).
536
+ */
537
+ function indexOptionsLooselyEqual(a, b) {
538
+ const aKeys = a ? Object.keys(a).sort() : [];
539
+ const bKeys = b ? Object.keys(b).sort() : [];
540
+ if (aKeys.length !== bKeys.length) return false;
541
+ for (let i = 0; i < aKeys.length; i += 1) if (aKeys[i] !== bKeys[i]) return false;
542
+ if (aKeys.length === 0) return true;
543
+ for (const key of aKeys) if (String(a?.[key]) !== String(b?.[key])) return false;
544
+ return true;
545
+ }
546
+ //#endregion
547
+ //#region src/ir/sql-unique-ir.ts
548
+ /**
549
+ * Schema IR node for a table-level unique constraint as observed by
550
+ * introspection.
551
+ *
552
+ * Implements `DiffableNode` so a unique constraint is directly a table's
553
+ * diff-tree child. Unique constraints are frequently unnamed, so `id` is
554
+ * derived from the column tuple rather than `name` — the column tuple is
555
+ * also what makes two unique constraints the same constraint, so it doubles
556
+ * as the pairing key. There are no further attributes to compare once
557
+ * columns are equal (the differ pairs on `id`), so `isEqualTo` is identity.
558
+ */
559
+ var SqlUniqueIR = class SqlUniqueIR extends SqlSchemaIRNode {
560
+ nodeKind = RelationalSchemaNodeKind.unique;
561
+ columns;
562
+ constructor(input) {
563
+ super();
564
+ this.columns = [...input.columns];
565
+ if (input.name !== void 0) this.name = input.name;
566
+ if (input.annotations !== void 0) this.annotations = input.annotations;
567
+ defineNonEnumerable(this, "dependsOn", input.dependsOn);
568
+ freezeNode(this);
569
+ }
570
+ get id() {
571
+ return `unique:${this.columns.join(",")}`;
572
+ }
573
+ children() {
574
+ return [];
575
+ }
576
+ static is(node) {
577
+ return node.nodeKind === RelationalSchemaNodeKind.unique;
578
+ }
579
+ isEqualTo(other) {
580
+ const node = blindCast(other);
581
+ assertNode(node, "SqlUniqueIR", SqlUniqueIR.is);
582
+ return this.id === node.id;
583
+ }
584
+ };
585
+ //#endregion
586
+ //#region src/ir/sql-table-ir.ts
587
+ /**
588
+ * Schema IR node for a single table as observed by introspection.
589
+ *
590
+ * Unlike the Contract IR `StorageTable`, this carries the table's
591
+ * `name` — introspection queries return tables as arrays and the
592
+ * verifier keys them into `SqlSchemaIR.tables` afterwards, so the name
593
+ * stays on the table object for downstream call sites that walk
594
+ * `Object.values(schema.tables)`.
595
+ *
596
+ * The constructor normalises nested IR-class fields so downstream
597
+ * walks see a uniform AST regardless of whether the input was a
598
+ * plain-data literal (from introspection) or already-constructed
599
+ * class instances.
600
+ *
601
+ * Implements `DiffableNode` so a flat (single-schema) tree is directly
602
+ * diffable: `id` is the table name; `isEqualTo` is identity (the table's
603
+ * structural drift is entirely expressed by its children); `children()`
604
+ * yields every column, the primary key (when present), every foreign key,
605
+ * unique, index, and check constraint — the same composition and order as
606
+ * the Postgres table node, minus policies.
607
+ */
608
+ var SqlTableIR = class extends SqlSchemaIRNode {
609
+ nodeKind = RelationalSchemaNodeKind.table;
610
+ name;
611
+ columns;
612
+ foreignKeys;
613
+ uniques;
614
+ indexes;
615
+ constructor(input) {
616
+ super();
617
+ this.name = input.name;
618
+ this.columns = Object.freeze(Object.fromEntries(Object.entries(input.columns).map(([key, col]) => [key, col instanceof SqlColumnIR ? col : new SqlColumnIR(col)])));
619
+ this.foreignKeys = Object.freeze(input.foreignKeys.map((fk) => fk instanceof SqlForeignKeyIR ? fk : new SqlForeignKeyIR(fk)));
620
+ this.uniques = Object.freeze(input.uniques.map((u) => u instanceof SqlUniqueIR ? u : new SqlUniqueIR(u)));
621
+ this.indexes = Object.freeze(input.indexes.map((i) => i instanceof SqlIndexIR ? i : new SqlIndexIR(i)));
622
+ if (input.primaryKey !== void 0) this.primaryKey = input.primaryKey instanceof PrimaryKey ? input.primaryKey : new PrimaryKey(input.primaryKey);
623
+ if (input.annotations !== void 0) this.annotations = input.annotations;
624
+ if (input.checks !== void 0 && input.checks.length > 0) this.checks = Object.freeze(input.checks.map((c) => c instanceof SqlCheckConstraintIR ? c : new SqlCheckConstraintIR(c)));
625
+ freezeNode(this);
626
+ }
627
+ get id() {
628
+ return this.name;
629
+ }
630
+ isEqualTo(other) {
631
+ return this.id === other.id;
632
+ }
633
+ children() {
634
+ return [
635
+ ...Object.values(this.columns),
636
+ ...this.primaryKey ? [this.primaryKey] : [],
637
+ ...this.foreignKeys,
638
+ ...this.uniques,
639
+ ...this.indexes,
640
+ ...this.checks ?? []
641
+ ];
642
+ }
643
+ };
644
+ //#endregion
645
+ //#region src/ir/sql-schema-ir.ts
646
+ /**
647
+ * Root Schema IR node representing the complete database schema as
648
+ * observed by introspection. Target-agnostic; used by both verifiers
649
+ * (compare against intended Contract storage) and migration planners
650
+ * (derive operations needed to reconcile).
651
+ *
652
+ * The constructor normalises nested `SqlTableIR` instances so
653
+ * downstream walks see a uniform AST regardless of whether the input
654
+ * was a plain-data literal or already-constructed class instances.
655
+ *
656
+ * Implements `DiffableNode` as the root of a flat (single-schema) diff
657
+ * tree: `id` is the fixed sentinel `'database'` (roots have no siblings),
658
+ * `isEqualTo` is identity (a container has no own attributes), and
659
+ * `children()` yields the table nodes.
660
+ */
661
+ var SqlSchemaIR = class extends SqlSchemaIRNode {
662
+ nodeKind = RelationalSchemaNodeKind.schema;
663
+ tables;
664
+ constructor(input) {
665
+ super();
666
+ this.tables = Object.freeze(Object.fromEntries(Object.entries(input.tables).map(([key, t]) => [key, t instanceof SqlTableIR ? t : new SqlTableIR(t)])));
667
+ if (input.annotations !== void 0) this.annotations = input.annotations;
668
+ freezeNode(this);
669
+ }
670
+ get id() {
671
+ return "database";
672
+ }
673
+ isEqualTo(other) {
674
+ return this.id === other.id;
675
+ }
676
+ children() {
677
+ return Object.values(this.tables);
678
+ }
679
+ };
680
+ //#endregion
681
+ export { SqlForeignKeyIR as a, SqlCheckConstraintIR as c, assertNode as d, defineNonEnumerable as f, relationalNodeGranularity as h, SqlIndexIR as i, PrimaryKey as l, relationalNodeEntityKind as m, SqlTableIR as n, SqlColumnIR as o, RelationalSchemaNodeKind as p, SqlUniqueIR as r, SqlColumnDefaultIR as s, SqlSchemaIR as t, SqlSchemaIRNode as u };
682
+
683
+ //# sourceMappingURL=types-pxgoVAJq.mjs.map