@prisma-next/family-sql 0.3.0-dev.16 → 0.3.0-dev.162
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/LICENSE +201 -0
- package/README.md +58 -22
- package/dist/authoring-type-constructors-DgU-RFaP.mjs +203 -0
- package/dist/authoring-type-constructors-DgU-RFaP.mjs.map +1 -0
- package/dist/control-adapter.d.mts +54 -0
- package/dist/control-adapter.d.mts.map +1 -0
- package/dist/control-adapter.mjs +1 -0
- package/dist/control.d.mts +386 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +643 -0
- package/dist/control.mjs.map +1 -0
- package/dist/operation-descriptors.d.mts +380 -0
- package/dist/operation-descriptors.d.mts.map +1 -0
- package/dist/operation-descriptors.mjs +294 -0
- package/dist/operation-descriptors.mjs.map +1 -0
- package/dist/pack.d.mts +248 -0
- package/dist/pack.d.mts.map +1 -0
- package/dist/pack.mjs +18 -0
- package/dist/pack.mjs.map +1 -0
- package/dist/runtime.d.mts +27 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +38 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/schema-verify.d.mts +48 -0
- package/dist/schema-verify.d.mts.map +1 -0
- package/dist/schema-verify.mjs +3 -0
- package/dist/test-utils.d.mts +2 -0
- package/dist/test-utils.mjs +3 -0
- package/dist/types-BaUzKt6Q.d.mts +353 -0
- package/dist/types-BaUzKt6Q.d.mts.map +1 -0
- package/dist/verify-DZHtfcmj.mjs +108 -0
- package/dist/verify-DZHtfcmj.mjs.map +1 -0
- package/dist/verify-sql-schema-BBhkqEDo.d.mts +67 -0
- package/dist/verify-sql-schema-BBhkqEDo.d.mts.map +1 -0
- package/dist/verify-sql-schema-lR-tlboL.mjs +1174 -0
- package/dist/verify-sql-schema-lR-tlboL.mjs.map +1 -0
- package/dist/verify.d.mts +31 -0
- package/dist/verify.d.mts.map +1 -0
- package/dist/verify.mjs +3 -0
- package/package.json +38 -46
- package/src/core/assembly.ts +132 -92
- package/src/core/authoring-field-presets.ts +207 -0
- package/src/core/authoring-type-constructors.ts +17 -0
- package/src/core/control-adapter.ts +18 -10
- package/src/core/control-descriptor.ts +28 -0
- package/src/core/control-instance.ts +700 -0
- package/src/core/migrations/contract-to-schema-ir.ts +269 -0
- package/src/core/migrations/descriptor-schemas.ts +172 -0
- package/src/core/migrations/operation-descriptors.ts +213 -0
- package/src/core/migrations/policies.ts +1 -1
- package/src/core/migrations/types.ts +199 -175
- package/src/core/runtime-descriptor.ts +19 -41
- package/src/core/runtime-instance.ts +11 -133
- package/src/core/schema-verify/verify-helpers.ts +206 -106
- package/src/core/schema-verify/verify-sql-schema.ts +964 -414
- package/src/core/verify.ts +4 -13
- package/src/exports/control.ts +31 -8
- package/src/exports/operation-descriptors.ts +52 -0
- package/src/exports/pack.ts +16 -0
- package/src/exports/runtime.ts +2 -6
- package/src/exports/schema-verify.ts +10 -2
- package/src/exports/test-utils.ts +3 -4
- package/dist/chunk-BHEGVBY7.js +0 -772
- package/dist/chunk-BHEGVBY7.js.map +0 -1
- package/dist/chunk-SQ2VWYDV.js +0 -589
- package/dist/chunk-SQ2VWYDV.js.map +0 -1
- package/dist/chunk-SU7LN2UH.js +0 -96
- package/dist/chunk-SU7LN2UH.js.map +0 -1
- package/dist/core/assembly.d.ts +0 -25
- package/dist/core/assembly.d.ts.map +0 -1
- package/dist/core/control-adapter.d.ts +0 -42
- package/dist/core/control-adapter.d.ts.map +0 -1
- package/dist/core/descriptor.d.ts +0 -24
- package/dist/core/descriptor.d.ts.map +0 -1
- package/dist/core/instance.d.ts +0 -140
- package/dist/core/instance.d.ts.map +0 -1
- package/dist/core/migrations/plan-helpers.d.ts +0 -20
- package/dist/core/migrations/plan-helpers.d.ts.map +0 -1
- package/dist/core/migrations/policies.d.ts +0 -6
- package/dist/core/migrations/policies.d.ts.map +0 -1
- package/dist/core/migrations/types.d.ts +0 -280
- package/dist/core/migrations/types.d.ts.map +0 -1
- package/dist/core/runtime-descriptor.d.ts +0 -19
- package/dist/core/runtime-descriptor.d.ts.map +0 -1
- package/dist/core/runtime-instance.d.ts +0 -54
- package/dist/core/runtime-instance.d.ts.map +0 -1
- package/dist/core/schema-verify/verify-helpers.d.ts +0 -50
- package/dist/core/schema-verify/verify-helpers.d.ts.map +0 -1
- package/dist/core/schema-verify/verify-sql-schema.d.ts +0 -45
- package/dist/core/schema-verify/verify-sql-schema.d.ts.map +0 -1
- package/dist/core/verify.d.ts +0 -39
- package/dist/core/verify.d.ts.map +0 -1
- package/dist/exports/control-adapter.d.ts +0 -2
- package/dist/exports/control-adapter.d.ts.map +0 -1
- package/dist/exports/control-adapter.js +0 -1
- package/dist/exports/control-adapter.js.map +0 -1
- package/dist/exports/control.d.ts +0 -13
- package/dist/exports/control.d.ts.map +0 -1
- package/dist/exports/control.js +0 -149
- package/dist/exports/control.js.map +0 -1
- package/dist/exports/runtime.d.ts +0 -8
- package/dist/exports/runtime.d.ts.map +0 -1
- package/dist/exports/runtime.js +0 -64
- package/dist/exports/runtime.js.map +0 -1
- package/dist/exports/schema-verify.d.ts +0 -11
- package/dist/exports/schema-verify.d.ts.map +0 -1
- package/dist/exports/schema-verify.js +0 -11
- package/dist/exports/schema-verify.js.map +0 -1
- package/dist/exports/test-utils.d.ts +0 -7
- package/dist/exports/test-utils.d.ts.map +0 -1
- package/dist/exports/test-utils.js +0 -17
- package/dist/exports/test-utils.js.map +0 -1
- package/dist/exports/verify.d.ts +0 -2
- package/dist/exports/verify.d.ts.map +0 -1
- package/dist/exports/verify.js +0 -11
- package/dist/exports/verify.js.map +0 -1
- package/src/core/descriptor.ts +0 -33
- package/src/core/instance.ts +0 -903
|
@@ -1,144 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type {
|
|
3
|
-
RuntimeAdapterDescriptor,
|
|
4
|
-
RuntimeDriverDescriptor,
|
|
5
|
-
RuntimeDriverInstance,
|
|
6
|
-
RuntimeFamilyDescriptor,
|
|
7
|
-
RuntimeFamilyInstance,
|
|
8
|
-
RuntimeTargetDescriptor,
|
|
9
|
-
} from '@prisma-next/core-execution-plane/types';
|
|
10
|
-
import type { Log, Plugin, RuntimeVerifyOptions } from '@prisma-next/runtime-executor';
|
|
11
|
-
import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
|
|
12
|
-
import type {
|
|
13
|
-
Adapter,
|
|
14
|
-
LoweredStatement,
|
|
15
|
-
SelectAst,
|
|
16
|
-
SqlDriver,
|
|
17
|
-
} from '@prisma-next/sql-relational-core/ast';
|
|
18
|
-
import type {
|
|
19
|
-
Runtime,
|
|
20
|
-
RuntimeOptions,
|
|
21
|
-
SqlRuntimeAdapterInstance,
|
|
22
|
-
SqlRuntimeExtensionDescriptor,
|
|
23
|
-
} from '@prisma-next/sql-runtime';
|
|
24
|
-
import { createRuntime, createRuntimeContext } from '@prisma-next/sql-runtime';
|
|
1
|
+
import type { RuntimeFamilyInstance } from '@prisma-next/framework-components/execution';
|
|
25
2
|
|
|
26
3
|
/**
|
|
27
|
-
* SQL
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
> &
|
|
34
|
-
SqlDriver;
|
|
35
|
-
|
|
36
|
-
// Re-export SqlRuntimeAdapterInstance from sql-runtime for consumers
|
|
37
|
-
export type { SqlRuntimeAdapterInstance } from '@prisma-next/sql-runtime';
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* SQL runtime family instance interface.
|
|
41
|
-
* Extends base RuntimeFamilyInstance with SQL-specific runtime creation method.
|
|
4
|
+
* SQL execution-plane family instance interface.
|
|
5
|
+
*
|
|
6
|
+
* Note: this is currently named `SqlRuntimeFamilyInstance` because the execution plane
|
|
7
|
+
* framework types are still using the `Runtime*` naming (`RuntimeFamilyInstance`, etc.).
|
|
8
|
+
*
|
|
9
|
+
* This will be renamed to `SqlExecutionFamilyInstance` as part of `TML-1842`.
|
|
42
10
|
*/
|
|
43
|
-
export interface SqlRuntimeFamilyInstance extends RuntimeFamilyInstance<'sql'> {
|
|
44
|
-
/**
|
|
45
|
-
* Creates a SQL runtime from contract, driver options, and verification settings.
|
|
46
|
-
*
|
|
47
|
-
* Extension packs are routed through composition (at instance creation time),
|
|
48
|
-
* not through this method. This aligns with control-plane composition patterns.
|
|
49
|
-
*
|
|
50
|
-
* @param options - Runtime creation options
|
|
51
|
-
* @param options.contract - SQL contract
|
|
52
|
-
* @param options.driverOptions - Driver options (e.g., PostgresDriverOptions)
|
|
53
|
-
* @param options.verify - Runtime verification options
|
|
54
|
-
* @param options.plugins - Optional plugins
|
|
55
|
-
* @param options.mode - Optional runtime mode
|
|
56
|
-
* @param options.log - Optional log instance
|
|
57
|
-
* @returns Runtime instance
|
|
58
|
-
*/
|
|
59
|
-
createRuntime<TContract extends SqlContract<SqlStorage>>(options: {
|
|
60
|
-
readonly contract: TContract;
|
|
61
|
-
readonly driverOptions: unknown;
|
|
62
|
-
readonly verify: RuntimeVerifyOptions;
|
|
63
|
-
readonly plugins?: readonly Plugin<
|
|
64
|
-
TContract,
|
|
65
|
-
Adapter<SelectAst, SqlContract<SqlStorage>, LoweredStatement>,
|
|
66
|
-
SqlDriver
|
|
67
|
-
>[];
|
|
68
|
-
readonly mode?: 'strict' | 'permissive';
|
|
69
|
-
readonly log?: Log;
|
|
70
|
-
}): Runtime;
|
|
71
|
-
}
|
|
11
|
+
export interface SqlRuntimeFamilyInstance extends RuntimeFamilyInstance<'sql'> {}
|
|
72
12
|
|
|
73
13
|
/**
|
|
74
|
-
* Creates a SQL
|
|
14
|
+
* Creates a SQL execution-plane family instance.
|
|
75
15
|
*
|
|
76
|
-
*
|
|
77
|
-
* family, target, adapter, driver, extensionPacks (all as descriptors with IDs).
|
|
16
|
+
* This will be renamed to `createSqlExecutionFamilyInstance()` as part of `TML-1842`.
|
|
78
17
|
*/
|
|
79
|
-
export function createSqlRuntimeFamilyInstance
|
|
80
|
-
readonly family: RuntimeFamilyDescriptor<'sql'>;
|
|
81
|
-
readonly target: RuntimeTargetDescriptor<'sql', TTargetId>;
|
|
82
|
-
readonly adapter: RuntimeAdapterDescriptor<
|
|
83
|
-
'sql',
|
|
84
|
-
TTargetId,
|
|
85
|
-
SqlRuntimeAdapterInstance<TTargetId>
|
|
86
|
-
>;
|
|
87
|
-
readonly driver: RuntimeDriverDescriptor<'sql', TTargetId, SqlRuntimeDriverInstance<TTargetId>>;
|
|
88
|
-
readonly extensionPacks?: readonly SqlRuntimeExtensionDescriptor<TTargetId>[];
|
|
89
|
-
}): SqlRuntimeFamilyInstance {
|
|
90
|
-
const {
|
|
91
|
-
family: familyDescriptor,
|
|
92
|
-
target: targetDescriptor,
|
|
93
|
-
adapter: adapterDescriptor,
|
|
94
|
-
driver: driverDescriptor,
|
|
95
|
-
extensionPacks: extensionDescriptors = [],
|
|
96
|
-
} = options;
|
|
97
|
-
|
|
18
|
+
export function createSqlRuntimeFamilyInstance(): SqlRuntimeFamilyInstance {
|
|
98
19
|
return {
|
|
99
20
|
familyId: 'sql' as const,
|
|
100
|
-
createRuntime<TContract extends SqlContract<SqlStorage>>(runtimeOptions: {
|
|
101
|
-
readonly contract: TContract;
|
|
102
|
-
readonly driverOptions: unknown;
|
|
103
|
-
readonly verify: RuntimeVerifyOptions;
|
|
104
|
-
readonly plugins?: readonly Plugin<
|
|
105
|
-
TContract,
|
|
106
|
-
Adapter<SelectAst, SqlContract<SqlStorage>, LoweredStatement>,
|
|
107
|
-
SqlDriver
|
|
108
|
-
>[];
|
|
109
|
-
readonly mode?: 'strict' | 'permissive';
|
|
110
|
-
readonly log?: Log;
|
|
111
|
-
}): Runtime {
|
|
112
|
-
// Validate contract requirements against provided descriptors
|
|
113
|
-
assertRuntimeContractRequirementsSatisfied({
|
|
114
|
-
contract: runtimeOptions.contract,
|
|
115
|
-
family: familyDescriptor,
|
|
116
|
-
target: targetDescriptor,
|
|
117
|
-
adapter: adapterDescriptor,
|
|
118
|
-
extensionPacks: extensionDescriptors,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Create driver instance
|
|
122
|
-
const driverInstance = driverDescriptor.create(runtimeOptions.driverOptions);
|
|
123
|
-
|
|
124
|
-
// Create context via descriptor-first API
|
|
125
|
-
const context = createRuntimeContext<TContract, TTargetId>({
|
|
126
|
-
contract: runtimeOptions.contract,
|
|
127
|
-
target: targetDescriptor,
|
|
128
|
-
adapter: adapterDescriptor,
|
|
129
|
-
extensionPacks: extensionDescriptors,
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const runtimeOptions_: RuntimeOptions<TContract> = {
|
|
133
|
-
driver: driverInstance,
|
|
134
|
-
verify: runtimeOptions.verify,
|
|
135
|
-
context,
|
|
136
|
-
...(runtimeOptions.plugins ? { plugins: runtimeOptions.plugins } : {}),
|
|
137
|
-
...(runtimeOptions.mode ? { mode: runtimeOptions.mode } : {}),
|
|
138
|
-
...(runtimeOptions.log ? { log: runtimeOptions.log } : {}),
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
return createRuntime(runtimeOptions_);
|
|
142
|
-
},
|
|
143
21
|
};
|
|
144
22
|
}
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
* These functions verify schema IR against contract requirements.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type {
|
|
6
|
+
import type {
|
|
7
|
+
SchemaIssue,
|
|
8
|
+
SchemaVerificationNode,
|
|
9
|
+
} from '@prisma-next/framework-components/control';
|
|
7
10
|
import type {
|
|
8
11
|
ForeignKey,
|
|
9
12
|
Index,
|
|
@@ -33,9 +36,71 @@ export function arraysEqual(a: readonly string[], b: readonly string[]): boolean
|
|
|
33
36
|
return true;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Semantic Satisfaction Predicates
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// These predicates implement the "stronger satisfies weaker" logic for storage
|
|
43
|
+
// objects. They are used by both verification and migration planning to ensure
|
|
44
|
+
// consistent behavior across the control plane.
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Checks if a unique constraint requirement is satisfied by the given columns.
|
|
48
|
+
*
|
|
49
|
+
* Semantic satisfaction: a unique constraint requirement can be satisfied by:
|
|
50
|
+
* - A unique constraint with the same columns, OR
|
|
51
|
+
* - A unique index with the same columns
|
|
52
|
+
*
|
|
53
|
+
* @param uniques - The unique constraints in the schema table
|
|
54
|
+
* @param indexes - The indexes in the schema table
|
|
55
|
+
* @param columns - The columns required by the unique constraint
|
|
56
|
+
* @returns true if the requirement is satisfied
|
|
57
|
+
*/
|
|
58
|
+
export function isUniqueConstraintSatisfied(
|
|
59
|
+
uniques: readonly SqlUniqueIR[],
|
|
60
|
+
indexes: readonly SqlIndexIR[],
|
|
61
|
+
columns: readonly string[],
|
|
62
|
+
): boolean {
|
|
63
|
+
// Check for matching unique constraint
|
|
64
|
+
const hasConstraint = uniques.some((unique) => arraysEqual(unique.columns, columns));
|
|
65
|
+
if (hasConstraint) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
// Check for matching unique index (semantic satisfaction)
|
|
69
|
+
return indexes.some((index) => index.unique && arraysEqual(index.columns, columns));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Checks if an index requirement is satisfied by the given columns.
|
|
74
|
+
*
|
|
75
|
+
* Semantic satisfaction: a non-unique index requirement can be satisfied by:
|
|
76
|
+
* - Any index (unique or non-unique) with the same columns, OR
|
|
77
|
+
* - A unique constraint with the same columns (stronger satisfies weaker)
|
|
78
|
+
*
|
|
79
|
+
* @param indexes - The indexes in the schema table
|
|
80
|
+
* @param uniques - The unique constraints in the schema table
|
|
81
|
+
* @param columns - The columns required by the index
|
|
82
|
+
* @returns true if the requirement is satisfied
|
|
83
|
+
*/
|
|
84
|
+
export function isIndexSatisfied(
|
|
85
|
+
indexes: readonly SqlIndexIR[],
|
|
86
|
+
uniques: readonly SqlUniqueIR[],
|
|
87
|
+
columns: readonly string[],
|
|
88
|
+
): boolean {
|
|
89
|
+
// Check for any matching index (unique or non-unique)
|
|
90
|
+
const hasMatchingIndex = indexes.some((index) => arraysEqual(index.columns, columns));
|
|
91
|
+
if (hasMatchingIndex) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
// Check for matching unique constraint (semantic satisfaction)
|
|
95
|
+
return uniques.some((unique) => arraysEqual(unique.columns, columns));
|
|
96
|
+
}
|
|
97
|
+
|
|
36
98
|
/**
|
|
37
99
|
* Verifies primary key matches between contract and schema.
|
|
38
100
|
* Returns 'pass' or 'fail'.
|
|
101
|
+
*
|
|
102
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
103
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
39
104
|
*/
|
|
40
105
|
export function verifyPrimaryKey(
|
|
41
106
|
contractPK: PrimaryKey,
|
|
@@ -64,18 +129,8 @@ export function verifyPrimaryKey(
|
|
|
64
129
|
return 'fail';
|
|
65
130
|
}
|
|
66
131
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
issues.push({
|
|
70
|
-
kind: 'primary_key_mismatch',
|
|
71
|
-
table: tableName,
|
|
72
|
-
indexOrConstraint: contractPK.name,
|
|
73
|
-
expected: contractPK.name,
|
|
74
|
-
actual: schemaPK.name,
|
|
75
|
-
message: `Table "${tableName}" has primary key name mismatch: expected "${contractPK.name}", got "${schemaPK.name}"`,
|
|
76
|
-
});
|
|
77
|
-
return 'fail';
|
|
78
|
-
}
|
|
132
|
+
// Name differences are ignored for semantic satisfaction.
|
|
133
|
+
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
79
134
|
|
|
80
135
|
return 'pass';
|
|
81
136
|
}
|
|
@@ -83,6 +138,9 @@ export function verifyPrimaryKey(
|
|
|
83
138
|
/**
|
|
84
139
|
* Verifies foreign keys match between contract and schema.
|
|
85
140
|
* Returns verification nodes for the tree.
|
|
141
|
+
*
|
|
142
|
+
* Uses semantic satisfaction: identity is based on (table + columns + referenced table + referenced columns).
|
|
143
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
86
144
|
*/
|
|
87
145
|
export function verifyForeignKeys(
|
|
88
146
|
contractFKs: readonly ForeignKey[],
|
|
@@ -124,15 +182,20 @@ export function verifyForeignKeys(
|
|
|
124
182
|
children: [],
|
|
125
183
|
});
|
|
126
184
|
} else {
|
|
127
|
-
|
|
128
|
-
if (
|
|
185
|
+
const actionMismatches = getReferentialActionMismatches(contractFK, matchingFK);
|
|
186
|
+
if (actionMismatches.length > 0) {
|
|
187
|
+
const combinedMessage = actionMismatches.map((m) => m.message).join('; ');
|
|
188
|
+
const combinedExpected = actionMismatches.map((m) => m.expected).join(', ');
|
|
189
|
+
const combinedActual = actionMismatches.map((m) => m.actual).join(', ');
|
|
129
190
|
issues.push({
|
|
130
191
|
kind: 'foreign_key_mismatch',
|
|
131
192
|
table: tableName,
|
|
132
|
-
indexOrConstraint
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
193
|
+
// Set indexOrConstraint so the planner classifies this as a non-additive
|
|
194
|
+
// conflict (existing FK with wrong actions cannot be fixed additively).
|
|
195
|
+
indexOrConstraint: matchingFK.name ?? `fk(${contractFK.columns.join(',')})`,
|
|
196
|
+
expected: combinedExpected,
|
|
197
|
+
actual: combinedActual,
|
|
198
|
+
message: `Table "${tableName}" foreign key ${contractFK.columns.join(', ')} -> ${contractFK.references.table}: ${combinedMessage}`,
|
|
136
199
|
});
|
|
137
200
|
nodes.push({
|
|
138
201
|
status: 'fail',
|
|
@@ -140,9 +203,9 @@ export function verifyForeignKeys(
|
|
|
140
203
|
name: `foreignKey(${contractFK.columns.join(', ')})`,
|
|
141
204
|
contractPath: fkPath,
|
|
142
205
|
code: 'foreign_key_mismatch',
|
|
143
|
-
message:
|
|
144
|
-
expected: contractFK
|
|
145
|
-
actual: matchingFK
|
|
206
|
+
message: combinedMessage,
|
|
207
|
+
expected: contractFK,
|
|
208
|
+
actual: matchingFK,
|
|
146
209
|
children: [],
|
|
147
210
|
});
|
|
148
211
|
} else {
|
|
@@ -176,6 +239,7 @@ export function verifyForeignKeys(
|
|
|
176
239
|
issues.push({
|
|
177
240
|
kind: 'extra_foreign_key',
|
|
178
241
|
table: tableName,
|
|
242
|
+
indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(',')})`,
|
|
179
243
|
message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(', ')} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(', ')})`,
|
|
180
244
|
});
|
|
181
245
|
nodes.push({
|
|
@@ -199,10 +263,18 @@ export function verifyForeignKeys(
|
|
|
199
263
|
/**
|
|
200
264
|
* Verifies unique constraints match between contract and schema.
|
|
201
265
|
* Returns verification nodes for the tree.
|
|
266
|
+
*
|
|
267
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
268
|
+
* A unique constraint requirement can be satisfied by either:
|
|
269
|
+
* - A unique constraint with the same columns, or
|
|
270
|
+
* - A unique index with the same columns
|
|
271
|
+
*
|
|
272
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
202
273
|
*/
|
|
203
274
|
export function verifyUniqueConstraints(
|
|
204
275
|
contractUniques: readonly UniqueConstraint[],
|
|
205
276
|
schemaUniques: readonly SqlUniqueIR[],
|
|
277
|
+
schemaIndexes: readonly SqlIndexIR[],
|
|
206
278
|
tableName: string,
|
|
207
279
|
tablePath: string,
|
|
208
280
|
issues: SchemaIssue[],
|
|
@@ -213,11 +285,18 @@ export function verifyUniqueConstraints(
|
|
|
213
285
|
// Check each contract unique exists in schema
|
|
214
286
|
for (const contractUnique of contractUniques) {
|
|
215
287
|
const uniquePath = `${tablePath}.uniques[${contractUnique.columns.join(',')}]`;
|
|
288
|
+
|
|
289
|
+
// First check for a matching unique constraint
|
|
216
290
|
const matchingUnique = schemaUniques.find((u) =>
|
|
217
291
|
arraysEqual(u.columns, contractUnique.columns),
|
|
218
292
|
);
|
|
219
293
|
|
|
220
|
-
|
|
294
|
+
// If no matching constraint, check for a unique index with the same columns
|
|
295
|
+
const matchingUniqueIndex =
|
|
296
|
+
!matchingUnique &&
|
|
297
|
+
schemaIndexes.find((idx) => idx.unique && arraysEqual(idx.columns, contractUnique.columns));
|
|
298
|
+
|
|
299
|
+
if (!matchingUnique && !matchingUniqueIndex) {
|
|
221
300
|
issues.push({
|
|
222
301
|
kind: 'unique_constraint_mismatch',
|
|
223
302
|
table: tableName,
|
|
@@ -236,44 +315,19 @@ export function verifyUniqueConstraints(
|
|
|
236
315
|
children: [],
|
|
237
316
|
});
|
|
238
317
|
} else {
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
});
|
|
253
|
-
nodes.push({
|
|
254
|
-
status: 'fail',
|
|
255
|
-
kind: 'unique',
|
|
256
|
-
name: `unique(${contractUnique.columns.join(', ')})`,
|
|
257
|
-
contractPath: uniquePath,
|
|
258
|
-
code: 'unique_constraint_mismatch',
|
|
259
|
-
message: 'Unique constraint name mismatch',
|
|
260
|
-
expected: contractUnique.name,
|
|
261
|
-
actual: matchingUnique.name,
|
|
262
|
-
children: [],
|
|
263
|
-
});
|
|
264
|
-
} else {
|
|
265
|
-
nodes.push({
|
|
266
|
-
status: 'pass',
|
|
267
|
-
kind: 'unique',
|
|
268
|
-
name: `unique(${contractUnique.columns.join(', ')})`,
|
|
269
|
-
contractPath: uniquePath,
|
|
270
|
-
code: '',
|
|
271
|
-
message: '',
|
|
272
|
-
expected: undefined,
|
|
273
|
-
actual: undefined,
|
|
274
|
-
children: [],
|
|
275
|
-
});
|
|
276
|
-
}
|
|
318
|
+
// Name differences are ignored for semantic satisfaction.
|
|
319
|
+
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
320
|
+
nodes.push({
|
|
321
|
+
status: 'pass',
|
|
322
|
+
kind: 'unique',
|
|
323
|
+
name: `unique(${contractUnique.columns.join(', ')})`,
|
|
324
|
+
contractPath: uniquePath,
|
|
325
|
+
code: '',
|
|
326
|
+
message: '',
|
|
327
|
+
expected: undefined,
|
|
328
|
+
actual: undefined,
|
|
329
|
+
children: [],
|
|
330
|
+
});
|
|
277
331
|
}
|
|
278
332
|
}
|
|
279
333
|
|
|
@@ -288,6 +342,7 @@ export function verifyUniqueConstraints(
|
|
|
288
342
|
issues.push({
|
|
289
343
|
kind: 'extra_unique_constraint',
|
|
290
344
|
table: tableName,
|
|
345
|
+
indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(',')})`,
|
|
291
346
|
message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(', ')}`,
|
|
292
347
|
});
|
|
293
348
|
nodes.push({
|
|
@@ -311,10 +366,18 @@ export function verifyUniqueConstraints(
|
|
|
311
366
|
/**
|
|
312
367
|
* Verifies indexes match between contract and schema.
|
|
313
368
|
* Returns verification nodes for the tree.
|
|
369
|
+
*
|
|
370
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
371
|
+
* A non-unique index requirement can be satisfied by either:
|
|
372
|
+
* - A non-unique index with the same columns, or
|
|
373
|
+
* - A unique index with the same columns (stronger satisfies weaker)
|
|
374
|
+
*
|
|
375
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
314
376
|
*/
|
|
315
377
|
export function verifyIndexes(
|
|
316
378
|
contractIndexes: readonly Index[],
|
|
317
379
|
schemaIndexes: readonly SqlIndexIR[],
|
|
380
|
+
schemaUniques: readonly SqlUniqueIR[],
|
|
318
381
|
tableName: string,
|
|
319
382
|
tablePath: string,
|
|
320
383
|
issues: SchemaIssue[],
|
|
@@ -325,11 +388,18 @@ export function verifyIndexes(
|
|
|
325
388
|
// Check each contract index exists in schema
|
|
326
389
|
for (const contractIndex of contractIndexes) {
|
|
327
390
|
const indexPath = `${tablePath}.indexes[${contractIndex.columns.join(',')}]`;
|
|
328
|
-
|
|
329
|
-
|
|
391
|
+
|
|
392
|
+
// Check for any matching index (unique or non-unique)
|
|
393
|
+
// A unique index can satisfy a non-unique index requirement (stronger satisfies weaker)
|
|
394
|
+
const matchingIndex = schemaIndexes.find((idx) =>
|
|
395
|
+
arraysEqual(idx.columns, contractIndex.columns),
|
|
330
396
|
);
|
|
331
397
|
|
|
332
|
-
if
|
|
398
|
+
// Also check if a unique constraint satisfies the index requirement
|
|
399
|
+
const matchingUniqueConstraint =
|
|
400
|
+
!matchingIndex && schemaUniques.find((u) => arraysEqual(u.columns, contractIndex.columns));
|
|
401
|
+
|
|
402
|
+
if (!matchingIndex && !matchingUniqueConstraint) {
|
|
333
403
|
issues.push({
|
|
334
404
|
kind: 'index_mismatch',
|
|
335
405
|
table: tableName,
|
|
@@ -348,40 +418,19 @@ export function verifyIndexes(
|
|
|
348
418
|
children: [],
|
|
349
419
|
});
|
|
350
420
|
} else {
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
name: `index(${contractIndex.columns.join(', ')})`,
|
|
365
|
-
contractPath: indexPath,
|
|
366
|
-
code: 'index_mismatch',
|
|
367
|
-
message: 'Index name mismatch',
|
|
368
|
-
expected: contractIndex.name,
|
|
369
|
-
actual: matchingIndex.name,
|
|
370
|
-
children: [],
|
|
371
|
-
});
|
|
372
|
-
} else {
|
|
373
|
-
nodes.push({
|
|
374
|
-
status: 'pass',
|
|
375
|
-
kind: 'index',
|
|
376
|
-
name: `index(${contractIndex.columns.join(', ')})`,
|
|
377
|
-
contractPath: indexPath,
|
|
378
|
-
code: '',
|
|
379
|
-
message: '',
|
|
380
|
-
expected: undefined,
|
|
381
|
-
actual: undefined,
|
|
382
|
-
children: [],
|
|
383
|
-
});
|
|
384
|
-
}
|
|
421
|
+
// Name differences are ignored for semantic satisfaction.
|
|
422
|
+
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
423
|
+
nodes.push({
|
|
424
|
+
status: 'pass',
|
|
425
|
+
kind: 'index',
|
|
426
|
+
name: `index(${contractIndex.columns.join(', ')})`,
|
|
427
|
+
contractPath: indexPath,
|
|
428
|
+
code: '',
|
|
429
|
+
message: '',
|
|
430
|
+
expected: undefined,
|
|
431
|
+
actual: undefined,
|
|
432
|
+
children: [],
|
|
433
|
+
});
|
|
385
434
|
}
|
|
386
435
|
}
|
|
387
436
|
|
|
@@ -401,6 +450,7 @@ export function verifyIndexes(
|
|
|
401
450
|
issues.push({
|
|
402
451
|
kind: 'extra_index',
|
|
403
452
|
table: tableName,
|
|
453
|
+
indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(',')})`,
|
|
404
454
|
message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(', ')}`,
|
|
405
455
|
});
|
|
406
456
|
nodes.push({
|
|
@@ -423,8 +473,8 @@ export function verifyIndexes(
|
|
|
423
473
|
|
|
424
474
|
/**
|
|
425
475
|
* Verifies database dependencies are installed using component-owned verification hooks.
|
|
426
|
-
*
|
|
427
|
-
*
|
|
476
|
+
* Checks whether each dependency is satisfied by verifying its id is present in
|
|
477
|
+
* schema.dependencies (populated from introspection).
|
|
428
478
|
*
|
|
429
479
|
* Returns verification nodes for the tree.
|
|
430
480
|
*/
|
|
@@ -434,16 +484,20 @@ export function verifyDatabaseDependencies(
|
|
|
434
484
|
issues: SchemaIssue[],
|
|
435
485
|
): SchemaVerificationNode[] {
|
|
436
486
|
const nodes: SchemaVerificationNode[] = [];
|
|
487
|
+
const installedIds = new Set(schema.dependencies.map((d) => d.id));
|
|
437
488
|
|
|
438
489
|
for (const dependency of dependencies) {
|
|
439
|
-
const
|
|
490
|
+
const isSatisfied = installedIds.has(dependency.id);
|
|
440
491
|
const depPath = `dependencies.${dependency.id}`;
|
|
441
492
|
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
493
|
+
if (!isSatisfied) {
|
|
494
|
+
const depIssue: SchemaIssue = {
|
|
495
|
+
kind: 'dependency_missing',
|
|
496
|
+
dependencyId: dependency.id,
|
|
497
|
+
message: `Dependency "${dependency.id}" is missing from database`,
|
|
498
|
+
};
|
|
499
|
+
issues.push(depIssue);
|
|
500
|
+
const nodeMessage = depIssue.message;
|
|
447
501
|
nodes.push({
|
|
448
502
|
status: 'fail',
|
|
449
503
|
kind: 'databaseDependency',
|
|
@@ -512,3 +566,49 @@ export function computeCounts(node: SchemaVerificationNode): {
|
|
|
512
566
|
totalNodes: pass + warn + fail,
|
|
513
567
|
};
|
|
514
568
|
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Compares referential actions between a contract FK and a schema FK.
|
|
572
|
+
* Only compares when the contract FK explicitly specifies onDelete or onUpdate.
|
|
573
|
+
* Returns all mismatches (both onDelete and onUpdate) so both are reported at once.
|
|
574
|
+
*
|
|
575
|
+
* Note: 'noAction' in the contract is semantically equivalent to undefined in the
|
|
576
|
+
* schema IR, because the introspection adapter omits 'NO ACTION' (the database default)
|
|
577
|
+
* to keep the IR sparse. We normalize both sides before comparing.
|
|
578
|
+
*/
|
|
579
|
+
function getReferentialActionMismatches(
|
|
580
|
+
contractFK: ForeignKey,
|
|
581
|
+
schemaFK: SqlForeignKeyIR,
|
|
582
|
+
): ReadonlyArray<{ expected: string; actual: string; message: string }> {
|
|
583
|
+
const mismatches: Array<{ expected: string; actual: string; message: string }> = [];
|
|
584
|
+
|
|
585
|
+
const contractOnDelete = normalizeReferentialAction(contractFK.onDelete);
|
|
586
|
+
const schemaOnDelete = normalizeReferentialAction(schemaFK.onDelete);
|
|
587
|
+
if (contractOnDelete !== undefined && contractOnDelete !== schemaOnDelete) {
|
|
588
|
+
mismatches.push({
|
|
589
|
+
expected: `onDelete: ${contractFK.onDelete}`,
|
|
590
|
+
actual: `onDelete: ${schemaFK.onDelete ?? 'noAction (default)'}`,
|
|
591
|
+
message: `onDelete mismatch: expected ${contractFK.onDelete}, got ${schemaFK.onDelete ?? 'noAction (default)'}`,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const contractOnUpdate = normalizeReferentialAction(contractFK.onUpdate);
|
|
596
|
+
const schemaOnUpdate = normalizeReferentialAction(schemaFK.onUpdate);
|
|
597
|
+
if (contractOnUpdate !== undefined && contractOnUpdate !== schemaOnUpdate) {
|
|
598
|
+
mismatches.push({
|
|
599
|
+
expected: `onUpdate: ${contractFK.onUpdate}`,
|
|
600
|
+
actual: `onUpdate: ${schemaFK.onUpdate ?? 'noAction (default)'}`,
|
|
601
|
+
message: `onUpdate mismatch: expected ${contractFK.onUpdate}, got ${schemaFK.onUpdate ?? 'noAction (default)'}`,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return mismatches;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Normalizes a referential action value for comparison.
|
|
610
|
+
* 'noAction' is the database default and equivalent to undefined (omitted) in the sparse IR.
|
|
611
|
+
*/
|
|
612
|
+
function normalizeReferentialAction(action: string | undefined): string | undefined {
|
|
613
|
+
return action === 'noAction' ? undefined : action;
|
|
614
|
+
}
|