@prisma-next/family-sql 0.3.0-dev.12 → 0.3.0-dev.123
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 +34 -7
- package/dist/assembly-Dzumaba1.mjs +159 -0
- package/dist/assembly-Dzumaba1.mjs.map +1 -0
- package/dist/control-adapter.d.mts +60 -0
- package/dist/control-adapter.d.mts.map +1 -0
- package/dist/control-adapter.mjs +1 -0
- package/dist/control-instance-BKuHINR7.d.mts +411 -0
- package/dist/control-instance-BKuHINR7.d.mts.map +1 -0
- package/dist/control.d.mts +128 -0
- package/dist/control.d.mts.map +1 -0
- package/dist/control.mjs +683 -0
- package/dist/control.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 +4 -0
- package/dist/test-utils.d.mts +2 -0
- package/dist/test-utils.mjs +3 -0
- package/dist/verify-BfMETJcM.mjs +108 -0
- package/dist/verify-BfMETJcM.mjs.map +1 -0
- package/dist/verify-sql-schema-C3Pit9o4.mjs +1085 -0
- package/dist/verify-sql-schema-C3Pit9o4.mjs.map +1 -0
- package/dist/verify-sql-schema-DhHnkpPa.d.mts +67 -0
- package/dist/verify-sql-schema-DhHnkpPa.d.mts.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 +35 -46
- package/src/core/assembly.ts +265 -59
- package/src/core/control-adapter.ts +15 -0
- package/src/core/{descriptor.ts → control-descriptor.ts} +15 -11
- package/src/core/{instance.ts → control-instance.ts} +106 -248
- package/src/core/migrations/contract-to-schema-ir.ts +265 -0
- package/src/core/migrations/types.ts +193 -168
- package/src/core/runtime-descriptor.ts +19 -41
- package/src/core/runtime-instance.ts +11 -133
- package/src/core/schema-verify/verify-helpers.ts +201 -105
- package/src/core/schema-verify/verify-sql-schema.ts +918 -413
- package/src/core/verify.ts +4 -13
- package/src/exports/control.ts +29 -6
- package/src/exports/runtime.ts +2 -6
- package/src/exports/schema-verify.ts +10 -2
- package/src/exports/test-utils.ts +1 -1
- 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
|
@@ -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/core-execution-plane/types';
|
|
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
|
}
|
|
@@ -33,9 +33,71 @@ export function arraysEqual(a: readonly string[], b: readonly string[]): boolean
|
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Semantic Satisfaction Predicates
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// These predicates implement the "stronger satisfies weaker" logic for storage
|
|
40
|
+
// objects. They are used by both verification and migration planning to ensure
|
|
41
|
+
// consistent behavior across the control plane.
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Checks if a unique constraint requirement is satisfied by the given columns.
|
|
45
|
+
*
|
|
46
|
+
* Semantic satisfaction: a unique constraint requirement can be satisfied by:
|
|
47
|
+
* - A unique constraint with the same columns, OR
|
|
48
|
+
* - A unique index with the same columns
|
|
49
|
+
*
|
|
50
|
+
* @param uniques - The unique constraints in the schema table
|
|
51
|
+
* @param indexes - The indexes in the schema table
|
|
52
|
+
* @param columns - The columns required by the unique constraint
|
|
53
|
+
* @returns true if the requirement is satisfied
|
|
54
|
+
*/
|
|
55
|
+
export function isUniqueConstraintSatisfied(
|
|
56
|
+
uniques: readonly SqlUniqueIR[],
|
|
57
|
+
indexes: readonly SqlIndexIR[],
|
|
58
|
+
columns: readonly string[],
|
|
59
|
+
): boolean {
|
|
60
|
+
// Check for matching unique constraint
|
|
61
|
+
const hasConstraint = uniques.some((unique) => arraysEqual(unique.columns, columns));
|
|
62
|
+
if (hasConstraint) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// Check for matching unique index (semantic satisfaction)
|
|
66
|
+
return indexes.some((index) => index.unique && arraysEqual(index.columns, columns));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Checks if an index requirement is satisfied by the given columns.
|
|
71
|
+
*
|
|
72
|
+
* Semantic satisfaction: a non-unique index requirement can be satisfied by:
|
|
73
|
+
* - Any index (unique or non-unique) with the same columns, OR
|
|
74
|
+
* - A unique constraint with the same columns (stronger satisfies weaker)
|
|
75
|
+
*
|
|
76
|
+
* @param indexes - The indexes in the schema table
|
|
77
|
+
* @param uniques - The unique constraints in the schema table
|
|
78
|
+
* @param columns - The columns required by the index
|
|
79
|
+
* @returns true if the requirement is satisfied
|
|
80
|
+
*/
|
|
81
|
+
export function isIndexSatisfied(
|
|
82
|
+
indexes: readonly SqlIndexIR[],
|
|
83
|
+
uniques: readonly SqlUniqueIR[],
|
|
84
|
+
columns: readonly string[],
|
|
85
|
+
): boolean {
|
|
86
|
+
// Check for any matching index (unique or non-unique)
|
|
87
|
+
const hasMatchingIndex = indexes.some((index) => arraysEqual(index.columns, columns));
|
|
88
|
+
if (hasMatchingIndex) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
// Check for matching unique constraint (semantic satisfaction)
|
|
92
|
+
return uniques.some((unique) => arraysEqual(unique.columns, columns));
|
|
93
|
+
}
|
|
94
|
+
|
|
36
95
|
/**
|
|
37
96
|
* Verifies primary key matches between contract and schema.
|
|
38
97
|
* Returns 'pass' or 'fail'.
|
|
98
|
+
*
|
|
99
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
100
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
39
101
|
*/
|
|
40
102
|
export function verifyPrimaryKey(
|
|
41
103
|
contractPK: PrimaryKey,
|
|
@@ -64,18 +126,8 @@ export function verifyPrimaryKey(
|
|
|
64
126
|
return 'fail';
|
|
65
127
|
}
|
|
66
128
|
|
|
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
|
-
}
|
|
129
|
+
// Name differences are ignored for semantic satisfaction.
|
|
130
|
+
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
79
131
|
|
|
80
132
|
return 'pass';
|
|
81
133
|
}
|
|
@@ -83,6 +135,9 @@ export function verifyPrimaryKey(
|
|
|
83
135
|
/**
|
|
84
136
|
* Verifies foreign keys match between contract and schema.
|
|
85
137
|
* Returns verification nodes for the tree.
|
|
138
|
+
*
|
|
139
|
+
* Uses semantic satisfaction: identity is based on (table + columns + referenced table + referenced columns).
|
|
140
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
86
141
|
*/
|
|
87
142
|
export function verifyForeignKeys(
|
|
88
143
|
contractFKs: readonly ForeignKey[],
|
|
@@ -124,15 +179,20 @@ export function verifyForeignKeys(
|
|
|
124
179
|
children: [],
|
|
125
180
|
});
|
|
126
181
|
} else {
|
|
127
|
-
|
|
128
|
-
if (
|
|
182
|
+
const actionMismatches = getReferentialActionMismatches(contractFK, matchingFK);
|
|
183
|
+
if (actionMismatches.length > 0) {
|
|
184
|
+
const combinedMessage = actionMismatches.map((m) => m.message).join('; ');
|
|
185
|
+
const combinedExpected = actionMismatches.map((m) => m.expected).join(', ');
|
|
186
|
+
const combinedActual = actionMismatches.map((m) => m.actual).join(', ');
|
|
129
187
|
issues.push({
|
|
130
188
|
kind: 'foreign_key_mismatch',
|
|
131
189
|
table: tableName,
|
|
132
|
-
indexOrConstraint
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
190
|
+
// Set indexOrConstraint so the planner classifies this as a non-additive
|
|
191
|
+
// conflict (existing FK with wrong actions cannot be fixed additively).
|
|
192
|
+
indexOrConstraint: matchingFK.name ?? `fk(${contractFK.columns.join(',')})`,
|
|
193
|
+
expected: combinedExpected,
|
|
194
|
+
actual: combinedActual,
|
|
195
|
+
message: `Table "${tableName}" foreign key ${contractFK.columns.join(', ')} -> ${contractFK.references.table}: ${combinedMessage}`,
|
|
136
196
|
});
|
|
137
197
|
nodes.push({
|
|
138
198
|
status: 'fail',
|
|
@@ -140,9 +200,9 @@ export function verifyForeignKeys(
|
|
|
140
200
|
name: `foreignKey(${contractFK.columns.join(', ')})`,
|
|
141
201
|
contractPath: fkPath,
|
|
142
202
|
code: 'foreign_key_mismatch',
|
|
143
|
-
message:
|
|
144
|
-
expected: contractFK
|
|
145
|
-
actual: matchingFK
|
|
203
|
+
message: combinedMessage,
|
|
204
|
+
expected: contractFK,
|
|
205
|
+
actual: matchingFK,
|
|
146
206
|
children: [],
|
|
147
207
|
});
|
|
148
208
|
} else {
|
|
@@ -176,6 +236,7 @@ export function verifyForeignKeys(
|
|
|
176
236
|
issues.push({
|
|
177
237
|
kind: 'extra_foreign_key',
|
|
178
238
|
table: tableName,
|
|
239
|
+
indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(',')})`,
|
|
179
240
|
message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(', ')} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(', ')})`,
|
|
180
241
|
});
|
|
181
242
|
nodes.push({
|
|
@@ -199,10 +260,18 @@ export function verifyForeignKeys(
|
|
|
199
260
|
/**
|
|
200
261
|
* Verifies unique constraints match between contract and schema.
|
|
201
262
|
* Returns verification nodes for the tree.
|
|
263
|
+
*
|
|
264
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
265
|
+
* A unique constraint requirement can be satisfied by either:
|
|
266
|
+
* - A unique constraint with the same columns, or
|
|
267
|
+
* - A unique index with the same columns
|
|
268
|
+
*
|
|
269
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
202
270
|
*/
|
|
203
271
|
export function verifyUniqueConstraints(
|
|
204
272
|
contractUniques: readonly UniqueConstraint[],
|
|
205
273
|
schemaUniques: readonly SqlUniqueIR[],
|
|
274
|
+
schemaIndexes: readonly SqlIndexIR[],
|
|
206
275
|
tableName: string,
|
|
207
276
|
tablePath: string,
|
|
208
277
|
issues: SchemaIssue[],
|
|
@@ -213,11 +282,18 @@ export function verifyUniqueConstraints(
|
|
|
213
282
|
// Check each contract unique exists in schema
|
|
214
283
|
for (const contractUnique of contractUniques) {
|
|
215
284
|
const uniquePath = `${tablePath}.uniques[${contractUnique.columns.join(',')}]`;
|
|
285
|
+
|
|
286
|
+
// First check for a matching unique constraint
|
|
216
287
|
const matchingUnique = schemaUniques.find((u) =>
|
|
217
288
|
arraysEqual(u.columns, contractUnique.columns),
|
|
218
289
|
);
|
|
219
290
|
|
|
220
|
-
|
|
291
|
+
// If no matching constraint, check for a unique index with the same columns
|
|
292
|
+
const matchingUniqueIndex =
|
|
293
|
+
!matchingUnique &&
|
|
294
|
+
schemaIndexes.find((idx) => idx.unique && arraysEqual(idx.columns, contractUnique.columns));
|
|
295
|
+
|
|
296
|
+
if (!matchingUnique && !matchingUniqueIndex) {
|
|
221
297
|
issues.push({
|
|
222
298
|
kind: 'unique_constraint_mismatch',
|
|
223
299
|
table: tableName,
|
|
@@ -236,44 +312,19 @@ export function verifyUniqueConstraints(
|
|
|
236
312
|
children: [],
|
|
237
313
|
});
|
|
238
314
|
} 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
|
-
}
|
|
315
|
+
// Name differences are ignored for semantic satisfaction.
|
|
316
|
+
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
317
|
+
nodes.push({
|
|
318
|
+
status: 'pass',
|
|
319
|
+
kind: 'unique',
|
|
320
|
+
name: `unique(${contractUnique.columns.join(', ')})`,
|
|
321
|
+
contractPath: uniquePath,
|
|
322
|
+
code: '',
|
|
323
|
+
message: '',
|
|
324
|
+
expected: undefined,
|
|
325
|
+
actual: undefined,
|
|
326
|
+
children: [],
|
|
327
|
+
});
|
|
277
328
|
}
|
|
278
329
|
}
|
|
279
330
|
|
|
@@ -288,6 +339,7 @@ export function verifyUniqueConstraints(
|
|
|
288
339
|
issues.push({
|
|
289
340
|
kind: 'extra_unique_constraint',
|
|
290
341
|
table: tableName,
|
|
342
|
+
indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(',')})`,
|
|
291
343
|
message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(', ')}`,
|
|
292
344
|
});
|
|
293
345
|
nodes.push({
|
|
@@ -311,10 +363,18 @@ export function verifyUniqueConstraints(
|
|
|
311
363
|
/**
|
|
312
364
|
* Verifies indexes match between contract and schema.
|
|
313
365
|
* Returns verification nodes for the tree.
|
|
366
|
+
*
|
|
367
|
+
* Uses semantic satisfaction: identity is based on (table + kind + columns).
|
|
368
|
+
* A non-unique index requirement can be satisfied by either:
|
|
369
|
+
* - A non-unique index with the same columns, or
|
|
370
|
+
* - A unique index with the same columns (stronger satisfies weaker)
|
|
371
|
+
*
|
|
372
|
+
* Name differences are ignored by default (names are for DDL/diagnostics, not identity).
|
|
314
373
|
*/
|
|
315
374
|
export function verifyIndexes(
|
|
316
375
|
contractIndexes: readonly Index[],
|
|
317
376
|
schemaIndexes: readonly SqlIndexIR[],
|
|
377
|
+
schemaUniques: readonly SqlUniqueIR[],
|
|
318
378
|
tableName: string,
|
|
319
379
|
tablePath: string,
|
|
320
380
|
issues: SchemaIssue[],
|
|
@@ -325,11 +385,18 @@ export function verifyIndexes(
|
|
|
325
385
|
// Check each contract index exists in schema
|
|
326
386
|
for (const contractIndex of contractIndexes) {
|
|
327
387
|
const indexPath = `${tablePath}.indexes[${contractIndex.columns.join(',')}]`;
|
|
328
|
-
|
|
329
|
-
|
|
388
|
+
|
|
389
|
+
// Check for any matching index (unique or non-unique)
|
|
390
|
+
// A unique index can satisfy a non-unique index requirement (stronger satisfies weaker)
|
|
391
|
+
const matchingIndex = schemaIndexes.find((idx) =>
|
|
392
|
+
arraysEqual(idx.columns, contractIndex.columns),
|
|
330
393
|
);
|
|
331
394
|
|
|
332
|
-
if
|
|
395
|
+
// Also check if a unique constraint satisfies the index requirement
|
|
396
|
+
const matchingUniqueConstraint =
|
|
397
|
+
!matchingIndex && schemaUniques.find((u) => arraysEqual(u.columns, contractIndex.columns));
|
|
398
|
+
|
|
399
|
+
if (!matchingIndex && !matchingUniqueConstraint) {
|
|
333
400
|
issues.push({
|
|
334
401
|
kind: 'index_mismatch',
|
|
335
402
|
table: tableName,
|
|
@@ -348,40 +415,19 @@ export function verifyIndexes(
|
|
|
348
415
|
children: [],
|
|
349
416
|
});
|
|
350
417
|
} 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
|
-
}
|
|
418
|
+
// Name differences are ignored for semantic satisfaction.
|
|
419
|
+
// Names are persisted for deterministic DDL and diagnostics but are not identity.
|
|
420
|
+
nodes.push({
|
|
421
|
+
status: 'pass',
|
|
422
|
+
kind: 'index',
|
|
423
|
+
name: `index(${contractIndex.columns.join(', ')})`,
|
|
424
|
+
contractPath: indexPath,
|
|
425
|
+
code: '',
|
|
426
|
+
message: '',
|
|
427
|
+
expected: undefined,
|
|
428
|
+
actual: undefined,
|
|
429
|
+
children: [],
|
|
430
|
+
});
|
|
385
431
|
}
|
|
386
432
|
}
|
|
387
433
|
|
|
@@ -401,6 +447,7 @@ export function verifyIndexes(
|
|
|
401
447
|
issues.push({
|
|
402
448
|
kind: 'extra_index',
|
|
403
449
|
table: tableName,
|
|
450
|
+
indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(',')})`,
|
|
404
451
|
message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(', ')}`,
|
|
405
452
|
});
|
|
406
453
|
nodes.push({
|
|
@@ -423,8 +470,8 @@ export function verifyIndexes(
|
|
|
423
470
|
|
|
424
471
|
/**
|
|
425
472
|
* Verifies database dependencies are installed using component-owned verification hooks.
|
|
426
|
-
*
|
|
427
|
-
*
|
|
473
|
+
* Checks whether each dependency is satisfied by verifying its id is present in
|
|
474
|
+
* schema.dependencies (populated from introspection).
|
|
428
475
|
*
|
|
429
476
|
* Returns verification nodes for the tree.
|
|
430
477
|
*/
|
|
@@ -434,16 +481,19 @@ export function verifyDatabaseDependencies(
|
|
|
434
481
|
issues: SchemaIssue[],
|
|
435
482
|
): SchemaVerificationNode[] {
|
|
436
483
|
const nodes: SchemaVerificationNode[] = [];
|
|
484
|
+
const installedIds = new Set(schema.dependencies.map((d) => d.id));
|
|
437
485
|
|
|
438
486
|
for (const dependency of dependencies) {
|
|
439
|
-
const
|
|
487
|
+
const isSatisfied = installedIds.has(dependency.id);
|
|
440
488
|
const depPath = `dependencies.${dependency.id}`;
|
|
441
489
|
|
|
442
|
-
if (
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
490
|
+
if (!isSatisfied) {
|
|
491
|
+
const depIssue: SchemaIssue = {
|
|
492
|
+
kind: 'dependency_missing',
|
|
493
|
+
message: `Dependency "${dependency.id}" is missing from database`,
|
|
494
|
+
};
|
|
495
|
+
issues.push(depIssue);
|
|
496
|
+
const nodeMessage = depIssue.message;
|
|
447
497
|
nodes.push({
|
|
448
498
|
status: 'fail',
|
|
449
499
|
kind: 'databaseDependency',
|
|
@@ -512,3 +562,49 @@ export function computeCounts(node: SchemaVerificationNode): {
|
|
|
512
562
|
totalNodes: pass + warn + fail,
|
|
513
563
|
};
|
|
514
564
|
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Compares referential actions between a contract FK and a schema FK.
|
|
568
|
+
* Only compares when the contract FK explicitly specifies onDelete or onUpdate.
|
|
569
|
+
* Returns all mismatches (both onDelete and onUpdate) so both are reported at once.
|
|
570
|
+
*
|
|
571
|
+
* Note: 'noAction' in the contract is semantically equivalent to undefined in the
|
|
572
|
+
* schema IR, because the introspection adapter omits 'NO ACTION' (the database default)
|
|
573
|
+
* to keep the IR sparse. We normalize both sides before comparing.
|
|
574
|
+
*/
|
|
575
|
+
function getReferentialActionMismatches(
|
|
576
|
+
contractFK: ForeignKey,
|
|
577
|
+
schemaFK: SqlForeignKeyIR,
|
|
578
|
+
): ReadonlyArray<{ expected: string; actual: string; message: string }> {
|
|
579
|
+
const mismatches: Array<{ expected: string; actual: string; message: string }> = [];
|
|
580
|
+
|
|
581
|
+
const contractOnDelete = normalizeReferentialAction(contractFK.onDelete);
|
|
582
|
+
const schemaOnDelete = normalizeReferentialAction(schemaFK.onDelete);
|
|
583
|
+
if (contractOnDelete !== undefined && contractOnDelete !== schemaOnDelete) {
|
|
584
|
+
mismatches.push({
|
|
585
|
+
expected: `onDelete: ${contractFK.onDelete}`,
|
|
586
|
+
actual: `onDelete: ${schemaFK.onDelete ?? 'noAction (default)'}`,
|
|
587
|
+
message: `onDelete mismatch: expected ${contractFK.onDelete}, got ${schemaFK.onDelete ?? 'noAction (default)'}`,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const contractOnUpdate = normalizeReferentialAction(contractFK.onUpdate);
|
|
592
|
+
const schemaOnUpdate = normalizeReferentialAction(schemaFK.onUpdate);
|
|
593
|
+
if (contractOnUpdate !== undefined && contractOnUpdate !== schemaOnUpdate) {
|
|
594
|
+
mismatches.push({
|
|
595
|
+
expected: `onUpdate: ${contractFK.onUpdate}`,
|
|
596
|
+
actual: `onUpdate: ${schemaFK.onUpdate ?? 'noAction (default)'}`,
|
|
597
|
+
message: `onUpdate mismatch: expected ${contractFK.onUpdate}, got ${schemaFK.onUpdate ?? 'noAction (default)'}`,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return mismatches;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Normalizes a referential action value for comparison.
|
|
606
|
+
* 'noAction' is the database default and equivalent to undefined (omitted) in the sparse IR.
|
|
607
|
+
*/
|
|
608
|
+
function normalizeReferentialAction(action: string | undefined): string | undefined {
|
|
609
|
+
return action === 'noAction' ? undefined : action;
|
|
610
|
+
}
|