@prisma-next/family-sql 0.14.0-dev.49 → 0.14.0-dev.50
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/{control-adapter-DHYFuOBy.d.mts → control-adapter-BVOj4gXI.d.mts} +34 -5
- package/dist/control-adapter-BVOj4gXI.d.mts.map +1 -0
- package/dist/control-adapter.d.mts +1 -1
- package/dist/control.d.mts +65 -50
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +308 -56
- package/dist/control.mjs.map +1 -1
- package/dist/diff.d.mts +141 -30
- package/dist/diff.d.mts.map +1 -1
- package/dist/diff.mjs +155 -1384
- package/dist/diff.mjs.map +1 -1
- package/dist/ir.d.mts +5 -5
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +2 -2
- package/dist/ir.mjs.map +1 -1
- package/dist/migration.d.mts +1 -1
- package/dist/schema-differ-DnoopSXm.d.mts +45 -0
- package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
- package/dist/schema-verify-W3r631Jh.mjs +226 -0
- package/dist/schema-verify-W3r631Jh.mjs.map +1 -0
- package/dist/{sql-contract-serializer-DwUSJ4PO.mjs → sql-contract-serializer-C75cfMSS.mjs} +2 -2
- package/dist/{sql-contract-serializer-DwUSJ4PO.mjs.map → sql-contract-serializer-C75cfMSS.mjs.map} +1 -1
- package/dist/{types-DBxrwN1d.d.mts → types-BW7pzb2j.d.mts} +24 -15
- package/dist/types-BW7pzb2j.d.mts.map +1 -0
- package/package.json +21 -21
- package/src/core/control-instance.ts +99 -80
- package/src/core/control-target-descriptor.ts +29 -14
- package/src/core/diff/diff-tree-normalization.ts +211 -0
- package/src/core/diff/schema-verify.ts +324 -0
- package/src/core/diff/sql-schema-diff.ts +40 -1466
- package/src/core/diff/verifier-disposition.ts +14 -42
- package/src/core/ir/sql-schema-verifier-base.ts +5 -5
- package/src/core/migrations/contract-to-schema-ir.ts +63 -20
- package/src/core/migrations/native-type-expander.ts +28 -0
- package/src/core/migrations/schema-differ.ts +30 -29
- package/src/core/migrations/types.ts +23 -14
- package/src/exports/control.ts +6 -0
- package/src/exports/diff.ts +28 -8
- package/dist/contract-to-schema-ir-S-evq8E6.mjs +0 -264
- package/dist/contract-to-schema-ir-S-evq8E6.mjs.map +0 -1
- package/dist/control-adapter-DHYFuOBy.d.mts.map +0 -1
- package/dist/sql-schema-diff-6z36dZt6.d.mts +0 -105
- package/dist/sql-schema-diff-6z36dZt6.d.mts.map +0 -1
- package/dist/types-DBxrwN1d.d.mts.map +0 -1
- package/src/core/diff/control-verify-emit.ts +0 -46
- package/src/core/diff/verify-helpers.ts +0 -832
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Contract,
|
|
3
3
|
ContractMarkerRecord,
|
|
4
|
-
ControlPolicy,
|
|
5
4
|
LedgerEntryRecord,
|
|
6
5
|
} from '@prisma-next/contract/types';
|
|
7
|
-
import { effectiveControlPolicy } from '@prisma-next/contract/types';
|
|
8
6
|
import type {
|
|
9
7
|
TargetBoundComponentDescriptor,
|
|
10
8
|
TargetDescriptor,
|
|
@@ -13,6 +11,7 @@ import type {
|
|
|
13
11
|
ControlFamilyInstance,
|
|
14
12
|
ControlStack,
|
|
15
13
|
CoreSchemaView,
|
|
14
|
+
DiffSubjectGranularity,
|
|
16
15
|
MigrationPlanOperation,
|
|
17
16
|
OperationPreview,
|
|
18
17
|
OperationPreviewCapable,
|
|
@@ -25,7 +24,6 @@ import type {
|
|
|
25
24
|
} from '@prisma-next/framework-components/control';
|
|
26
25
|
import {
|
|
27
26
|
APP_SPACE_ID,
|
|
28
|
-
dispositionForCategory,
|
|
29
27
|
SchemaTreeNode,
|
|
30
28
|
VERIFY_CODE_HASH_MISMATCH,
|
|
31
29
|
VERIFY_CODE_MARKER_MISSING,
|
|
@@ -52,8 +50,13 @@ import type {
|
|
|
52
50
|
SqlControlTargetDescriptor,
|
|
53
51
|
SqlDescribedContractSpace,
|
|
54
52
|
} from './control-target-descriptor';
|
|
53
|
+
import {
|
|
54
|
+
classifyDiffEntityKind,
|
|
55
|
+
classifyDiffSubjectGranularity,
|
|
56
|
+
verifySqlSchemaByDiff,
|
|
57
|
+
} from './diff/schema-verify';
|
|
55
58
|
import { SqlContractSerializer } from './ir/sql-contract-serializer';
|
|
56
|
-
import type {
|
|
59
|
+
import type { SqlSchemaDiffFn } from './migrations/schema-differ';
|
|
57
60
|
import type {
|
|
58
61
|
SqlControlAdapterDescriptor,
|
|
59
62
|
SqlControlExtensionDescriptor,
|
|
@@ -234,6 +237,26 @@ export interface SqlControlFamilyInstance
|
|
|
234
237
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
235
238
|
}): VerifyDatabaseSchemaResult;
|
|
236
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Classifies a diff issue's subject granularity on demand, resolved from
|
|
242
|
+
* its node's `nodeKind` via the target's classifier. Satisfies the
|
|
243
|
+
* {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}
|
|
244
|
+
* capability that framework consumers spanning contract spaces (the
|
|
245
|
+
* migration aggregate's unclaimed-elements sweep) detect via
|
|
246
|
+
* `hasSchemaSubjectClassifier` and call instead of reading family/target
|
|
247
|
+
* node vocabulary. Nothing is stamped on the issue or the node.
|
|
248
|
+
*/
|
|
249
|
+
classifySubjectGranularity(issue: SchemaDiffIssue): DiffSubjectGranularity | undefined;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Classifies a diff issue's subject storage `entityKind` on demand,
|
|
253
|
+
* resolved from its node's `nodeKind` via the target's classifier —
|
|
254
|
+
* sibling of `classifySubjectGranularity` above, part of the same
|
|
255
|
+
* {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}
|
|
256
|
+
* capability. Nothing is stamped on the issue or the node.
|
|
257
|
+
*/
|
|
258
|
+
classifyEntityKind(issue: SchemaDiffIssue): string | undefined;
|
|
259
|
+
|
|
237
260
|
sign(options: {
|
|
238
261
|
readonly driver: SqlControlDriverInstance<string>;
|
|
239
262
|
readonly contract: unknown;
|
|
@@ -542,6 +565,29 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
542
565
|
SqlControlTargetDescriptor<TTargetId, unknown>,
|
|
543
566
|
'reading the optional target-descriptor inferPslContract hook'
|
|
544
567
|
>(target).inferPslContract;
|
|
568
|
+
// The full-tree node diff the verify VERDICT derives from. Read lazily so
|
|
569
|
+
// construction-only stub descriptors (schema-view tests) keep working; the
|
|
570
|
+
// throw happens at verify time.
|
|
571
|
+
const diffSchema = blindCast<
|
|
572
|
+
{ readonly diffSchema?: SqlSchemaDiffFn },
|
|
573
|
+
'reading the target-descriptor diffSchema hook'
|
|
574
|
+
>(target).diffSchema;
|
|
575
|
+
// The target's nodeKind → granularity classifier, resolved on demand by the
|
|
576
|
+
// verdict and by the instance's `classifySubjectGranularity` method below —
|
|
577
|
+
// never stamped onto an issue. Read lazily for the same
|
|
578
|
+
// construction-only-stub reason as `diffSchema`; the throw happens at call
|
|
579
|
+
// time.
|
|
580
|
+
const targetGranularityOf = blindCast<
|
|
581
|
+
{ readonly classifySubjectGranularity?: (nodeKind: string) => DiffSubjectGranularity },
|
|
582
|
+
'reading the target-descriptor classifySubjectGranularity hook'
|
|
583
|
+
>(target).classifySubjectGranularity;
|
|
584
|
+
// The target's nodeKind → storage entityKind classifier — sibling of
|
|
585
|
+
// `targetGranularityOf` above, read the same lazy way for the same
|
|
586
|
+
// construction-only-stub reason.
|
|
587
|
+
const targetEntityKindOf = blindCast<
|
|
588
|
+
{ readonly classifyEntityKind?: (nodeKind: string) => string | undefined },
|
|
589
|
+
'reading the target-descriptor classifyEntityKind hook'
|
|
590
|
+
>(target).classifyEntityKind;
|
|
545
591
|
// `contract infer` needs each extension pack's already-assembled contract,
|
|
546
592
|
// carried as-is (no merging — that is the contract-spaces machinery's
|
|
547
593
|
// concern), paired with the `spaceId` its descriptor was registered under
|
|
@@ -554,24 +600,6 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
554
600
|
? [{ spaceId: extension.id, contract: extension.contractSpace.contractJson }]
|
|
555
601
|
: [],
|
|
556
602
|
);
|
|
557
|
-
// The combined database-schema verify is a required target-descriptor
|
|
558
|
-
// operation: every SQL target provides it, wrapping the same comparison
|
|
559
|
-
// `diffDatabaseSchema` runs in the verify envelope plus the pass/warn/fail
|
|
560
|
-
// tree the CLI renders. Read it off the descriptor like the other
|
|
561
|
-
// target-owned hooks.
|
|
562
|
-
const verifyDatabaseSchema = blindCast<
|
|
563
|
-
{
|
|
564
|
-
readonly verifyDatabaseSchema?: (
|
|
565
|
-
input: DiffDatabaseSchemaInput,
|
|
566
|
-
) => VerifyDatabaseSchemaResult;
|
|
567
|
-
},
|
|
568
|
-
'reading the required target-descriptor verifyDatabaseSchema hook'
|
|
569
|
-
>(target).verifyDatabaseSchema;
|
|
570
|
-
if (!verifyDatabaseSchema) {
|
|
571
|
-
throw new Error(
|
|
572
|
-
`SQL target "${target.targetId}" is missing the required verifyDatabaseSchema descriptor operation`,
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
603
|
const deserializeWithTargetSerializer = (contractOrJson: unknown): Contract<SqlStorage> => {
|
|
576
604
|
const serializer = targetSerializer ?? new SqlContractSerializer();
|
|
577
605
|
const json =
|
|
@@ -729,42 +757,61 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
729
757
|
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
730
758
|
}): VerifyDatabaseSchemaResult {
|
|
731
759
|
const contract = deserializeWithTargetSerializer(options.contract) as Contract<SqlStorage>;
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
760
|
+
if (!diffSchema) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
`SQL target "${target.targetId}" is missing the required diffSchema descriptor operation`,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
if (!targetGranularityOf) {
|
|
766
|
+
throw new Error(
|
|
767
|
+
`SQL target "${target.targetId}" is missing the required classifySubjectGranularity descriptor operation`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
// THE VERDICT: the target's full-tree node diff, graded by the
|
|
771
|
+
// family's post-diff filters (strict gating + control-policy
|
|
772
|
+
// disposition), plus the codec verifyType hook findings. The result
|
|
773
|
+
// is issue-based — `ok` holds exactly when both issue lists are empty.
|
|
774
|
+
return verifySqlSchemaByDiff({
|
|
738
775
|
contract,
|
|
739
776
|
schema: options.schema,
|
|
740
777
|
strict: options.strict,
|
|
741
|
-
typeMetadataRegistry,
|
|
742
778
|
frameworkComponents: options.frameworkComponents,
|
|
779
|
+
diffSchema,
|
|
780
|
+
granularityOf: targetGranularityOf,
|
|
743
781
|
});
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
782
|
+
},
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Classifies a diff issue's subject granularity on demand, by resolving
|
|
786
|
+
* its node's `nodeKind` through the target's classifier — the
|
|
787
|
+
* {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}
|
|
788
|
+
* capability. Framework consumers spanning contract spaces (the
|
|
789
|
+
* migration aggregate's unclaimed-elements sweep) detect and call this
|
|
790
|
+
* instead of reaching into the concrete schema-IR node, which they
|
|
791
|
+
* cannot read; nothing is stamped on the issue or the node.
|
|
792
|
+
*/
|
|
793
|
+
classifySubjectGranularity(issue: SchemaDiffIssue): DiffSubjectGranularity | undefined {
|
|
794
|
+
if (!targetGranularityOf) {
|
|
795
|
+
throw new Error(
|
|
796
|
+
`SQL target "${target.targetId}" is missing the required classifySubjectGranularity descriptor operation`,
|
|
797
|
+
);
|
|
755
798
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
799
|
+
return classifyDiffSubjectGranularity(issue, targetGranularityOf);
|
|
800
|
+
},
|
|
801
|
+
/**
|
|
802
|
+
* Classifies a diff issue's subject storage `entityKind` on demand, by
|
|
803
|
+
* resolving its node's `nodeKind` through the target's classifier —
|
|
804
|
+
* the sibling of `classifySubjectGranularity` above, and part of the
|
|
805
|
+
* same {@link import('@prisma-next/framework-components/control').SchemaSubjectClassifierCapable}
|
|
806
|
+
* capability.
|
|
807
|
+
*/
|
|
808
|
+
classifyEntityKind(issue: SchemaDiffIssue): string | undefined {
|
|
809
|
+
if (!targetEntityKindOf) {
|
|
810
|
+
throw new Error(
|
|
811
|
+
`SQL target "${target.targetId}" is missing the required classifyEntityKind descriptor operation`,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
return classifyDiffEntityKind(issue, targetEntityKindOf);
|
|
768
815
|
},
|
|
769
816
|
async sign(options: {
|
|
770
817
|
readonly driver: SqlControlDriverInstance<string>;
|
|
@@ -1115,31 +1162,3 @@ export function createSqlFamilyInstance<TTargetId extends string>(
|
|
|
1115
1162
|
},
|
|
1116
1163
|
};
|
|
1117
1164
|
}
|
|
1118
|
-
|
|
1119
|
-
/**
|
|
1120
|
-
* Filters the structural schema-diff issues (from `diffDatabaseSchema`) through
|
|
1121
|
-
* the contract's `defaultControlPolicy`. Issues whose outcome maps to a suppressed
|
|
1122
|
-
* category under the effective policy are removed. This mirrors the control-policy
|
|
1123
|
-
* filtering applied by `verifySqlSchema` for table/column-level findings.
|
|
1124
|
-
*
|
|
1125
|
-
* Outcome → category mapping:
|
|
1126
|
-
* - `'extra'` → `'extraAuxiliary'` (an extra auxiliary entity, e.g. an RLS policy)
|
|
1127
|
-
* - `'missing'` → `'declaredMissing'`
|
|
1128
|
-
* - `'mismatch'` → `'declaredIncompatible'`
|
|
1129
|
-
*/
|
|
1130
|
-
export function filterSchemaDiffIssues(
|
|
1131
|
-
issues: readonly SchemaDiffIssue[],
|
|
1132
|
-
defaultControlPolicy: ControlPolicy | undefined,
|
|
1133
|
-
): readonly SchemaDiffIssue[] {
|
|
1134
|
-
if (issues.length === 0) return issues;
|
|
1135
|
-
const policy = effectiveControlPolicy(undefined, defaultControlPolicy);
|
|
1136
|
-
return issues.filter((issue) => {
|
|
1137
|
-
const category =
|
|
1138
|
-
issue.outcome === 'extra'
|
|
1139
|
-
? ('extraAuxiliary' as const)
|
|
1140
|
-
: issue.outcome === 'missing'
|
|
1141
|
-
? ('declaredMissing' as const)
|
|
1142
|
-
: ('declaredIncompatible' as const);
|
|
1143
|
-
return dispositionForCategory(policy, category) !== 'suppress';
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
2
|
import type {
|
|
3
3
|
ContractSerializer,
|
|
4
|
+
DiffSubjectGranularity,
|
|
4
5
|
MigratableTargetDescriptor,
|
|
5
6
|
SchemaVerifier,
|
|
6
7
|
} from '@prisma-next/framework-components/control';
|
|
@@ -10,7 +11,7 @@ import type { SqlOperationDescriptors } from '@prisma-next/sql-operations';
|
|
|
10
11
|
import type { SqlSchemaIR, SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
|
|
11
12
|
import type { SqlControlAdapter } from './control-adapter';
|
|
12
13
|
import type { SqlControlFamilyInstance } from './control-instance';
|
|
13
|
-
import type {
|
|
14
|
+
import type { SqlSchemaDiffFn } from './migrations/schema-differ';
|
|
14
15
|
import type { SqlMigrationPlanner, SqlMigrationRunner } from './migrations/types';
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -62,22 +63,36 @@ export interface SqlControlTargetDescriptor<
|
|
|
62
63
|
describedContracts?: readonly SqlDescribedContractSpace[],
|
|
63
64
|
) => PslDocumentAst;
|
|
64
65
|
/**
|
|
65
|
-
* The
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* See {@link SqlDiffDatabaseSchema} / {@link SqlVerifyDatabaseSchema}.
|
|
66
|
+
* The full-tree node diff the family verify verdict derives from —
|
|
67
|
+
* expected-tree derivation, pre-diff normalization, the generic differ,
|
|
68
|
+
* and ownership scoping, all target-side. The family applies strict
|
|
69
|
+
* gating + control-policy disposition over the returned issues; verify
|
|
70
|
+
* rejects when a surviving issue is a failure.
|
|
71
71
|
*/
|
|
72
|
-
readonly
|
|
72
|
+
readonly diffSchema: SqlSchemaDiffFn;
|
|
73
73
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
74
|
+
* Classifies a diff-tree node's `nodeKind` into its framework-neutral
|
|
75
|
+
* {@link DiffSubjectGranularity} — the target owns the full node vocabulary
|
|
76
|
+
* that appears in its diff tree (its own kinds plus the relational kinds it
|
|
77
|
+
* delegates to), so it is the one place that can resolve this. The family
|
|
78
|
+
* verdict calls it inline per issue (never stamping the result); the
|
|
79
|
+
* framework aggregate's unclaimed-elements sweep reaches the same
|
|
80
|
+
* classifier via the family instance's `classifySubjectGranularity`
|
|
81
|
+
* capability.
|
|
79
82
|
*/
|
|
80
|
-
readonly
|
|
83
|
+
readonly classifySubjectGranularity: (nodeKind: string) => DiffSubjectGranularity;
|
|
84
|
+
/**
|
|
85
|
+
* Classifies a diff-tree node's `nodeKind` into its storage `entityKind` —
|
|
86
|
+
* the same vocabulary the contract storage's `entries` dictionary keys use
|
|
87
|
+
* (e.g. `'table'`). Sibling of `classifySubjectGranularity`, resolved the
|
|
88
|
+
* same way: the target owns the full node vocabulary, so it is the one
|
|
89
|
+
* place that can resolve this. `undefined` for a node kind with no
|
|
90
|
+
* storage entity of its own (a column, an index, …). The framework
|
|
91
|
+
* aggregate's unclaimed-elements sweep reaches this via the family
|
|
92
|
+
* instance's `classifyEntityKind` capability, so it never hardcodes a
|
|
93
|
+
* family entity kind.
|
|
94
|
+
*/
|
|
95
|
+
readonly classifyEntityKind: (nodeKind: string) => string | undefined;
|
|
81
96
|
createPlanner(adapter: SqlControlAdapter<TTargetId>): SqlMigrationPlanner<TTargetDetails>;
|
|
82
97
|
createRunner(family: SqlControlFamilyInstance): SqlMigrationRunner<TTargetDetails>;
|
|
83
98
|
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ⚠️ ACCEPTED-UNDER-PROTEST DEBT — DELETE IN SLICE 2.6 (`unify-unique-and-index-nodes`).
|
|
3
|
+
*
|
|
4
|
+
* A unique constraint IS a unique index. This slice modeled uniques and indexes
|
|
5
|
+
* as two separate node kinds (`SqlUniqueIR` vs `SqlIndexIR` — even though
|
|
6
|
+
* `SqlIndexIR` already carries `unique: boolean`), so the strict-by-kind differ
|
|
7
|
+
* cannot pair a contract unique with a live unique index. This whole file
|
|
8
|
+
* exists only to reconcile those two representations after the fact — it
|
|
9
|
+
* should not exist.
|
|
10
|
+
*
|
|
11
|
+
* The fix — delete `SqlUniqueIR`, model unique enforcement as
|
|
12
|
+
* `SqlIndexIR { unique: true }` on both derivation and introspection, and
|
|
13
|
+
* delete `diff-tree-normalization.ts` — is specified in
|
|
14
|
+
* `projects/postgres-rls/slices/unify-unique-and-index-nodes/spec.md` and
|
|
15
|
+
* scheduled as the very next slice. Do NOT extend this.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Pre-diff tree normalization: adjustments to the actual/expected trees the
|
|
20
|
+
* generic differ (`diffSchemas`) runs over, so legacy semantic-equivalence
|
|
21
|
+
* rules (a unique constraint satisfied by a unique index, an FK schema
|
|
22
|
+
* segment that differs only in spelling) surface as same-kind node pairs
|
|
23
|
+
* instead of spurious drift.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
SqlForeignKeyIR,
|
|
28
|
+
SqlIndexIR,
|
|
29
|
+
SqlSchemaIR,
|
|
30
|
+
SqlTableIR,
|
|
31
|
+
SqlUniqueIR,
|
|
32
|
+
} from '@prisma-next/sql-schema-ir/types';
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Semantic satisfaction — derivation-side normalization of the actual tree
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
export interface SemanticSatisfactionInput {
|
|
39
|
+
readonly expectedUniques: readonly SqlUniqueIR[];
|
|
40
|
+
readonly expectedIndexes: readonly SqlIndexIR[];
|
|
41
|
+
readonly actualUniques: readonly SqlUniqueIR[];
|
|
42
|
+
readonly actualIndexes: readonly SqlIndexIR[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface SemanticSatisfactionResult {
|
|
46
|
+
readonly actualUniques: readonly SqlUniqueIR[];
|
|
47
|
+
readonly actualIndexes: readonly SqlIndexIR[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sameColumns(a: readonly string[], b: readonly string[]): boolean {
|
|
51
|
+
return a.length === b.length && a.every((c, i) => c === b[i]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Adjusts a table pair's ACTUAL unique/index child lists so the legacy
|
|
56
|
+
* walk's cross-kind semantic satisfaction materializes as same-kind node
|
|
57
|
+
* pairs for the differ (the differ pairs strictly by id, so a `unique:`
|
|
58
|
+
* node can never pair with an `index:` node). Three legacy rules, ported
|
|
59
|
+
* from `isUniqueConstraintSatisfied` / `isIndexSatisfied` and the
|
|
60
|
+
* strict-extras loops of the retired relational walk:
|
|
61
|
+
*
|
|
62
|
+
* 1. A contract unique satisfied by a live unique INDEX: the actual index
|
|
63
|
+
* node is reclassified as a unique node (it pairs with the expected
|
|
64
|
+
* unique and stops being a candidate extra).
|
|
65
|
+
* 2. A contract index (with no type/options demands) satisfied by a live
|
|
66
|
+
* unique CONSTRAINT: a same-kind actual index node is synthesized so
|
|
67
|
+
* the expected index pairs — the unique constraint itself stays (the
|
|
68
|
+
* legacy strict-extras loop still reports it as an undeclared unique).
|
|
69
|
+
* 3. Live unique indexes are never extras in the legacy walk (its
|
|
70
|
+
* strict-extras loop skips `unique: true` rows), so any remaining
|
|
71
|
+
* actual unique-index node with no expected index counterpart is
|
|
72
|
+
* dropped rather than surfacing as `not-expected`.
|
|
73
|
+
*/
|
|
74
|
+
export function resolveSemanticSatisfaction(
|
|
75
|
+
input: SemanticSatisfactionInput,
|
|
76
|
+
): SemanticSatisfactionResult {
|
|
77
|
+
let actualIndexes = [...input.actualIndexes];
|
|
78
|
+
const actualUniques = [...input.actualUniques];
|
|
79
|
+
|
|
80
|
+
// Rule 1: reclassify a satisfying unique index as the unique constraint
|
|
81
|
+
// the contract declared.
|
|
82
|
+
for (const expectedUnique of input.expectedUniques) {
|
|
83
|
+
const alreadyPaired = actualUniques.some((u) => sameColumns(u.columns, expectedUnique.columns));
|
|
84
|
+
if (alreadyPaired) continue;
|
|
85
|
+
const satisfyingIndex = actualIndexes.find(
|
|
86
|
+
(idx) => idx.unique && sameColumns(idx.columns, expectedUnique.columns),
|
|
87
|
+
);
|
|
88
|
+
if (satisfyingIndex) {
|
|
89
|
+
actualIndexes = actualIndexes.filter((idx) => idx !== satisfyingIndex);
|
|
90
|
+
actualUniques.push(
|
|
91
|
+
new SqlUniqueIR({
|
|
92
|
+
columns: satisfyingIndex.columns,
|
|
93
|
+
...(satisfyingIndex.name !== undefined ? { name: satisfyingIndex.name } : {}),
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Rule 2: synthesize an index node from a satisfying unique constraint.
|
|
100
|
+
for (const expectedIndex of input.expectedIndexes) {
|
|
101
|
+
if (expectedIndex.type !== undefined || expectedIndex.options !== undefined) continue;
|
|
102
|
+
const alreadyPaired = actualIndexes.some((idx) =>
|
|
103
|
+
sameColumns(idx.columns, expectedIndex.columns),
|
|
104
|
+
);
|
|
105
|
+
if (alreadyPaired) continue;
|
|
106
|
+
const satisfyingUnique = actualUniques.find((u) =>
|
|
107
|
+
sameColumns(u.columns, expectedIndex.columns),
|
|
108
|
+
);
|
|
109
|
+
if (satisfyingUnique) {
|
|
110
|
+
actualIndexes.push(new SqlIndexIR({ columns: satisfyingUnique.columns, unique: false }));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Rule 3: remaining unique indexes with no expected counterpart are
|
|
115
|
+
// invisible to the legacy extras loop — drop them.
|
|
116
|
+
actualIndexes = actualIndexes.filter(
|
|
117
|
+
(idx) =>
|
|
118
|
+
!idx.unique || input.expectedIndexes.some((exp) => sameColumns(exp.columns, idx.columns)),
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return { actualUniques, actualIndexes };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Flat-tree helpers (single-schema targets)
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Applies {@link resolveSemanticSatisfaction} across a flat table pair set:
|
|
130
|
+
* every actual table with an expected counterpart gets its unique/index
|
|
131
|
+
* child lists adjusted; unpaired tables pass through untouched.
|
|
132
|
+
*/
|
|
133
|
+
export function normalizeFlatActualForDiff(
|
|
134
|
+
expected: SqlSchemaIR,
|
|
135
|
+
actual: SqlSchemaIR,
|
|
136
|
+
): SqlSchemaIR {
|
|
137
|
+
const tables: Record<string, SqlTableIR> = {};
|
|
138
|
+
for (const [name, actualTable] of Object.entries(actual.tables)) {
|
|
139
|
+
const expectedTable = expected.tables[name];
|
|
140
|
+
if (expectedTable === undefined) {
|
|
141
|
+
tables[name] = actualTable;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const adjusted = resolveSemanticSatisfaction({
|
|
145
|
+
expectedUniques: expectedTable.uniques,
|
|
146
|
+
expectedIndexes: expectedTable.indexes,
|
|
147
|
+
actualUniques: actualTable.uniques,
|
|
148
|
+
actualIndexes: actualTable.indexes,
|
|
149
|
+
});
|
|
150
|
+
tables[name] = new SqlTableIR({
|
|
151
|
+
name: actualTable.name,
|
|
152
|
+
columns: actualTable.columns,
|
|
153
|
+
foreignKeys: actualTable.foreignKeys,
|
|
154
|
+
uniques: adjusted.actualUniques,
|
|
155
|
+
indexes: adjusted.actualIndexes,
|
|
156
|
+
...(actualTable.primaryKey !== undefined ? { primaryKey: actualTable.primaryKey } : {}),
|
|
157
|
+
...(actualTable.annotations !== undefined ? { annotations: actualTable.annotations } : {}),
|
|
158
|
+
...(actualTable.checks !== undefined ? { checks: actualTable.checks } : {}),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return new SqlSchemaIR({
|
|
162
|
+
tables,
|
|
163
|
+
...(actual.annotations !== undefined ? { annotations: actual.annotations } : {}),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Neutralizes the FK schema segment on a flat expected tree so its FK diff
|
|
169
|
+
* nodes pair with introspected FKs on single-schema targets: the family
|
|
170
|
+
* converter stamps `referencedSchema` with the contract namespace id
|
|
171
|
+
* verbatim (the unbound sentinel on non-namespaced targets), while a
|
|
172
|
+
* single-schema introspection stamps none — resolving both sides to the
|
|
173
|
+
* empty segment makes the ids meet.
|
|
174
|
+
*/
|
|
175
|
+
export function neutralizeFlatExpectedFkSchemas(expected: SqlSchemaIR): SqlSchemaIR {
|
|
176
|
+
const tables: Record<string, SqlTableIR> = {};
|
|
177
|
+
for (const [name, table] of Object.entries(expected.tables)) {
|
|
178
|
+
if (table.foreignKeys.length === 0) {
|
|
179
|
+
tables[name] = table;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const foreignKeys = table.foreignKeys.map(
|
|
183
|
+
(fk) =>
|
|
184
|
+
new SqlForeignKeyIR({
|
|
185
|
+
columns: fk.columns,
|
|
186
|
+
referencedTable: fk.referencedTable,
|
|
187
|
+
referencedColumns: fk.referencedColumns,
|
|
188
|
+
...(fk.referencedSchema !== undefined ? { referencedSchema: fk.referencedSchema } : {}),
|
|
189
|
+
...(fk.name !== undefined ? { name: fk.name } : {}),
|
|
190
|
+
...(fk.onDelete !== undefined ? { onDelete: fk.onDelete } : {}),
|
|
191
|
+
...(fk.onUpdate !== undefined ? { onUpdate: fk.onUpdate } : {}),
|
|
192
|
+
...(fk.annotations !== undefined ? { annotations: fk.annotations } : {}),
|
|
193
|
+
resolvedReferencedNamespace: '',
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
tables[name] = new SqlTableIR({
|
|
197
|
+
name: table.name,
|
|
198
|
+
columns: table.columns,
|
|
199
|
+
foreignKeys,
|
|
200
|
+
uniques: table.uniques,
|
|
201
|
+
indexes: table.indexes,
|
|
202
|
+
...(table.primaryKey !== undefined ? { primaryKey: table.primaryKey } : {}),
|
|
203
|
+
...(table.annotations !== undefined ? { annotations: table.annotations } : {}),
|
|
204
|
+
...(table.checks !== undefined ? { checks: table.checks } : {}),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
return new SqlSchemaIR({
|
|
208
|
+
tables,
|
|
209
|
+
...(expected.annotations !== undefined ? { annotations: expected.annotations } : {}),
|
|
210
|
+
});
|
|
211
|
+
}
|