@prisma-next/family-sql 0.3.0-dev.4 → 0.3.0-dev.6
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/chunk-6P44BVZ4.js → chunk-6K3RPBDP.js} +3 -3
- package/dist/chunk-6K3RPBDP.js.map +1 -0
- package/dist/{exports/chunk-F252JMEU.js → chunk-BHEGVBY7.js} +1 -1
- package/dist/chunk-BHEGVBY7.js.map +1 -0
- package/dist/{exports/chunk-C3GKWCKA.js → chunk-SU7LN2UH.js} +1 -1
- package/dist/chunk-SU7LN2UH.js.map +1 -0
- package/dist/core/assembly.d.ts +25 -0
- package/dist/core/assembly.d.ts.map +1 -0
- package/dist/core/control-adapter.d.ts +42 -0
- package/dist/core/control-adapter.d.ts.map +1 -0
- package/dist/core/descriptor.d.ts +31 -0
- package/dist/core/descriptor.d.ts.map +1 -0
- package/dist/{exports/instance-DiZi2k_2.d.ts → core/instance.d.ts} +30 -15
- package/dist/core/instance.d.ts.map +1 -0
- package/dist/core/migrations/plan-helpers.d.ts +20 -0
- package/dist/core/migrations/plan-helpers.d.ts.map +1 -0
- package/dist/core/migrations/policies.d.ts +6 -0
- package/dist/core/migrations/policies.d.ts.map +1 -0
- package/dist/{exports/types-Bh7ftf0Q.d.ts → core/migrations/types.d.ts} +41 -36
- package/dist/core/migrations/types.d.ts.map +1 -0
- package/dist/core/runtime-descriptor.d.ts +19 -0
- package/dist/core/runtime-descriptor.d.ts.map +1 -0
- package/dist/core/runtime-instance.d.ts +54 -0
- package/dist/core/runtime-instance.d.ts.map +1 -0
- package/dist/core/schema-verify/verify-helpers.d.ts +50 -0
- package/dist/core/schema-verify/verify-helpers.d.ts.map +1 -0
- package/dist/core/schema-verify/verify-sql-schema.d.ts +45 -0
- package/dist/core/schema-verify/verify-sql-schema.d.ts.map +1 -0
- package/dist/core/verify.d.ts +39 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/exports/control-adapter.d.ts +2 -44
- package/dist/exports/control-adapter.d.ts.map +1 -0
- package/dist/exports/control.d.ts +8 -70
- package/dist/exports/control.d.ts.map +1 -0
- package/dist/exports/control.js +3 -3
- package/dist/exports/runtime.d.ts +3 -61
- package/dist/exports/runtime.d.ts.map +1 -0
- package/dist/exports/schema-verify.d.ts +8 -72
- package/dist/exports/schema-verify.d.ts.map +1 -0
- package/dist/exports/schema-verify.js +1 -1
- package/dist/exports/test-utils.d.ts +5 -31
- package/dist/exports/test-utils.d.ts.map +1 -0
- package/dist/exports/test-utils.js +3 -3
- package/dist/exports/verify.d.ts +2 -28
- package/dist/exports/verify.d.ts.map +1 -0
- package/dist/exports/verify.js +1 -1
- package/package.json +19 -18
- package/src/core/assembly.ts +117 -0
- package/src/core/control-adapter.ts +52 -0
- package/src/core/descriptor.ts +37 -0
- package/src/core/instance.ts +887 -0
- package/src/core/migrations/plan-helpers.ts +164 -0
- package/src/core/migrations/policies.ts +8 -0
- package/src/core/migrations/types.ts +380 -0
- package/src/core/runtime-descriptor.ts +45 -0
- package/src/core/runtime-instance.ts +144 -0
- package/src/core/schema-verify/verify-helpers.ts +514 -0
- package/src/core/schema-verify/verify-sql-schema.ts +584 -0
- package/src/core/verify.ts +177 -0
- package/src/exports/control-adapter.ts +1 -0
- package/src/exports/control.ts +56 -0
- package/src/exports/runtime.ts +7 -0
- package/src/exports/schema-verify.ts +11 -0
- package/src/exports/test-utils.ts +11 -0
- package/src/exports/verify.ts +1 -0
- package/dist/exports/chunk-6P44BVZ4.js.map +0 -1
- package/dist/exports/chunk-C3GKWCKA.js.map +0 -1
- package/dist/exports/chunk-F252JMEU.js.map +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { assertRuntimeContractRequirementsSatisfied } from '@prisma-next/core-execution-plane/framework-components';
|
|
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';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* SQL runtime driver instance type.
|
|
28
|
+
* Combines identity properties with SQL-specific behavior methods.
|
|
29
|
+
*/
|
|
30
|
+
export type SqlRuntimeDriverInstance<TTargetId extends string = string> = RuntimeDriverInstance<
|
|
31
|
+
'sql',
|
|
32
|
+
TTargetId
|
|
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.
|
|
42
|
+
*/
|
|
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
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a SQL runtime family instance from runtime descriptors.
|
|
75
|
+
*
|
|
76
|
+
* Routes the same framework composition as control-plane:
|
|
77
|
+
* family, target, adapter, driver, extensionPacks (all as descriptors with IDs).
|
|
78
|
+
*/
|
|
79
|
+
export function createSqlRuntimeFamilyInstance<TTargetId extends string>(options: {
|
|
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
|
+
|
|
98
|
+
return {
|
|
99
|
+
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
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure verification helper functions for SQL schema verification.
|
|
3
|
+
* These functions verify schema IR against contract requirements.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SchemaIssue, SchemaVerificationNode } from '@prisma-next/core-control-plane/types';
|
|
7
|
+
import type {
|
|
8
|
+
ForeignKey,
|
|
9
|
+
Index,
|
|
10
|
+
PrimaryKey,
|
|
11
|
+
UniqueConstraint,
|
|
12
|
+
} from '@prisma-next/sql-contract/types';
|
|
13
|
+
import type {
|
|
14
|
+
SqlForeignKeyIR,
|
|
15
|
+
SqlIndexIR,
|
|
16
|
+
SqlSchemaIR,
|
|
17
|
+
SqlUniqueIR,
|
|
18
|
+
} from '@prisma-next/sql-schema-ir/types';
|
|
19
|
+
import type { ComponentDatabaseDependency } from '../migrations/types';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Compares two arrays of strings for equality (order-sensitive).
|
|
23
|
+
*/
|
|
24
|
+
export function arraysEqual(a: readonly string[], b: readonly string[]): boolean {
|
|
25
|
+
if (a.length !== b.length) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
for (let i = 0; i < a.length; i++) {
|
|
29
|
+
if (a[i] !== b[i]) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Verifies primary key matches between contract and schema.
|
|
38
|
+
* Returns 'pass' or 'fail'.
|
|
39
|
+
*/
|
|
40
|
+
export function verifyPrimaryKey(
|
|
41
|
+
contractPK: PrimaryKey,
|
|
42
|
+
schemaPK: PrimaryKey | undefined,
|
|
43
|
+
tableName: string,
|
|
44
|
+
issues: SchemaIssue[],
|
|
45
|
+
): 'pass' | 'fail' {
|
|
46
|
+
if (!schemaPK) {
|
|
47
|
+
issues.push({
|
|
48
|
+
kind: 'primary_key_mismatch',
|
|
49
|
+
table: tableName,
|
|
50
|
+
expected: contractPK.columns.join(', '),
|
|
51
|
+
message: `Table "${tableName}" is missing primary key`,
|
|
52
|
+
});
|
|
53
|
+
return 'fail';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
|
|
57
|
+
issues.push({
|
|
58
|
+
kind: 'primary_key_mismatch',
|
|
59
|
+
table: tableName,
|
|
60
|
+
expected: contractPK.columns.join(', '),
|
|
61
|
+
actual: schemaPK.columns.join(', '),
|
|
62
|
+
message: `Table "${tableName}" has primary key mismatch: expected columns [${contractPK.columns.join(', ')}], got [${schemaPK.columns.join(', ')}]`,
|
|
63
|
+
});
|
|
64
|
+
return 'fail';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Compare name if both are modeled
|
|
68
|
+
if (contractPK.name && schemaPK.name && contractPK.name !== schemaPK.name) {
|
|
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
|
+
}
|
|
79
|
+
|
|
80
|
+
return 'pass';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Verifies foreign keys match between contract and schema.
|
|
85
|
+
* Returns verification nodes for the tree.
|
|
86
|
+
*/
|
|
87
|
+
export function verifyForeignKeys(
|
|
88
|
+
contractFKs: readonly ForeignKey[],
|
|
89
|
+
schemaFKs: readonly SqlForeignKeyIR[],
|
|
90
|
+
tableName: string,
|
|
91
|
+
tablePath: string,
|
|
92
|
+
issues: SchemaIssue[],
|
|
93
|
+
strict: boolean,
|
|
94
|
+
): SchemaVerificationNode[] {
|
|
95
|
+
const nodes: SchemaVerificationNode[] = [];
|
|
96
|
+
|
|
97
|
+
// Check each contract FK exists in schema
|
|
98
|
+
for (const contractFK of contractFKs) {
|
|
99
|
+
const fkPath = `${tablePath}.foreignKeys[${contractFK.columns.join(',')}]`;
|
|
100
|
+
const matchingFK = schemaFKs.find((fk) => {
|
|
101
|
+
return (
|
|
102
|
+
arraysEqual(fk.columns, contractFK.columns) &&
|
|
103
|
+
fk.referencedTable === contractFK.references.table &&
|
|
104
|
+
arraysEqual(fk.referencedColumns, contractFK.references.columns)
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!matchingFK) {
|
|
109
|
+
issues.push({
|
|
110
|
+
kind: 'foreign_key_mismatch',
|
|
111
|
+
table: tableName,
|
|
112
|
+
expected: `${contractFK.columns.join(', ')} -> ${contractFK.references.table}(${contractFK.references.columns.join(', ')})`,
|
|
113
|
+
message: `Table "${tableName}" is missing foreign key: ${contractFK.columns.join(', ')} -> ${contractFK.references.table}(${contractFK.references.columns.join(', ')})`,
|
|
114
|
+
});
|
|
115
|
+
nodes.push({
|
|
116
|
+
status: 'fail',
|
|
117
|
+
kind: 'foreignKey',
|
|
118
|
+
name: `foreignKey(${contractFK.columns.join(', ')})`,
|
|
119
|
+
contractPath: fkPath,
|
|
120
|
+
code: 'foreign_key_mismatch',
|
|
121
|
+
message: 'Foreign key missing',
|
|
122
|
+
expected: contractFK,
|
|
123
|
+
actual: undefined,
|
|
124
|
+
children: [],
|
|
125
|
+
});
|
|
126
|
+
} else {
|
|
127
|
+
// Compare name if both are modeled
|
|
128
|
+
if (contractFK.name && matchingFK.name && contractFK.name !== matchingFK.name) {
|
|
129
|
+
issues.push({
|
|
130
|
+
kind: 'foreign_key_mismatch',
|
|
131
|
+
table: tableName,
|
|
132
|
+
indexOrConstraint: contractFK.name,
|
|
133
|
+
expected: contractFK.name,
|
|
134
|
+
actual: matchingFK.name,
|
|
135
|
+
message: `Table "${tableName}" has foreign key name mismatch: expected "${contractFK.name}", got "${matchingFK.name}"`,
|
|
136
|
+
});
|
|
137
|
+
nodes.push({
|
|
138
|
+
status: 'fail',
|
|
139
|
+
kind: 'foreignKey',
|
|
140
|
+
name: `foreignKey(${contractFK.columns.join(', ')})`,
|
|
141
|
+
contractPath: fkPath,
|
|
142
|
+
code: 'foreign_key_mismatch',
|
|
143
|
+
message: 'Foreign key name mismatch',
|
|
144
|
+
expected: contractFK.name,
|
|
145
|
+
actual: matchingFK.name,
|
|
146
|
+
children: [],
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
nodes.push({
|
|
150
|
+
status: 'pass',
|
|
151
|
+
kind: 'foreignKey',
|
|
152
|
+
name: `foreignKey(${contractFK.columns.join(', ')})`,
|
|
153
|
+
contractPath: fkPath,
|
|
154
|
+
code: '',
|
|
155
|
+
message: '',
|
|
156
|
+
expected: undefined,
|
|
157
|
+
actual: undefined,
|
|
158
|
+
children: [],
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check for extra FKs in strict mode
|
|
165
|
+
if (strict) {
|
|
166
|
+
for (const schemaFK of schemaFKs) {
|
|
167
|
+
const matchingFK = contractFKs.find((fk) => {
|
|
168
|
+
return (
|
|
169
|
+
arraysEqual(fk.columns, schemaFK.columns) &&
|
|
170
|
+
fk.references.table === schemaFK.referencedTable &&
|
|
171
|
+
arraysEqual(fk.references.columns, schemaFK.referencedColumns)
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
if (!matchingFK) {
|
|
176
|
+
issues.push({
|
|
177
|
+
kind: 'extra_foreign_key',
|
|
178
|
+
table: tableName,
|
|
179
|
+
message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(', ')} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(', ')})`,
|
|
180
|
+
});
|
|
181
|
+
nodes.push({
|
|
182
|
+
status: 'fail',
|
|
183
|
+
kind: 'foreignKey',
|
|
184
|
+
name: `foreignKey(${schemaFK.columns.join(', ')})`,
|
|
185
|
+
contractPath: `${tablePath}.foreignKeys[${schemaFK.columns.join(',')}]`,
|
|
186
|
+
code: 'extra_foreign_key',
|
|
187
|
+
message: 'Extra foreign key found',
|
|
188
|
+
expected: undefined,
|
|
189
|
+
actual: schemaFK,
|
|
190
|
+
children: [],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return nodes;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Verifies unique constraints match between contract and schema.
|
|
201
|
+
* Returns verification nodes for the tree.
|
|
202
|
+
*/
|
|
203
|
+
export function verifyUniqueConstraints(
|
|
204
|
+
contractUniques: readonly UniqueConstraint[],
|
|
205
|
+
schemaUniques: readonly SqlUniqueIR[],
|
|
206
|
+
tableName: string,
|
|
207
|
+
tablePath: string,
|
|
208
|
+
issues: SchemaIssue[],
|
|
209
|
+
strict: boolean,
|
|
210
|
+
): SchemaVerificationNode[] {
|
|
211
|
+
const nodes: SchemaVerificationNode[] = [];
|
|
212
|
+
|
|
213
|
+
// Check each contract unique exists in schema
|
|
214
|
+
for (const contractUnique of contractUniques) {
|
|
215
|
+
const uniquePath = `${tablePath}.uniques[${contractUnique.columns.join(',')}]`;
|
|
216
|
+
const matchingUnique = schemaUniques.find((u) =>
|
|
217
|
+
arraysEqual(u.columns, contractUnique.columns),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
if (!matchingUnique) {
|
|
221
|
+
issues.push({
|
|
222
|
+
kind: 'unique_constraint_mismatch',
|
|
223
|
+
table: tableName,
|
|
224
|
+
expected: contractUnique.columns.join(', '),
|
|
225
|
+
message: `Table "${tableName}" is missing unique constraint: ${contractUnique.columns.join(', ')}`,
|
|
226
|
+
});
|
|
227
|
+
nodes.push({
|
|
228
|
+
status: 'fail',
|
|
229
|
+
kind: 'unique',
|
|
230
|
+
name: `unique(${contractUnique.columns.join(', ')})`,
|
|
231
|
+
contractPath: uniquePath,
|
|
232
|
+
code: 'unique_constraint_mismatch',
|
|
233
|
+
message: 'Unique constraint missing',
|
|
234
|
+
expected: contractUnique,
|
|
235
|
+
actual: undefined,
|
|
236
|
+
children: [],
|
|
237
|
+
});
|
|
238
|
+
} else {
|
|
239
|
+
// Compare name if both are modeled
|
|
240
|
+
if (
|
|
241
|
+
contractUnique.name &&
|
|
242
|
+
matchingUnique.name &&
|
|
243
|
+
contractUnique.name !== matchingUnique.name
|
|
244
|
+
) {
|
|
245
|
+
issues.push({
|
|
246
|
+
kind: 'unique_constraint_mismatch',
|
|
247
|
+
table: tableName,
|
|
248
|
+
indexOrConstraint: contractUnique.name,
|
|
249
|
+
expected: contractUnique.name,
|
|
250
|
+
actual: matchingUnique.name,
|
|
251
|
+
message: `Table "${tableName}" has unique constraint name mismatch: expected "${contractUnique.name}", got "${matchingUnique.name}"`,
|
|
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
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Check for extra uniques in strict mode
|
|
281
|
+
if (strict) {
|
|
282
|
+
for (const schemaUnique of schemaUniques) {
|
|
283
|
+
const matchingUnique = contractUniques.find((u) =>
|
|
284
|
+
arraysEqual(u.columns, schemaUnique.columns),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (!matchingUnique) {
|
|
288
|
+
issues.push({
|
|
289
|
+
kind: 'extra_unique_constraint',
|
|
290
|
+
table: tableName,
|
|
291
|
+
message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(', ')}`,
|
|
292
|
+
});
|
|
293
|
+
nodes.push({
|
|
294
|
+
status: 'fail',
|
|
295
|
+
kind: 'unique',
|
|
296
|
+
name: `unique(${schemaUnique.columns.join(', ')})`,
|
|
297
|
+
contractPath: `${tablePath}.uniques[${schemaUnique.columns.join(',')}]`,
|
|
298
|
+
code: 'extra_unique_constraint',
|
|
299
|
+
message: 'Extra unique constraint found',
|
|
300
|
+
expected: undefined,
|
|
301
|
+
actual: schemaUnique,
|
|
302
|
+
children: [],
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return nodes;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Verifies indexes match between contract and schema.
|
|
313
|
+
* Returns verification nodes for the tree.
|
|
314
|
+
*/
|
|
315
|
+
export function verifyIndexes(
|
|
316
|
+
contractIndexes: readonly Index[],
|
|
317
|
+
schemaIndexes: readonly SqlIndexIR[],
|
|
318
|
+
tableName: string,
|
|
319
|
+
tablePath: string,
|
|
320
|
+
issues: SchemaIssue[],
|
|
321
|
+
strict: boolean,
|
|
322
|
+
): SchemaVerificationNode[] {
|
|
323
|
+
const nodes: SchemaVerificationNode[] = [];
|
|
324
|
+
|
|
325
|
+
// Check each contract index exists in schema
|
|
326
|
+
for (const contractIndex of contractIndexes) {
|
|
327
|
+
const indexPath = `${tablePath}.indexes[${contractIndex.columns.join(',')}]`;
|
|
328
|
+
const matchingIndex = schemaIndexes.find(
|
|
329
|
+
(idx) => arraysEqual(idx.columns, contractIndex.columns) && idx.unique === false,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
if (!matchingIndex) {
|
|
333
|
+
issues.push({
|
|
334
|
+
kind: 'index_mismatch',
|
|
335
|
+
table: tableName,
|
|
336
|
+
expected: contractIndex.columns.join(', '),
|
|
337
|
+
message: `Table "${tableName}" is missing index: ${contractIndex.columns.join(', ')}`,
|
|
338
|
+
});
|
|
339
|
+
nodes.push({
|
|
340
|
+
status: 'fail',
|
|
341
|
+
kind: 'index',
|
|
342
|
+
name: `index(${contractIndex.columns.join(', ')})`,
|
|
343
|
+
contractPath: indexPath,
|
|
344
|
+
code: 'index_mismatch',
|
|
345
|
+
message: 'Index missing',
|
|
346
|
+
expected: contractIndex,
|
|
347
|
+
actual: undefined,
|
|
348
|
+
children: [],
|
|
349
|
+
});
|
|
350
|
+
} else {
|
|
351
|
+
// Compare name if both are modeled
|
|
352
|
+
if (contractIndex.name && matchingIndex.name && contractIndex.name !== matchingIndex.name) {
|
|
353
|
+
issues.push({
|
|
354
|
+
kind: 'index_mismatch',
|
|
355
|
+
table: tableName,
|
|
356
|
+
indexOrConstraint: contractIndex.name,
|
|
357
|
+
expected: contractIndex.name,
|
|
358
|
+
actual: matchingIndex.name,
|
|
359
|
+
message: `Table "${tableName}" has index name mismatch: expected "${contractIndex.name}", got "${matchingIndex.name}"`,
|
|
360
|
+
});
|
|
361
|
+
nodes.push({
|
|
362
|
+
status: 'fail',
|
|
363
|
+
kind: 'index',
|
|
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
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Check for extra indexes in strict mode
|
|
389
|
+
if (strict) {
|
|
390
|
+
for (const schemaIndex of schemaIndexes) {
|
|
391
|
+
// Skip unique indexes (they're handled as unique constraints)
|
|
392
|
+
if (schemaIndex.unique) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const matchingIndex = contractIndexes.find((idx) =>
|
|
397
|
+
arraysEqual(idx.columns, schemaIndex.columns),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
if (!matchingIndex) {
|
|
401
|
+
issues.push({
|
|
402
|
+
kind: 'extra_index',
|
|
403
|
+
table: tableName,
|
|
404
|
+
message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(', ')}`,
|
|
405
|
+
});
|
|
406
|
+
nodes.push({
|
|
407
|
+
status: 'fail',
|
|
408
|
+
kind: 'index',
|
|
409
|
+
name: `index(${schemaIndex.columns.join(', ')})`,
|
|
410
|
+
contractPath: `${tablePath}.indexes[${schemaIndex.columns.join(',')}]`,
|
|
411
|
+
code: 'extra_index',
|
|
412
|
+
message: 'Extra index found',
|
|
413
|
+
expected: undefined,
|
|
414
|
+
actual: schemaIndex,
|
|
415
|
+
children: [],
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return nodes;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Verifies database dependencies are installed using component-owned verification hooks.
|
|
426
|
+
* Each dependency provides a pure verifyDatabaseDependencyInstalled function that checks
|
|
427
|
+
* whether the dependency is satisfied based on the in-memory schema IR (no DB I/O).
|
|
428
|
+
*
|
|
429
|
+
* Returns verification nodes for the tree.
|
|
430
|
+
*/
|
|
431
|
+
export function verifyDatabaseDependencies(
|
|
432
|
+
dependencies: ReadonlyArray<ComponentDatabaseDependency<unknown>>,
|
|
433
|
+
schema: SqlSchemaIR,
|
|
434
|
+
issues: SchemaIssue[],
|
|
435
|
+
): SchemaVerificationNode[] {
|
|
436
|
+
const nodes: SchemaVerificationNode[] = [];
|
|
437
|
+
|
|
438
|
+
for (const dependency of dependencies) {
|
|
439
|
+
const depIssues = dependency.verifyDatabaseDependencyInstalled(schema);
|
|
440
|
+
const depPath = `dependencies.${dependency.id}`;
|
|
441
|
+
|
|
442
|
+
if (depIssues.length > 0) {
|
|
443
|
+
// Dependency is not satisfied
|
|
444
|
+
issues.push(...depIssues);
|
|
445
|
+
const issuesMessage = depIssues.map((i) => i.message).join('; ');
|
|
446
|
+
const nodeMessage = issuesMessage ? `${dependency.id}: ${issuesMessage}` : dependency.id;
|
|
447
|
+
nodes.push({
|
|
448
|
+
status: 'fail',
|
|
449
|
+
kind: 'databaseDependency',
|
|
450
|
+
name: dependency.label,
|
|
451
|
+
contractPath: depPath,
|
|
452
|
+
code: 'dependency_missing',
|
|
453
|
+
message: nodeMessage,
|
|
454
|
+
expected: undefined,
|
|
455
|
+
actual: undefined,
|
|
456
|
+
children: [],
|
|
457
|
+
});
|
|
458
|
+
} else {
|
|
459
|
+
// Dependency is satisfied
|
|
460
|
+
nodes.push({
|
|
461
|
+
status: 'pass',
|
|
462
|
+
kind: 'databaseDependency',
|
|
463
|
+
name: dependency.label,
|
|
464
|
+
contractPath: depPath,
|
|
465
|
+
code: '',
|
|
466
|
+
message: '',
|
|
467
|
+
expected: undefined,
|
|
468
|
+
actual: undefined,
|
|
469
|
+
children: [],
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return nodes;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Computes counts of pass/warn/fail nodes by traversing the tree.
|
|
479
|
+
*/
|
|
480
|
+
export function computeCounts(node: SchemaVerificationNode): {
|
|
481
|
+
pass: number;
|
|
482
|
+
warn: number;
|
|
483
|
+
fail: number;
|
|
484
|
+
totalNodes: number;
|
|
485
|
+
} {
|
|
486
|
+
let pass = 0;
|
|
487
|
+
let warn = 0;
|
|
488
|
+
let fail = 0;
|
|
489
|
+
|
|
490
|
+
function traverse(n: SchemaVerificationNode): void {
|
|
491
|
+
if (n.status === 'pass') {
|
|
492
|
+
pass++;
|
|
493
|
+
} else if (n.status === 'warn') {
|
|
494
|
+
warn++;
|
|
495
|
+
} else if (n.status === 'fail') {
|
|
496
|
+
fail++;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (n.children) {
|
|
500
|
+
for (const child of n.children) {
|
|
501
|
+
traverse(child);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
traverse(node);
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
pass,
|
|
510
|
+
warn,
|
|
511
|
+
fail,
|
|
512
|
+
totalNodes: pass + warn + fail,
|
|
513
|
+
};
|
|
514
|
+
}
|