@prisma-next/sql-schema-ir 0.14.0-dev.7 → 0.14.0-dev.71
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-DGBX_sNF.mjs +668 -0
- package/dist/types-DGBX_sNF.mjs.map +1 -0
- package/dist/types-ouvPYrIy.d.mts +569 -0
- package/dist/types-ouvPYrIy.d.mts.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 +141 -2
- package/src/ir/sql-foreign-key-ir.ts +71 -2
- package/src/ir/sql-index-ir.ts +73 -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/primary-key.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
1
2
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
2
|
-
import {
|
|
3
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
4
|
+
import { RelationalSchemaNodeKind } from './schema-node-kinds';
|
|
5
|
+
import { assertNode, SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
3
6
|
|
|
4
7
|
export interface PrimaryKeyInput {
|
|
5
8
|
readonly columns: readonly string[];
|
|
@@ -11,8 +14,17 @@ export interface PrimaryKeyInput {
|
|
|
11
14
|
* shape (same `columns` + optional `name`) so verification can compare
|
|
12
15
|
* intent and actual structurally. Defined here independently to avoid
|
|
13
16
|
* a sql-schema-ir -> sql-contract dependency.
|
|
17
|
+
*
|
|
18
|
+
* Implements `DiffableNode` so a primary key is directly a table's diff-tree
|
|
19
|
+
* child. `id` is a fixed sentinel — a table has at most one primary key, so
|
|
20
|
+
* there is never a sibling to disambiguate against. `isEqualTo` compares the
|
|
21
|
+
* column tuple; the PK's own `name` is a database-assigned label with no
|
|
22
|
+
* semantic weight, so it is not compared (mirrors the policy node's
|
|
23
|
+
* name-insensitivity to non-identifying detail).
|
|
14
24
|
*/
|
|
15
|
-
export class PrimaryKey extends SqlSchemaIRNode {
|
|
25
|
+
export class PrimaryKey extends SqlSchemaIRNode implements DiffableNode {
|
|
26
|
+
override readonly nodeKind = RelationalSchemaNodeKind.primaryKey;
|
|
27
|
+
|
|
16
28
|
readonly columns: readonly string[];
|
|
17
29
|
declare readonly name?: string;
|
|
18
30
|
|
|
@@ -22,4 +34,28 @@ export class PrimaryKey extends SqlSchemaIRNode {
|
|
|
22
34
|
if (input.name !== undefined) this.name = input.name;
|
|
23
35
|
freezeNode(this);
|
|
24
36
|
}
|
|
37
|
+
|
|
38
|
+
get id(): string {
|
|
39
|
+
return 'primary-key';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
children(): readonly DiffableNode[] {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static is(node: SqlSchemaIRNode): node is PrimaryKey {
|
|
47
|
+
return node.nodeKind === RelationalSchemaNodeKind.primaryKey;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
51
|
+
const node = blindCast<
|
|
52
|
+
SqlSchemaIRNode,
|
|
53
|
+
'every diff-tree node the differ pairs is a SqlSchemaIRNode'
|
|
54
|
+
>(other);
|
|
55
|
+
assertNode(node, 'PrimaryKey', PrimaryKey.is);
|
|
56
|
+
return (
|
|
57
|
+
this.columns.length === node.columns.length &&
|
|
58
|
+
this.columns.every((c, i) => c === node.columns[i])
|
|
59
|
+
);
|
|
60
|
+
}
|
|
25
61
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ColumnDefault } from '@prisma-next/contract/types';
|
|
2
|
+
import { canonicalStringify } from '@prisma-next/utils/canonical-stringify';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Structural equality for two resolved column defaults, ported from the
|
|
6
|
+
* relational walk's `columnDefaultsEqual` normalized branch: kinds must
|
|
7
|
+
* match; literal values are normalized (Date and temporal-typed strings to
|
|
8
|
+
* ISO instants) then compared canonically (JSON objects match their
|
|
9
|
+
* canonical string form); function expressions compare case- and
|
|
10
|
+
* whitespace-insensitively.
|
|
11
|
+
*
|
|
12
|
+
* `nativeType` provides the temporal-normalization context (the actual
|
|
13
|
+
* side's resolved native type in a diff comparison).
|
|
14
|
+
*/
|
|
15
|
+
export function resolvedDefaultsEqual(
|
|
16
|
+
expected: ColumnDefault,
|
|
17
|
+
actual: ColumnDefault,
|
|
18
|
+
nativeType?: string,
|
|
19
|
+
): boolean {
|
|
20
|
+
if (expected.kind !== actual.kind) return false;
|
|
21
|
+
if (expected.kind === 'literal' && actual.kind === 'literal') {
|
|
22
|
+
return literalValuesEqual(
|
|
23
|
+
normalizeLiteralValue(expected.value, nativeType),
|
|
24
|
+
normalizeLiteralValue(actual.value, nativeType),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (expected.kind === 'function' && actual.kind === 'function') {
|
|
28
|
+
return (
|
|
29
|
+
normalizeFunctionExpression(expected.expression) ===
|
|
30
|
+
normalizeFunctionExpression(actual.expression)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeFunctionExpression(expression: string): string {
|
|
37
|
+
return expression.toLowerCase().replace(/\s+/g, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isTemporalNativeType(nativeType?: string): boolean {
|
|
41
|
+
if (!nativeType) return false;
|
|
42
|
+
const normalized = nativeType.toLowerCase();
|
|
43
|
+
return normalized.includes('timestamp') || normalized === 'date';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeLiteralValue(value: unknown, nativeType?: string): unknown {
|
|
47
|
+
if (value instanceof Date) {
|
|
48
|
+
return value.toISOString();
|
|
49
|
+
}
|
|
50
|
+
if (typeof value === 'string' && isTemporalNativeType(nativeType)) {
|
|
51
|
+
const parsed = new Date(value);
|
|
52
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
53
|
+
return parsed.toISOString();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function literalValuesEqual(a: unknown, b: unknown): boolean {
|
|
60
|
+
if (a === b) return true;
|
|
61
|
+
if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
|
|
62
|
+
return canonicalStringify(a) === canonicalStringify(b);
|
|
63
|
+
}
|
|
64
|
+
if (typeof a === 'object' && a !== null && typeof b === 'string') {
|
|
65
|
+
try {
|
|
66
|
+
return canonicalStringify(a) === canonicalStringify(JSON.parse(b));
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (typeof a === 'string' && typeof b === 'object' && b !== null) {
|
|
72
|
+
try {
|
|
73
|
+
return canonicalStringify(JSON.parse(a)) === canonicalStringify(b);
|
|
74
|
+
} catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { DiffSubjectGranularity } from '@prisma-next/framework-components/control';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `nodeKind` discriminant for each relational schema-diff leaf node.
|
|
5
|
+
* Each node carries a unique value; the differ pairs siblings by `id`, and
|
|
6
|
+
* these kinds distinguish the node types that appear as `PostgresTableSchemaNode`
|
|
7
|
+
* children (columns, primary key, foreign keys, uniques, indexes, checks) from
|
|
8
|
+
* each other and from the RLS policy/role kinds a target defines separately.
|
|
9
|
+
*/
|
|
10
|
+
export const RelationalSchemaNodeKind = {
|
|
11
|
+
schema: 'sql-schema',
|
|
12
|
+
table: 'sql-table',
|
|
13
|
+
column: 'sql-column',
|
|
14
|
+
columnDefault: 'sql-column-default',
|
|
15
|
+
primaryKey: 'sql-primary-key',
|
|
16
|
+
foreignKey: 'sql-foreign-key',
|
|
17
|
+
unique: 'sql-unique',
|
|
18
|
+
index: 'sql-index',
|
|
19
|
+
check: 'sql-check-constraint',
|
|
20
|
+
} as const;
|
|
21
|
+
|
|
22
|
+
export type RelationalSchemaNodeKind =
|
|
23
|
+
(typeof RelationalSchemaNodeKind)[keyof typeof RelationalSchemaNodeKind];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The one real map from a relational `nodeKind` to the framework-neutral
|
|
27
|
+
* {@link DiffSubjectGranularity} its diff issues carry — the SQL family's
|
|
28
|
+
* post-diff filters (issue category, strict-mode gating) and the framework
|
|
29
|
+
* aggregate's unclaimed-elements sweep key on the granularity, never on the
|
|
30
|
+
* `nodeKind` spelling and never on anything stamped on the node. Resolution
|
|
31
|
+
* is by `nodeKind` equality against this map, not a suffix string match.
|
|
32
|
+
* Target-specific node kinds (Postgres namespace/table/policy/role) are
|
|
33
|
+
* outside this family layer's vocabulary and map their own kinds directly.
|
|
34
|
+
*/
|
|
35
|
+
const RELATIONAL_NODE_GRANULARITY: Readonly<
|
|
36
|
+
Record<RelationalSchemaNodeKind, DiffSubjectGranularity>
|
|
37
|
+
> = {
|
|
38
|
+
[RelationalSchemaNodeKind.schema]: 'structural',
|
|
39
|
+
[RelationalSchemaNodeKind.table]: 'entity',
|
|
40
|
+
[RelationalSchemaNodeKind.column]: 'field',
|
|
41
|
+
[RelationalSchemaNodeKind.columnDefault]: 'auxiliary',
|
|
42
|
+
[RelationalSchemaNodeKind.primaryKey]: 'auxiliary',
|
|
43
|
+
[RelationalSchemaNodeKind.foreignKey]: 'auxiliary',
|
|
44
|
+
[RelationalSchemaNodeKind.unique]: 'auxiliary',
|
|
45
|
+
[RelationalSchemaNodeKind.index]: 'auxiliary',
|
|
46
|
+
[RelationalSchemaNodeKind.check]: 'auxiliary',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function isRelationalSchemaNodeKind(nodeKind: string): nodeKind is RelationalSchemaNodeKind {
|
|
50
|
+
return Object.hasOwn(RELATIONAL_NODE_GRANULARITY, nodeKind);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Looks up the subject granularity for a relational `nodeKind`. Throws for a
|
|
55
|
+
* `nodeKind` outside this map (a target-specific kind, e.g. Postgres's
|
|
56
|
+
* namespace/table/policy/role) — those map their own kinds directly rather
|
|
57
|
+
* than through this family-level map.
|
|
58
|
+
*/
|
|
59
|
+
export function relationalNodeGranularity(nodeKind: string): DiffSubjectGranularity {
|
|
60
|
+
if (!isRelationalSchemaNodeKind(nodeKind)) {
|
|
61
|
+
throw new Error(`relationalNodeGranularity: unrecognized relational node kind "${nodeKind}"`);
|
|
62
|
+
}
|
|
63
|
+
return RELATIONAL_NODE_GRANULARITY[nodeKind];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The one real map from a relational `nodeKind` to its storage `entityKind`
|
|
68
|
+
* — the same vocabulary the contract storage's `entries` dictionary keys use
|
|
69
|
+
* (`elementCoordinates` walks it). Only the whole-table kind has an entity of
|
|
70
|
+
* its own; every other relational node (a column, an index, …) is nested
|
|
71
|
+
* under one and maps to nothing here.
|
|
72
|
+
*/
|
|
73
|
+
const RELATIONAL_NODE_ENTITY_KIND: Partial<Readonly<Record<RelationalSchemaNodeKind, string>>> = {
|
|
74
|
+
[RelationalSchemaNodeKind.table]: 'table',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Looks up the storage `entityKind` for a relational `nodeKind` — sibling of
|
|
79
|
+
* {@link relationalNodeGranularity}, resolved by `nodeKind` equality against
|
|
80
|
+
* {@link RELATIONAL_NODE_ENTITY_KIND}. `undefined` for a target-specific kind
|
|
81
|
+
* (targets map their own kinds directly) or a relational kind with no entity
|
|
82
|
+
* of its own.
|
|
83
|
+
*/
|
|
84
|
+
export function relationalNodeEntityKind(nodeKind: string): string | undefined {
|
|
85
|
+
return isRelationalSchemaNodeKind(nodeKind) ? RELATIONAL_NODE_ENTITY_KIND[nodeKind] : undefined;
|
|
86
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
1
2
|
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
2
|
-
import {
|
|
3
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
4
|
+
import { RelationalSchemaNodeKind } from './schema-node-kinds';
|
|
5
|
+
import { assertNode, SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
3
6
|
|
|
4
7
|
export interface SqlCheckConstraintIRInput {
|
|
5
8
|
/** Constraint name as stored in the database catalog. */
|
|
@@ -16,8 +19,15 @@ export interface SqlCheckConstraintIRInput {
|
|
|
16
19
|
*
|
|
17
20
|
* Carries the **resolved values** rather than a raw SQL predicate so
|
|
18
21
|
* callers can compare value-sets without parsing SQL.
|
|
22
|
+
*
|
|
23
|
+
* Implements `DiffableNode` so a check constraint is directly a table's
|
|
24
|
+
* diff-tree child: `id` is the constraint name. `isEqualTo` compares
|
|
25
|
+
* `column` and the permitted-value set (order-insensitive — the database
|
|
26
|
+
* does not guarantee `IN (...)` ordering).
|
|
19
27
|
*/
|
|
20
|
-
export class SqlCheckConstraintIR extends SqlSchemaIRNode {
|
|
28
|
+
export class SqlCheckConstraintIR extends SqlSchemaIRNode implements DiffableNode {
|
|
29
|
+
override readonly nodeKind = RelationalSchemaNodeKind.check;
|
|
30
|
+
|
|
21
31
|
readonly name: string;
|
|
22
32
|
readonly column: string;
|
|
23
33
|
readonly permittedValues: readonly string[];
|
|
@@ -29,4 +39,35 @@ export class SqlCheckConstraintIR extends SqlSchemaIRNode {
|
|
|
29
39
|
this.permittedValues = Object.freeze([...input.permittedValues]);
|
|
30
40
|
freezeNode(this);
|
|
31
41
|
}
|
|
42
|
+
|
|
43
|
+
get id(): string {
|
|
44
|
+
return `check:${this.name}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
children(): readonly DiffableNode[] {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static is(node: SqlSchemaIRNode): node is SqlCheckConstraintIR {
|
|
52
|
+
return node.nodeKind === RelationalSchemaNodeKind.check;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compares the permitted-value sets only (unordered), matching the
|
|
57
|
+
* relational walk's `verifyCheckConstraints`: two checks pairing by name
|
|
58
|
+
* compare their value sets — the `column` field is descriptive, not part
|
|
59
|
+
* of the comparison, so a name-paired check with equal values verifies
|
|
60
|
+
* regardless of which column carries it.
|
|
61
|
+
*/
|
|
62
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
63
|
+
const node = blindCast<
|
|
64
|
+
SqlSchemaIRNode,
|
|
65
|
+
'every diff-tree node the differ pairs is a SqlSchemaIRNode'
|
|
66
|
+
>(other);
|
|
67
|
+
assertNode(node, 'SqlCheckConstraintIR', SqlCheckConstraintIR.is);
|
|
68
|
+
const thisValues = new Set(this.permittedValues);
|
|
69
|
+
const otherValues = new Set(node.permittedValues);
|
|
70
|
+
if (thisValues.size !== otherValues.size) return false;
|
|
71
|
+
return [...thisValues].every((v) => otherValues.has(v));
|
|
72
|
+
}
|
|
32
73
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ColumnDefault } from '@prisma-next/contract/types';
|
|
2
|
+
import type { DiffableNode } from '@prisma-next/framework-components/control';
|
|
3
|
+
import { freezeNode } from '@prisma-next/framework-components/ir';
|
|
4
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
5
|
+
import { resolvedDefaultsEqual } from './resolved-default-equality';
|
|
6
|
+
import { RelationalSchemaNodeKind } from './schema-node-kinds';
|
|
7
|
+
import { assertNode, defineNonEnumerable, SqlSchemaIRNode } from './sql-schema-ir-node';
|
|
8
|
+
|
|
9
|
+
export interface SqlColumnDefaultIRInput {
|
|
10
|
+
/** Structured resolved default (contract-declared or normalizer-parsed). */
|
|
11
|
+
readonly resolved?: ColumnDefault;
|
|
12
|
+
/** Raw database default expression, when known (introspected side). */
|
|
13
|
+
readonly raw?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Native-type context for temporal literal normalization — the owning
|
|
16
|
+
* column's resolved native type.
|
|
17
|
+
*/
|
|
18
|
+
readonly nativeTypeContext?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The owning column's array-ness, threaded through so the planner's
|
|
21
|
+
* set-default op-builder can render an array-literal default (e.g.
|
|
22
|
+
* `ARRAY[...]`) the same way the pre-`plan(start, end)` op-path did. See
|
|
23
|
+
* {@link import('./sql-column-ir').SqlColumnIRInput.many}.
|
|
24
|
+
*/
|
|
25
|
+
readonly many?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Schema-diff node for a column's default value. The default is the one
|
|
30
|
+
* column attribute with an extra/missing/drift lifecycle of its own, so it
|
|
31
|
+
* is a child node of the column rather than a compared attribute: an
|
|
32
|
+
* undeclared live default surfaces as `not-expected`, a declared default the
|
|
33
|
+
* database lacks as `not-found`, and a divergent value as `not-equal` — the
|
|
34
|
+
* three reasons cover the legacy `extra_default` / `default_missing` /
|
|
35
|
+
* `default_mismatch` vocabulary with no attribute inspection.
|
|
36
|
+
*
|
|
37
|
+
* `id` is a fixed sentinel — a column has at most one default. Built
|
|
38
|
+
* transiently by `SqlColumnIR.children()` from the column's own fields;
|
|
39
|
+
* never constructed by derivation or introspection directly.
|
|
40
|
+
*/
|
|
41
|
+
export class SqlColumnDefaultIR extends SqlSchemaIRNode implements DiffableNode {
|
|
42
|
+
override readonly nodeKind = RelationalSchemaNodeKind.columnDefault;
|
|
43
|
+
|
|
44
|
+
declare readonly resolved?: ColumnDefault;
|
|
45
|
+
declare readonly raw?: string;
|
|
46
|
+
declare readonly nativeTypeContext?: string;
|
|
47
|
+
/** See {@link SqlColumnDefaultIRInput.many}. Non-enumerable so it stays out of JSON and structural equality. */
|
|
48
|
+
declare readonly many?: boolean;
|
|
49
|
+
|
|
50
|
+
constructor(input: SqlColumnDefaultIRInput) {
|
|
51
|
+
super();
|
|
52
|
+
if (input.resolved !== undefined) this.resolved = input.resolved;
|
|
53
|
+
if (input.raw !== undefined) this.raw = input.raw;
|
|
54
|
+
if (input.nativeTypeContext !== undefined) this.nativeTypeContext = input.nativeTypeContext;
|
|
55
|
+
defineNonEnumerable(this, 'many', input.many);
|
|
56
|
+
freezeNode(this);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get id(): string {
|
|
60
|
+
return 'default';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
children(): readonly DiffableNode[] {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static is(node: SqlSchemaIRNode): node is SqlColumnDefaultIR {
|
|
68
|
+
return node.nodeKind === RelationalSchemaNodeKind.columnDefault;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Structured comparison with `this` as the expected side: both sides
|
|
73
|
+
* resolved compare per the relational walk's `columnDefaultsEqual`
|
|
74
|
+
* semantics; a declared expected default against an unparseable actual
|
|
75
|
+
* (raw present, no resolved parse) is a mismatch; two raw-only nodes fall
|
|
76
|
+
* back to raw string equality.
|
|
77
|
+
*/
|
|
78
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
79
|
+
const node = blindCast<
|
|
80
|
+
SqlSchemaIRNode,
|
|
81
|
+
'every diff-tree node the differ pairs is a SqlSchemaIRNode'
|
|
82
|
+
>(other);
|
|
83
|
+
assertNode(node, 'SqlColumnDefaultIR', SqlColumnDefaultIR.is);
|
|
84
|
+
if (this.resolved !== undefined && node.resolved !== undefined) {
|
|
85
|
+
return resolvedDefaultsEqual(
|
|
86
|
+
this.resolved,
|
|
87
|
+
node.resolved,
|
|
88
|
+
node.nativeTypeContext ?? this.nativeTypeContext,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (this.resolved !== undefined || node.resolved !== undefined) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return this.raw === node.raw;
|
|
95
|
+
}
|
|
96
|
+
}
|
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,67 @@ 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
|
+
// `this.many` is unset on contract-derived columns (array-ness rides
|
|
149
|
+
// on the `nativeType` `[]` suffix there — `codecRef.many` carries
|
|
150
|
+
// it instead); introspected/hand-built columns set `this.many`
|
|
151
|
+
// directly. Either source works for the default node's array-
|
|
152
|
+
// literal rendering.
|
|
153
|
+
...ifDefined('many', this.many ?? this.codecRef?.many),
|
|
154
|
+
}),
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
static is(node: SqlSchemaIRNode): node is SqlColumnIR {
|
|
159
|
+
return node.nodeKind === RelationalSchemaNodeKind.column;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Compares the column's own attributes only — the default lives on the
|
|
164
|
+
* child node. When both sides carry `resolvedNativeType`, the resolved
|
|
165
|
+
* value governs (array-ness rides on its `[]` suffix); otherwise raw
|
|
166
|
+
* fields compare.
|
|
167
|
+
*/
|
|
168
|
+
isEqualTo(other: DiffableNode): boolean {
|
|
169
|
+
const node = blindCast<
|
|
170
|
+
SqlSchemaIRNode,
|
|
171
|
+
'every diff-tree node the differ pairs is a SqlSchemaIRNode'
|
|
172
|
+
>(other);
|
|
173
|
+
assertNode(node, 'SqlColumnIR', SqlColumnIR.is);
|
|
174
|
+
if (this.resolvedNativeType !== undefined && node.resolvedNativeType !== undefined) {
|
|
175
|
+
return this.resolvedNativeType === node.resolvedNativeType && this.nullable === node.nullable;
|
|
176
|
+
}
|
|
177
|
+
return (
|
|
178
|
+
this.nativeType === node.nativeType &&
|
|
179
|
+
this.nullable === node.nullable &&
|
|
180
|
+
Boolean(this.many) === Boolean(node.many)
|
|
181
|
+
);
|
|
182
|
+
}
|
|
44
183
|
}
|
|
@@ -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
|
}
|