@prisma-next/sql-schema-ir 0.14.0 → 0.15.0-dev.1
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/dist/exports/types.d.mts +2 -2
- package/dist/exports/types.mjs +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/types-CXbBE9Uv.d.mts +580 -0
- package/dist/types-CXbBE9Uv.d.mts.map +1 -0
- package/dist/types-CZqCTmou.mjs +679 -0
- package/dist/types-CZqCTmou.mjs.map +1 -0
- package/package.json +9 -8
- package/src/exports/types.ts +6 -0
- package/src/ir/primary-key.ts +38 -2
- package/src/ir/resolved-default-equality.ts +79 -0
- package/src/ir/schema-node-kinds.ts +86 -0
- package/src/ir/sql-check-constraint-ir.ts +43 -2
- package/src/ir/sql-column-default-ir.ts +96 -0
- package/src/ir/sql-column-ir.ts +142 -2
- package/src/ir/sql-foreign-key-ir.ts +71 -2
- package/src/ir/sql-index-ir.ts +84 -2
- package/src/ir/sql-schema-ir-node.ts +58 -7
- package/src/ir/sql-schema-ir.ts +22 -1
- package/src/ir/sql-table-ir.ts +31 -1
- package/src/ir/sql-unique-ir.ts +35 -2
- package/src/types.ts +10 -1
- package/dist/types-C0WFfEkA.mjs +0 -220
- package/dist/types-C0WFfEkA.mjs.map +0 -1
- package/dist/types-Du44_nsJ.d.mts +0 -261
- package/dist/types-Du44_nsJ.d.mts.map +0 -1
package/src/ir/sql-column-ir.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import type { ColumnDefault } from '@prisma-next/contract/types';
|
|
2
|
+
import type { CodecRef } from '@prisma-next/framework-components/codec';
|
|
3
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
1
4
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
2
|
-
import {
|
|
5
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
6
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
7
|
+
import { RelationalSchemaNodeKind } from './schema-node-kinds';
|
|
8
|
+
import { SqlColumnDefaultIR } from './sql-column-default-ir';
|
|
9
|
+
import { assertNode, defineNonEnumerable, SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Namespaced annotations for extensibility. Each namespace
|
|
@@ -16,6 +23,56 @@ export interface SqlColumnIRInput {
|
|
|
16
23
|
/** Raw database default expression (e.g. `'hello'::text`, `nextval('seq')`). */
|
|
17
24
|
readonly default?: string;
|
|
18
25
|
readonly annotations?: SqlAnnotations;
|
|
26
|
+
/** True when the column is a native array (e.g. `text[]`, `int4[]`). The `nativeType` carries the element type only (e.g. `text`, `int4`). */
|
|
27
|
+
readonly many?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Fully resolved native type, comparable across the two diff sides:
|
|
30
|
+
* the contract-derived side stamps the codec-expanded type (typeRef
|
|
31
|
+
* resolved, parameterized types expanded, `[]` appended for arrays);
|
|
32
|
+
* the introspected side stamps the target-normalized type with the same
|
|
33
|
+
* `[]` convention. Stamped at construction by derivation/introspection;
|
|
34
|
+
* absent on raw hand-built nodes.
|
|
35
|
+
*/
|
|
36
|
+
readonly resolvedNativeType?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Structured default, comparable across the two diff sides: the
|
|
39
|
+
* contract-derived side stamps the contract's `ColumnDefault`; the
|
|
40
|
+
* introspected side stamps the target default-normalizer's parse of the
|
|
41
|
+
* raw expression. Absent when the column declares no default, or when
|
|
42
|
+
* the introspected raw default could not be parsed.
|
|
43
|
+
*/
|
|
44
|
+
readonly resolvedDefault?: ColumnDefault;
|
|
45
|
+
/**
|
|
46
|
+
* The column's resolved codec reference — the identity the migration
|
|
47
|
+
* planner's op-builders resolve DDL type rendering against at plan time
|
|
48
|
+
* (parameterized type expansion, e.g. `character` + `{ length: 36 }` →
|
|
49
|
+
* `character(36)`), calling the same codec hooks the pre-`plan(start,
|
|
50
|
+
* end)` op-path called. Carried the same way the query AST carries
|
|
51
|
+
* `CodecRef` (TML-2456) and the migration DDL renderer (TML-2918).
|
|
52
|
+
* Stamped on the EXPECTED (contract-derived) column at derivation,
|
|
53
|
+
* post-`typeRef` resolution (the referenced storage type's own
|
|
54
|
+
* codec/params, not the column's `typeRef` pointer). Absent on
|
|
55
|
+
* introspected/hand-built nodes — the actual side never renders DDL —
|
|
56
|
+
* and never compared by `isEqualTo`.
|
|
57
|
+
*/
|
|
58
|
+
readonly codecRef?: CodecRef;
|
|
59
|
+
/**
|
|
60
|
+
* The column's resolved BASE native type: pre-parameter-expansion,
|
|
61
|
+
* pre-array-suffix (e.g. `character`, not `character(36)` or
|
|
62
|
+
* `character[]`). Distinct from {@link resolvedNativeType} (the EXPANDED
|
|
63
|
+
* comparison value) — DDL rendering re-expands a parameterized type at
|
|
64
|
+
* plan time from this base, so it must not already carry the expansion.
|
|
65
|
+
* Stamped alongside {@link codecRef}.
|
|
66
|
+
*/
|
|
67
|
+
readonly codecBaseNativeType?: string;
|
|
68
|
+
/**
|
|
69
|
+
* True when the contract column declared its type via a named
|
|
70
|
+
* `storage.types` reference (`typeRef`) rather than inline fields — the
|
|
71
|
+
* migration planner quotes the base native type as an identifier in this
|
|
72
|
+
* case (e.g. a native enum's type name), matching the pre-`plan(start,
|
|
73
|
+
* end)` rendering exactly.
|
|
74
|
+
*/
|
|
75
|
+
readonly codecNamedType?: boolean;
|
|
19
76
|
}
|
|
20
77
|
|
|
21
78
|
/**
|
|
@@ -24,13 +81,34 @@ export interface SqlColumnIRInput {
|
|
|
24
81
|
* the column's `name` (Schema IR columns are returned as arrays from
|
|
25
82
|
* introspection queries; the parent table re-keys them into a record
|
|
26
83
|
* for downstream consumers).
|
|
84
|
+
*
|
|
85
|
+
* Implements `DiffableNode` so a column is directly a table's diff-tree
|
|
86
|
+
* child: `id` is the column name (unique among a table's columns); `isEqualTo`
|
|
87
|
+
* compares this column's own attributes only — never children, since a
|
|
88
|
+
* column is a leaf. When both sides carry `resolvedNativeType` (stamped at
|
|
89
|
+
* derivation/introspection), the comparison uses the resolved values —
|
|
90
|
+
* resolved native type, nullability, and structured default equality per
|
|
91
|
+
* the relational walk's `columnDefaultsEqual` semantics, with `this` as the
|
|
92
|
+
* expected side. Otherwise it falls back to comparing raw fields.
|
|
27
93
|
*/
|
|
28
|
-
export class SqlColumnIR extends SqlSchemaIRNode {
|
|
94
|
+
export class SqlColumnIR extends SqlSchemaIRNode implements DiffableNode {
|
|
95
|
+
override readonly nodeKind = RelationalSchemaNodeKind.column;
|
|
96
|
+
|
|
29
97
|
readonly name: string;
|
|
30
98
|
readonly nativeType: string;
|
|
31
99
|
readonly nullable: boolean;
|
|
32
100
|
declare readonly default?: string;
|
|
33
101
|
declare readonly annotations?: SqlAnnotations;
|
|
102
|
+
/** True when the column is a native array (e.g. `text[]`, `int4[]`). The `nativeType` carries the element type only (e.g. `text`, `int4`). */
|
|
103
|
+
declare readonly many?: boolean;
|
|
104
|
+
declare readonly resolvedNativeType?: string;
|
|
105
|
+
declare readonly resolvedDefault?: ColumnDefault;
|
|
106
|
+
/** See {@link SqlColumnIRInput.codecRef}. Non-enumerable so it stays out of JSON and structural equality. */
|
|
107
|
+
declare readonly codecRef?: CodecRef;
|
|
108
|
+
/** See {@link SqlColumnIRInput.codecBaseNativeType}. Non-enumerable, same reason as {@link codecRef}. */
|
|
109
|
+
declare readonly codecBaseNativeType?: string;
|
|
110
|
+
/** See {@link SqlColumnIRInput.codecNamedType}. Non-enumerable, same reason as {@link codecRef}. */
|
|
111
|
+
declare readonly codecNamedType?: boolean;
|
|
34
112
|
|
|
35
113
|
constructor(input: SqlColumnIRInput) {
|
|
36
114
|
super();
|
|
@@ -39,6 +117,68 @@ export class SqlColumnIR extends SqlSchemaIRNode {
|
|
|
39
117
|
this.nullable = input.nullable;
|
|
40
118
|
if (input.default !== undefined) this.default = input.default;
|
|
41
119
|
if (input.annotations !== undefined) this.annotations = input.annotations;
|
|
120
|
+
if (input.many !== undefined) this.many = input.many;
|
|
121
|
+
if (input.resolvedNativeType !== undefined) this.resolvedNativeType = input.resolvedNativeType;
|
|
122
|
+
if (input.resolvedDefault !== undefined) this.resolvedDefault = input.resolvedDefault;
|
|
123
|
+
defineNonEnumerable(this, 'codecRef', input.codecRef);
|
|
124
|
+
defineNonEnumerable(this, 'codecBaseNativeType', input.codecBaseNativeType);
|
|
125
|
+
defineNonEnumerable(this, 'codecNamedType', input.codecNamedType);
|
|
42
126
|
freezeNode(this);
|
|
43
127
|
}
|
|
128
|
+
|
|
129
|
+
get id(): string {
|
|
130
|
+
return `column:${this.name}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The column's default, when declared/present, is the column's one child
|
|
135
|
+
* node — it has an extra/missing/drift lifecycle of its own, so the differ
|
|
136
|
+
* recurses to it rather than `isEqualTo` comparing it. Built transiently
|
|
137
|
+
* from this column's own fields.
|
|
138
|
+
*/
|
|
139
|
+
children(): readonly DiffableNode[] {
|
|
140
|
+
if (this.resolvedDefault === undefined && this.default === undefined) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
return [
|
|
144
|
+
new SqlColumnDefaultIR({
|
|
145
|
+
...ifDefined('resolved', this.resolvedDefault),
|
|
146
|
+
...ifDefined('raw', this.default),
|
|
147
|
+
...ifDefined('nativeTypeContext', this.resolvedNativeType),
|
|
148
|
+
// Contract-derived and introspected columns both set `this.many`
|
|
149
|
+
// directly (with `nativeType` as the bare element type; array-ness
|
|
150
|
+
// never rides on a `[]` suffix). `codecRef.many` is a fallback for
|
|
151
|
+
// hand-built nodes that carry a codec ref without the column-level
|
|
152
|
+
// flag. Either source works for the default node's array-literal
|
|
153
|
+
// rendering.
|
|
154
|
+
...ifDefined('many', this.many ?? this.codecRef?.many),
|
|
155
|
+
}),
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
static is(node: SqlSchemaIRNode): node is SqlColumnIR {
|
|
160
|
+
return node.nodeKind === RelationalSchemaNodeKind.column;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Compares the column's own attributes only — the default lives on the
|
|
165
|
+
* child node. When both sides carry `resolvedNativeType`, the resolved
|
|
166
|
+
* value governs (array-ness rides on its `[]` suffix); otherwise raw
|
|
167
|
+
* fields compare.
|
|
168
|
+
*/
|
|
169
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
170
|
+
const node = blindCast<
|
|
171
|
+
SqlSchemaIRNode,
|
|
172
|
+
'every diff-tree node the differ pairs is a SqlSchemaIRNode'
|
|
173
|
+
>(other);
|
|
174
|
+
assertNode(node, 'SqlColumnIR', SqlColumnIR.is);
|
|
175
|
+
if (this.resolvedNativeType !== undefined && node.resolvedNativeType !== undefined) {
|
|
176
|
+
return this.resolvedNativeType === node.resolvedNativeType && this.nullable === node.nullable;
|
|
177
|
+
}
|
|
178
|
+
return (
|
|
179
|
+
this.nativeType === node.nativeType &&
|
|
180
|
+
this.nullable === node.nullable &&
|
|
181
|
+
Boolean(this.many) === Boolean(node.many)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
44
184
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
1
2
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
3
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
4
|
+
import { RelationalSchemaNodeKind } from './schema-node-kinds';
|
|
2
5
|
import type { SqlAnnotations } from './sql-column-ir';
|
|
3
|
-
import { SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
6
|
+
import { assertNode, SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
4
7
|
|
|
5
8
|
export type SqlReferentialAction = 'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault';
|
|
6
9
|
|
|
@@ -14,6 +17,15 @@ export interface SqlForeignKeyIRInput {
|
|
|
14
17
|
readonly onDelete?: SqlReferentialAction;
|
|
15
18
|
readonly onUpdate?: SqlReferentialAction;
|
|
16
19
|
readonly annotations?: SqlAnnotations;
|
|
20
|
+
/**
|
|
21
|
+
* The real live DDL namespace of the referenced table, comparable across
|
|
22
|
+
* the two diff sides. Contract-derived trees stamp it explicitly (resolving
|
|
23
|
+
* namespace ids — including the unbound sentinel — to the DDL namespace);
|
|
24
|
+
* introspected FKs default it to `referencedSchema`, whose value already
|
|
25
|
+
* is the live namespace. Folded into `id` in place of the raw value so an
|
|
26
|
+
* unbound-namespace contract FK pairs with its introspected counterpart.
|
|
27
|
+
*/
|
|
28
|
+
readonly resolvedReferencedNamespace?: string;
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
/**
|
|
@@ -23,8 +35,19 @@ export interface SqlForeignKeyIRInput {
|
|
|
23
35
|
* etc.) and intentionally differ from the Contract IR's nested
|
|
24
36
|
* `references: { table, columns }` shape so that the verifier's
|
|
25
37
|
* structural comparison stays explicit about which side it's reading.
|
|
38
|
+
*
|
|
39
|
+
* Implements `DiffableNode` so a foreign key is directly a table's diff-tree
|
|
40
|
+
* child. Foreign keys are frequently unnamed (introspection may not carry a
|
|
41
|
+
* constraint name, and the contract side never invents one), so `id` is
|
|
42
|
+
* derived from the referencing/referenced coordinates rather than `name` —
|
|
43
|
+
* the same tuple that makes two FK constraints the same constraint. This
|
|
44
|
+
* also serves as the comparison key: two FKs with the same coordinates are
|
|
45
|
+
* paired by the differ, and `isEqualTo` then compares the remaining
|
|
46
|
+
* attribute — the referential actions.
|
|
26
47
|
*/
|
|
27
|
-
export class SqlForeignKeyIR extends SqlSchemaIRNode {
|
|
48
|
+
export class SqlForeignKeyIR extends SqlSchemaIRNode implements DiffableNode {
|
|
49
|
+
override readonly nodeKind = RelationalSchemaNodeKind.foreignKey;
|
|
50
|
+
|
|
28
51
|
readonly columns: readonly string[];
|
|
29
52
|
readonly referencedTable: string;
|
|
30
53
|
readonly referencedColumns: readonly string[];
|
|
@@ -33,6 +56,7 @@ export class SqlForeignKeyIR extends SqlSchemaIRNode {
|
|
|
33
56
|
declare readonly onDelete?: SqlReferentialAction;
|
|
34
57
|
declare readonly onUpdate?: SqlReferentialAction;
|
|
35
58
|
declare readonly annotations?: SqlAnnotations;
|
|
59
|
+
declare readonly resolvedReferencedNamespace?: string;
|
|
36
60
|
|
|
37
61
|
constructor(input: SqlForeignKeyIRInput) {
|
|
38
62
|
super();
|
|
@@ -44,6 +68,51 @@ export class SqlForeignKeyIR extends SqlSchemaIRNode {
|
|
|
44
68
|
if (input.onDelete !== undefined) this.onDelete = input.onDelete;
|
|
45
69
|
if (input.onUpdate !== undefined) this.onUpdate = input.onUpdate;
|
|
46
70
|
if (input.annotations !== undefined) this.annotations = input.annotations;
|
|
71
|
+
const resolvedReferencedNamespace = input.resolvedReferencedNamespace ?? input.referencedSchema;
|
|
72
|
+
if (resolvedReferencedNamespace !== undefined) {
|
|
73
|
+
this.resolvedReferencedNamespace = resolvedReferencedNamespace;
|
|
74
|
+
}
|
|
47
75
|
freezeNode(this);
|
|
48
76
|
}
|
|
77
|
+
|
|
78
|
+
get id(): string {
|
|
79
|
+
const referencedNamespace = this.resolvedReferencedNamespace ?? '';
|
|
80
|
+
return `foreign-key:${this.columns.join(',')}->${referencedNamespace}.${this.referencedTable}(${this.referencedColumns.join(',')})`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
children(): readonly DiffableNode[] {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static is(node: SqlSchemaIRNode): node is SqlForeignKeyIR {
|
|
88
|
+
return node.nodeKind === RelationalSchemaNodeKind.foreignKey;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Referential-action comparison with `this` as the expected side, matching
|
|
93
|
+
* the relational walk's `getReferentialActionMismatches`: `noAction` is the
|
|
94
|
+
* database default and equivalent to an undeclared action, and drift is
|
|
95
|
+
* flagged only when the expected side declares a (normalized) action.
|
|
96
|
+
*/
|
|
97
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
98
|
+
const node = blindCast<
|
|
99
|
+
SqlSchemaIRNode,
|
|
100
|
+
'every diff-tree node the differ pairs is a SqlSchemaIRNode'
|
|
101
|
+
>(other);
|
|
102
|
+
assertNode(node, 'SqlForeignKeyIR', SqlForeignKeyIR.is);
|
|
103
|
+
return (
|
|
104
|
+
referentialActionMatches(this.onDelete, node.onDelete) &&
|
|
105
|
+
referentialActionMatches(this.onUpdate, node.onUpdate)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function referentialActionMatches(
|
|
111
|
+
expected: SqlReferentialAction | undefined,
|
|
112
|
+
actual: SqlReferentialAction | undefined,
|
|
113
|
+
): boolean {
|
|
114
|
+
const normalizedExpected = expected === 'noAction' ? undefined : expected;
|
|
115
|
+
if (normalizedExpected === undefined) return true;
|
|
116
|
+
const normalizedActual = actual === 'noAction' ? undefined : actual;
|
|
117
|
+
return normalizedExpected === normalizedActual;
|
|
49
118
|
}
|
package/src/ir/sql-index-ir.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
1
2
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
3
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
4
|
+
import { RelationalSchemaNodeKind } from './schema-node-kinds';
|
|
2
5
|
import type { SqlAnnotations } from './sql-column-ir';
|
|
3
|
-
import { SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
6
|
+
import { assertNode, SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
4
7
|
|
|
5
8
|
export interface SqlIndexIRInput {
|
|
6
9
|
readonly columns: readonly string[];
|
|
@@ -17,8 +20,29 @@ export interface SqlIndexIRInput {
|
|
|
17
20
|
* `unique` field — introspection sees the underlying index regardless
|
|
18
21
|
* of whether the user expressed it as `@@index` or `@@unique`, and the
|
|
19
22
|
* verifier needs to distinguish them when comparing to the Contract.
|
|
23
|
+
*
|
|
24
|
+
* Implements `DiffableNode` so an index is directly a table's diff-tree
|
|
25
|
+
* child. Indexes are frequently unnamed, so `id` is derived from the column
|
|
26
|
+
* tuple — the same tuple that makes two indexes the same index, so it
|
|
27
|
+
* doubles as the pairing key. `isEqualTo` is symmetric structural equality
|
|
28
|
+
* on the remaining attributes: `unique`, `type`, and `options`. A unique
|
|
29
|
+
* index and a non-unique index on the same columns are different objects
|
|
30
|
+
* and are not equal — there is no "stronger satisfies weaker".
|
|
31
|
+
*
|
|
32
|
+
* Deliberately column-tuple-only (not `unique`): a contract `@@index`
|
|
33
|
+
* (non-unique) must still pair against a live unique index of the same
|
|
34
|
+
* columns so the differ can report the mismatch as an incompatible index
|
|
35
|
+
* change rather than a spurious missing+extra pair — see
|
|
36
|
+
* `planner.unique-index-structural.test.ts`. The corollary is that two
|
|
37
|
+
* indexes legitimately coexisting on one table with the *same* column tuple
|
|
38
|
+
* (e.g. a unique index and a redundant plain index Postgres has no problem
|
|
39
|
+
* hosting side by side) collide on this id; the postgres control adapter's
|
|
40
|
+
* introspection keeps only one such index per table+column-tuple rather
|
|
41
|
+
* than handing the differ two same-tree siblings it cannot represent.
|
|
20
42
|
*/
|
|
21
|
-
export class SqlIndexIR extends SqlSchemaIRNode {
|
|
43
|
+
export class SqlIndexIR extends SqlSchemaIRNode implements DiffableNode {
|
|
44
|
+
override readonly nodeKind = RelationalSchemaNodeKind.index;
|
|
45
|
+
|
|
22
46
|
readonly columns: readonly string[];
|
|
23
47
|
readonly unique: boolean;
|
|
24
48
|
declare readonly name?: string;
|
|
@@ -36,4 +60,62 @@ export class SqlIndexIR extends SqlSchemaIRNode {
|
|
|
36
60
|
if (input.annotations !== undefined) this.annotations = input.annotations;
|
|
37
61
|
freezeNode(this);
|
|
38
62
|
}
|
|
63
|
+
|
|
64
|
+
get id(): string {
|
|
65
|
+
return `index:${this.columns.join(',')}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
children(): readonly DiffableNode[] {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static is(node: SqlSchemaIRNode): node is SqlIndexIR {
|
|
73
|
+
return node.nodeKind === RelationalSchemaNodeKind.index;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Symmetric structural equality: two paired index nodes are equal iff their
|
|
78
|
+
* `unique` flag, `type`, and (loosely-compared) `options` all match. There
|
|
79
|
+
* is no satisfaction — a unique index does not equal a non-unique index.
|
|
80
|
+
* `options` compares loosely (introspection stringifies reloptions); `type`
|
|
81
|
+
* compares strictly after the introspection-side btree→undefined
|
|
82
|
+
* normalization done at construction.
|
|
83
|
+
*/
|
|
84
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
85
|
+
const node = blindCast<
|
|
86
|
+
SqlSchemaIRNode,
|
|
87
|
+
'every diff-tree node the differ pairs is a SqlSchemaIRNode'
|
|
88
|
+
>(other);
|
|
89
|
+
assertNode(node, 'SqlIndexIR', SqlIndexIR.is);
|
|
90
|
+
return (
|
|
91
|
+
this.unique === node.unique &&
|
|
92
|
+
this.type === node.type &&
|
|
93
|
+
indexOptionsLooselyEqual(this.options, node.options)
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Option-bag equality ported from the relational walk: same key set, values
|
|
100
|
+
* compared via `String()` coercion — Postgres introspection returns
|
|
101
|
+
* reloptions values as raw strings (`'70'`, `'false'`) while contract option
|
|
102
|
+
* leaves are typed (number, boolean, string).
|
|
103
|
+
*/
|
|
104
|
+
function indexOptionsLooselyEqual(
|
|
105
|
+
a: Record<string, unknown> | undefined,
|
|
106
|
+
b: Record<string, unknown> | undefined,
|
|
107
|
+
): boolean {
|
|
108
|
+
const aKeys = a ? Object.keys(a).sort() : [];
|
|
109
|
+
const bKeys = b ? Object.keys(b).sort() : [];
|
|
110
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
111
|
+
for (let i = 0; i < aKeys.length; i += 1) {
|
|
112
|
+
if (aKeys[i] !== bKeys[i]) return false;
|
|
113
|
+
}
|
|
114
|
+
if (aKeys.length === 0) return true;
|
|
115
|
+
for (const key of aKeys) {
|
|
116
|
+
if (String(a?.[key]) !== String(b?.[key])) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
39
121
|
}
|
|
@@ -7,20 +7,32 @@ import { IRNodeBase } from '@prisma-next/framework-components/ir';
|
|
|
7
7
|
*
|
|
8
8
|
* SQL Schema IR represents the actual database state as discovered by
|
|
9
9
|
* introspection (the parallel to SQL Contract IR, which represents the
|
|
10
|
-
* desired state).
|
|
11
|
-
* polymorphic dispatch — verifiers and planners walk by structural
|
|
12
|
-
* position, not by inspecting `kind` — so a single family-level
|
|
13
|
-
* discriminator is sufficient. Future per-leaf overrides land cleanly
|
|
14
|
-
* the same way as on the Contract side.
|
|
10
|
+
* desired state).
|
|
15
11
|
*
|
|
16
12
|
* The discriminator is installed as a non-enumerable own property,
|
|
17
13
|
* matching the SqlNode pattern. This keeps `JSON.stringify(node)`
|
|
18
14
|
* canonical (no `kind` field), keeps `toEqual({...})` test assertions
|
|
19
15
|
* against pre-lift flat shapes passing, and keeps `node.kind` readable
|
|
20
|
-
* for
|
|
16
|
+
* for dispatch.
|
|
17
|
+
*
|
|
18
|
+
* Both `kind` and `nodeKind` are required: every concrete leaf is a node
|
|
19
|
+
* the generic differ can pair and compare, so every leaf must declare which
|
|
20
|
+
* node it is. `nodeKind` has no default here — every direct subclass sets its
|
|
21
|
+
* own literal value (the relational leaves via `RelationalSchemaNodeKind`,
|
|
22
|
+
* target concretions via their own vocabulary, e.g. `PostgresSchemaNodeKind`).
|
|
21
23
|
*/
|
|
22
24
|
export abstract class SqlSchemaIRNode extends IRNodeBase {
|
|
23
|
-
readonly kind
|
|
25
|
+
declare readonly kind: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Enumerable discriminant identifying which node this is (column / primary
|
|
29
|
+
* key / foreign key / unique / index / check / database / namespace /
|
|
30
|
+
* table / policy / role / …). Concretions set a unique value; the
|
|
31
|
+
* `.is`/`.assert` guards compare against it. Unlike `kind`, it is
|
|
32
|
+
* enumerable, so it survives a spread that flattens a node into a plain
|
|
33
|
+
* object.
|
|
34
|
+
*/
|
|
35
|
+
abstract readonly nodeKind: string;
|
|
24
36
|
|
|
25
37
|
constructor() {
|
|
26
38
|
super();
|
|
@@ -32,3 +44,42 @@ export abstract class SqlSchemaIRNode extends IRNodeBase {
|
|
|
32
44
|
});
|
|
33
45
|
}
|
|
34
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Asserts `node` matches `predicate`, narrowing its type to `T`. The one
|
|
50
|
+
* shared implementation every node class's `static assert` and `isEqualTo`
|
|
51
|
+
* reach for, instead of each hand-writing its own throw: the message names
|
|
52
|
+
* the class the caller expected.
|
|
53
|
+
*/
|
|
54
|
+
export function assertNode<T extends SqlSchemaIRNode>(
|
|
55
|
+
node: SqlSchemaIRNode | undefined,
|
|
56
|
+
className: string,
|
|
57
|
+
predicate: (node: SqlSchemaIRNode) => node is T,
|
|
58
|
+
): asserts node is T {
|
|
59
|
+
if (node === undefined || !predicate(node)) {
|
|
60
|
+
throw new Error(`Expected a ${className} but got nodeKind=${node?.nodeKind ?? 'undefined'}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Defines a non-enumerable own property, the same treatment `kind` gets
|
|
66
|
+
* above: a derivation-time render-support field stays out of
|
|
67
|
+
* `JSON.stringify`, `toEqual({...})` structural assertions, and spreads,
|
|
68
|
+
* while remaining directly readable (`node.field`) for the one consumer
|
|
69
|
+
* that resolves it at plan time. A no-op when `value` is `undefined` — the
|
|
70
|
+
* property is simply absent, matching every other optional field on these
|
|
71
|
+
* nodes.
|
|
72
|
+
*/
|
|
73
|
+
export function defineNonEnumerable<T extends object>(
|
|
74
|
+
target: T,
|
|
75
|
+
key: string,
|
|
76
|
+
value: unknown,
|
|
77
|
+
): void {
|
|
78
|
+
if (value === undefined) return;
|
|
79
|
+
Object.defineProperty(target, key, {
|
|
80
|
+
value,
|
|
81
|
+
enumerable: false,
|
|
82
|
+
writable: false,
|
|
83
|
+
configurable: false,
|
|
84
|
+
});
|
|
85
|
+
}
|
package/src/ir/sql-schema-ir.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
1
2
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
3
|
+
import { RelationalSchemaNodeKind } from './schema-node-kinds';
|
|
2
4
|
import type { SqlAnnotations } from './sql-column-ir';
|
|
3
5
|
import { SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
4
6
|
import { SqlTableIR, type SqlTableIRInput } from './sql-table-ir';
|
|
@@ -17,8 +19,15 @@ export interface SqlSchemaIRInput {
|
|
|
17
19
|
* The constructor normalises nested `SqlTableIR` instances so
|
|
18
20
|
* downstream walks see a uniform AST regardless of whether the input
|
|
19
21
|
* was a plain-data literal or already-constructed class instances.
|
|
22
|
+
*
|
|
23
|
+
* Implements `DiffableNode` as the root of a flat (single-schema) diff
|
|
24
|
+
* tree: `id` is the fixed sentinel `'database'` (roots have no siblings),
|
|
25
|
+
* `isEqualTo` is identity (a container has no own attributes), and
|
|
26
|
+
* `children()` yields the table nodes.
|
|
20
27
|
*/
|
|
21
|
-
export class SqlSchemaIR extends SqlSchemaIRNode {
|
|
28
|
+
export class SqlSchemaIR extends SqlSchemaIRNode implements DiffableNode {
|
|
29
|
+
override readonly nodeKind = RelationalSchemaNodeKind.schema;
|
|
30
|
+
|
|
22
31
|
readonly tables: Readonly<Record<string, SqlTableIR>>;
|
|
23
32
|
declare readonly annotations?: SqlAnnotations;
|
|
24
33
|
|
|
@@ -35,4 +44,16 @@ export class SqlSchemaIR extends SqlSchemaIRNode {
|
|
|
35
44
|
if (input.annotations !== undefined) this.annotations = input.annotations;
|
|
36
45
|
freezeNode(this);
|
|
37
46
|
}
|
|
47
|
+
|
|
48
|
+
get id(): string {
|
|
49
|
+
return 'database';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
53
|
+
return this.id === other.id;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
children(): readonly DiffableNode[] {
|
|
57
|
+
return Object.values(this.tables);
|
|
58
|
+
}
|
|
38
59
|
}
|
package/src/ir/sql-table-ir.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
1
2
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
2
3
|
import { PrimaryKey, type PrimaryKeyInput } from './primary-key';
|
|
4
|
+
import { RelationalSchemaNodeKind } from './schema-node-kinds';
|
|
3
5
|
import { SqlCheckConstraintIR, type SqlCheckConstraintIRInput } from './sql-check-constraint-ir';
|
|
4
6
|
import { type SqlAnnotations, SqlColumnIR, type SqlColumnIRInput } from './sql-column-ir';
|
|
5
7
|
import { SqlForeignKeyIR, type SqlForeignKeyIRInput } from './sql-foreign-key-ir';
|
|
@@ -32,8 +34,17 @@ export interface SqlTableIRInput {
|
|
|
32
34
|
* walks see a uniform AST regardless of whether the input was a
|
|
33
35
|
* plain-data literal (from introspection) or already-constructed
|
|
34
36
|
* class instances.
|
|
37
|
+
*
|
|
38
|
+
* Implements `DiffableNode` so a flat (single-schema) tree is directly
|
|
39
|
+
* diffable: `id` is the table name; `isEqualTo` is identity (the table's
|
|
40
|
+
* structural drift is entirely expressed by its children); `children()`
|
|
41
|
+
* yields every column, the primary key (when present), every foreign key,
|
|
42
|
+
* unique, index, and check constraint — the same composition and order as
|
|
43
|
+
* the Postgres table node, minus policies.
|
|
35
44
|
*/
|
|
36
|
-
export class SqlTableIR extends SqlSchemaIRNode {
|
|
45
|
+
export class SqlTableIR extends SqlSchemaIRNode implements DiffableNode {
|
|
46
|
+
override readonly nodeKind = RelationalSchemaNodeKind.table;
|
|
47
|
+
|
|
37
48
|
readonly name: string;
|
|
38
49
|
readonly columns: Readonly<Record<string, SqlColumnIR>>;
|
|
39
50
|
readonly foreignKeys: ReadonlyArray<SqlForeignKeyIR>;
|
|
@@ -79,4 +90,23 @@ export class SqlTableIR extends SqlSchemaIRNode {
|
|
|
79
90
|
}
|
|
80
91
|
freezeNode(this);
|
|
81
92
|
}
|
|
93
|
+
|
|
94
|
+
get id(): string {
|
|
95
|
+
return this.name;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
99
|
+
return this.id === other.id;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
children(): readonly DiffableNode[] {
|
|
103
|
+
return [
|
|
104
|
+
...Object.values(this.columns),
|
|
105
|
+
...(this.primaryKey ? [this.primaryKey] : []),
|
|
106
|
+
...this.foreignKeys,
|
|
107
|
+
...this.uniques,
|
|
108
|
+
...this.indexes,
|
|
109
|
+
...(this.checks ?? []),
|
|
110
|
+
];
|
|
111
|
+
}
|
|
82
112
|
}
|
package/src/ir/sql-unique-ir.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
1
2
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
3
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
4
|
+
import { RelationalSchemaNodeKind } from './schema-node-kinds';
|
|
2
5
|
import type { SqlAnnotations } from './sql-column-ir';
|
|
3
|
-
import { SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
6
|
+
import { assertNode, SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
4
7
|
|
|
5
8
|
export interface SqlUniqueIRInput {
|
|
6
9
|
readonly columns: readonly string[];
|
|
@@ -11,8 +14,17 @@ export interface SqlUniqueIRInput {
|
|
|
11
14
|
/**
|
|
12
15
|
* Schema IR node for a table-level unique constraint as observed by
|
|
13
16
|
* introspection.
|
|
17
|
+
*
|
|
18
|
+
* Implements `DiffableNode` so a unique constraint is directly a table's
|
|
19
|
+
* diff-tree child. Unique constraints are frequently unnamed, so `id` is
|
|
20
|
+
* derived from the column tuple rather than `name` — the column tuple is
|
|
21
|
+
* also what makes two unique constraints the same constraint, so it doubles
|
|
22
|
+
* as the pairing key. There are no further attributes to compare once
|
|
23
|
+
* columns are equal (the differ pairs on `id`), so `isEqualTo` is identity.
|
|
14
24
|
*/
|
|
15
|
-
export class SqlUniqueIR extends SqlSchemaIRNode {
|
|
25
|
+
export class SqlUniqueIR extends SqlSchemaIRNode implements DiffableNode {
|
|
26
|
+
override readonly nodeKind = RelationalSchemaNodeKind.unique;
|
|
27
|
+
|
|
16
28
|
readonly columns: readonly string[];
|
|
17
29
|
declare readonly name?: string;
|
|
18
30
|
declare readonly annotations?: SqlAnnotations;
|
|
@@ -24,4 +36,25 @@ export class SqlUniqueIR extends SqlSchemaIRNode {
|
|
|
24
36
|
if (input.annotations !== undefined) this.annotations = input.annotations;
|
|
25
37
|
freezeNode(this);
|
|
26
38
|
}
|
|
39
|
+
|
|
40
|
+
get id(): string {
|
|
41
|
+
return `unique:${this.columns.join(',')}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
children(): readonly DiffableNode[] {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static is(node: SqlSchemaIRNode): node is SqlUniqueIR {
|
|
49
|
+
return node.nodeKind === RelationalSchemaNodeKind.unique;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
53
|
+
const node = blindCast<
|
|
54
|
+
SqlSchemaIRNode,
|
|
55
|
+
'every diff-tree node the differ pairs is a SqlSchemaIRNode'
|
|
56
|
+
>(other);
|
|
57
|
+
assertNode(node, 'SqlUniqueIR', SqlUniqueIR.is);
|
|
58
|
+
return this.id === node.id;
|
|
59
|
+
}
|
|
27
60
|
}
|
package/src/types.ts
CHANGED
|
@@ -10,10 +10,19 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
export { PrimaryKey, type PrimaryKeyInput } from './ir/primary-key';
|
|
13
|
+
export {
|
|
14
|
+
RelationalSchemaNodeKind,
|
|
15
|
+
relationalNodeEntityKind,
|
|
16
|
+
relationalNodeGranularity,
|
|
17
|
+
} from './ir/schema-node-kinds';
|
|
13
18
|
export {
|
|
14
19
|
SqlCheckConstraintIR,
|
|
15
20
|
type SqlCheckConstraintIRInput,
|
|
16
21
|
} from './ir/sql-check-constraint-ir';
|
|
22
|
+
export {
|
|
23
|
+
SqlColumnDefaultIR,
|
|
24
|
+
type SqlColumnDefaultIRInput,
|
|
25
|
+
} from './ir/sql-column-default-ir';
|
|
17
26
|
export {
|
|
18
27
|
type SqlAnnotations,
|
|
19
28
|
SqlColumnIR,
|
|
@@ -26,7 +35,7 @@ export {
|
|
|
26
35
|
} from './ir/sql-foreign-key-ir';
|
|
27
36
|
export { SqlIndexIR, type SqlIndexIRInput } from './ir/sql-index-ir';
|
|
28
37
|
export { SqlSchemaIR, type SqlSchemaIRInput } from './ir/sql-schema-ir';
|
|
29
|
-
export { SqlSchemaIRNode } from './ir/sql-schema-ir-node';
|
|
38
|
+
export { assertNode, SqlSchemaIRNode } from './ir/sql-schema-ir-node';
|
|
30
39
|
export { SqlTableIR, type SqlTableIRInput } from './ir/sql-table-ir';
|
|
31
40
|
export { SqlUniqueIR, type SqlUniqueIRInput } from './ir/sql-unique-ir';
|
|
32
41
|
|