@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.
Files changed (68) hide show
  1. package/dist/{exports/chunk-6P44BVZ4.js → chunk-6K3RPBDP.js} +3 -3
  2. package/dist/chunk-6K3RPBDP.js.map +1 -0
  3. package/dist/{exports/chunk-F252JMEU.js → chunk-BHEGVBY7.js} +1 -1
  4. package/dist/chunk-BHEGVBY7.js.map +1 -0
  5. package/dist/{exports/chunk-C3GKWCKA.js → chunk-SU7LN2UH.js} +1 -1
  6. package/dist/chunk-SU7LN2UH.js.map +1 -0
  7. package/dist/core/assembly.d.ts +25 -0
  8. package/dist/core/assembly.d.ts.map +1 -0
  9. package/dist/core/control-adapter.d.ts +42 -0
  10. package/dist/core/control-adapter.d.ts.map +1 -0
  11. package/dist/core/descriptor.d.ts +31 -0
  12. package/dist/core/descriptor.d.ts.map +1 -0
  13. package/dist/{exports/instance-DiZi2k_2.d.ts → core/instance.d.ts} +30 -15
  14. package/dist/core/instance.d.ts.map +1 -0
  15. package/dist/core/migrations/plan-helpers.d.ts +20 -0
  16. package/dist/core/migrations/plan-helpers.d.ts.map +1 -0
  17. package/dist/core/migrations/policies.d.ts +6 -0
  18. package/dist/core/migrations/policies.d.ts.map +1 -0
  19. package/dist/{exports/types-Bh7ftf0Q.d.ts → core/migrations/types.d.ts} +41 -36
  20. package/dist/core/migrations/types.d.ts.map +1 -0
  21. package/dist/core/runtime-descriptor.d.ts +19 -0
  22. package/dist/core/runtime-descriptor.d.ts.map +1 -0
  23. package/dist/core/runtime-instance.d.ts +54 -0
  24. package/dist/core/runtime-instance.d.ts.map +1 -0
  25. package/dist/core/schema-verify/verify-helpers.d.ts +50 -0
  26. package/dist/core/schema-verify/verify-helpers.d.ts.map +1 -0
  27. package/dist/core/schema-verify/verify-sql-schema.d.ts +45 -0
  28. package/dist/core/schema-verify/verify-sql-schema.d.ts.map +1 -0
  29. package/dist/core/verify.d.ts +39 -0
  30. package/dist/core/verify.d.ts.map +1 -0
  31. package/dist/exports/control-adapter.d.ts +2 -44
  32. package/dist/exports/control-adapter.d.ts.map +1 -0
  33. package/dist/exports/control.d.ts +8 -70
  34. package/dist/exports/control.d.ts.map +1 -0
  35. package/dist/exports/control.js +3 -3
  36. package/dist/exports/runtime.d.ts +3 -61
  37. package/dist/exports/runtime.d.ts.map +1 -0
  38. package/dist/exports/schema-verify.d.ts +8 -72
  39. package/dist/exports/schema-verify.d.ts.map +1 -0
  40. package/dist/exports/schema-verify.js +1 -1
  41. package/dist/exports/test-utils.d.ts +5 -31
  42. package/dist/exports/test-utils.d.ts.map +1 -0
  43. package/dist/exports/test-utils.js +3 -3
  44. package/dist/exports/verify.d.ts +2 -28
  45. package/dist/exports/verify.d.ts.map +1 -0
  46. package/dist/exports/verify.js +1 -1
  47. package/package.json +19 -18
  48. package/src/core/assembly.ts +117 -0
  49. package/src/core/control-adapter.ts +52 -0
  50. package/src/core/descriptor.ts +37 -0
  51. package/src/core/instance.ts +887 -0
  52. package/src/core/migrations/plan-helpers.ts +164 -0
  53. package/src/core/migrations/policies.ts +8 -0
  54. package/src/core/migrations/types.ts +380 -0
  55. package/src/core/runtime-descriptor.ts +45 -0
  56. package/src/core/runtime-instance.ts +144 -0
  57. package/src/core/schema-verify/verify-helpers.ts +514 -0
  58. package/src/core/schema-verify/verify-sql-schema.ts +584 -0
  59. package/src/core/verify.ts +177 -0
  60. package/src/exports/control-adapter.ts +1 -0
  61. package/src/exports/control.ts +56 -0
  62. package/src/exports/runtime.ts +7 -0
  63. package/src/exports/schema-verify.ts +11 -0
  64. package/src/exports/test-utils.ts +11 -0
  65. package/src/exports/verify.ts +1 -0
  66. package/dist/exports/chunk-6P44BVZ4.js.map +0 -1
  67. package/dist/exports/chunk-C3GKWCKA.js.map +0 -1
  68. 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
+ }