@prisma-next/family-sql 0.1.0-dev.15 → 0.1.0-dev.16

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.
@@ -0,0 +1,764 @@
1
+ // src/core/schema-verify/verify-sql-schema.ts
2
+ import { ifDefined } from "@prisma-next/utils/defined";
3
+
4
+ // src/core/schema-verify/verify-helpers.ts
5
+ function arraysEqual(a, b) {
6
+ if (a.length !== b.length) {
7
+ return false;
8
+ }
9
+ for (let i = 0; i < a.length; i++) {
10
+ if (a[i] !== b[i]) {
11
+ return false;
12
+ }
13
+ }
14
+ return true;
15
+ }
16
+ function verifyPrimaryKey(contractPK, schemaPK, tableName, issues) {
17
+ if (!schemaPK) {
18
+ issues.push({
19
+ kind: "primary_key_mismatch",
20
+ table: tableName,
21
+ expected: contractPK.columns.join(", "),
22
+ message: `Table "${tableName}" is missing primary key`
23
+ });
24
+ return "fail";
25
+ }
26
+ if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
27
+ issues.push({
28
+ kind: "primary_key_mismatch",
29
+ table: tableName,
30
+ expected: contractPK.columns.join(", "),
31
+ actual: schemaPK.columns.join(", "),
32
+ message: `Table "${tableName}" has primary key mismatch: expected columns [${contractPK.columns.join(", ")}], got [${schemaPK.columns.join(", ")}]`
33
+ });
34
+ return "fail";
35
+ }
36
+ if (contractPK.name && schemaPK.name && contractPK.name !== schemaPK.name) {
37
+ issues.push({
38
+ kind: "primary_key_mismatch",
39
+ table: tableName,
40
+ indexOrConstraint: contractPK.name,
41
+ expected: contractPK.name,
42
+ actual: schemaPK.name,
43
+ message: `Table "${tableName}" has primary key name mismatch: expected "${contractPK.name}", got "${schemaPK.name}"`
44
+ });
45
+ return "fail";
46
+ }
47
+ return "pass";
48
+ }
49
+ function verifyForeignKeys(contractFKs, schemaFKs, tableName, tablePath, issues, strict) {
50
+ const nodes = [];
51
+ for (const contractFK of contractFKs) {
52
+ const fkPath = `${tablePath}.foreignKeys[${contractFK.columns.join(",")}]`;
53
+ const matchingFK = schemaFKs.find((fk) => {
54
+ return arraysEqual(fk.columns, contractFK.columns) && fk.referencedTable === contractFK.references.table && arraysEqual(fk.referencedColumns, contractFK.references.columns);
55
+ });
56
+ if (!matchingFK) {
57
+ issues.push({
58
+ kind: "foreign_key_mismatch",
59
+ table: tableName,
60
+ expected: `${contractFK.columns.join(", ")} -> ${contractFK.references.table}(${contractFK.references.columns.join(", ")})`,
61
+ message: `Table "${tableName}" is missing foreign key: ${contractFK.columns.join(", ")} -> ${contractFK.references.table}(${contractFK.references.columns.join(", ")})`
62
+ });
63
+ nodes.push({
64
+ status: "fail",
65
+ kind: "foreignKey",
66
+ name: `foreignKey(${contractFK.columns.join(", ")})`,
67
+ contractPath: fkPath,
68
+ code: "foreign_key_mismatch",
69
+ message: "Foreign key missing",
70
+ expected: contractFK,
71
+ actual: void 0,
72
+ children: []
73
+ });
74
+ } else {
75
+ if (contractFK.name && matchingFK.name && contractFK.name !== matchingFK.name) {
76
+ issues.push({
77
+ kind: "foreign_key_mismatch",
78
+ table: tableName,
79
+ indexOrConstraint: contractFK.name,
80
+ expected: contractFK.name,
81
+ actual: matchingFK.name,
82
+ message: `Table "${tableName}" has foreign key name mismatch: expected "${contractFK.name}", got "${matchingFK.name}"`
83
+ });
84
+ nodes.push({
85
+ status: "fail",
86
+ kind: "foreignKey",
87
+ name: `foreignKey(${contractFK.columns.join(", ")})`,
88
+ contractPath: fkPath,
89
+ code: "foreign_key_mismatch",
90
+ message: "Foreign key name mismatch",
91
+ expected: contractFK.name,
92
+ actual: matchingFK.name,
93
+ children: []
94
+ });
95
+ } else {
96
+ nodes.push({
97
+ status: "pass",
98
+ kind: "foreignKey",
99
+ name: `foreignKey(${contractFK.columns.join(", ")})`,
100
+ contractPath: fkPath,
101
+ code: "",
102
+ message: "",
103
+ expected: void 0,
104
+ actual: void 0,
105
+ children: []
106
+ });
107
+ }
108
+ }
109
+ }
110
+ if (strict) {
111
+ for (const schemaFK of schemaFKs) {
112
+ const matchingFK = contractFKs.find((fk) => {
113
+ return arraysEqual(fk.columns, schemaFK.columns) && fk.references.table === schemaFK.referencedTable && arraysEqual(fk.references.columns, schemaFK.referencedColumns);
114
+ });
115
+ if (!matchingFK) {
116
+ issues.push({
117
+ kind: "extra_foreign_key",
118
+ table: tableName,
119
+ message: `Extra foreign key found in database (not in contract): ${schemaFK.columns.join(", ")} -> ${schemaFK.referencedTable}(${schemaFK.referencedColumns.join(", ")})`
120
+ });
121
+ nodes.push({
122
+ status: "fail",
123
+ kind: "foreignKey",
124
+ name: `foreignKey(${schemaFK.columns.join(", ")})`,
125
+ contractPath: `${tablePath}.foreignKeys[${schemaFK.columns.join(",")}]`,
126
+ code: "extra_foreign_key",
127
+ message: "Extra foreign key found",
128
+ expected: void 0,
129
+ actual: schemaFK,
130
+ children: []
131
+ });
132
+ }
133
+ }
134
+ }
135
+ return nodes;
136
+ }
137
+ function verifyUniqueConstraints(contractUniques, schemaUniques, tableName, tablePath, issues, strict) {
138
+ const nodes = [];
139
+ for (const contractUnique of contractUniques) {
140
+ const uniquePath = `${tablePath}.uniques[${contractUnique.columns.join(",")}]`;
141
+ const matchingUnique = schemaUniques.find(
142
+ (u) => arraysEqual(u.columns, contractUnique.columns)
143
+ );
144
+ if (!matchingUnique) {
145
+ issues.push({
146
+ kind: "unique_constraint_mismatch",
147
+ table: tableName,
148
+ expected: contractUnique.columns.join(", "),
149
+ message: `Table "${tableName}" is missing unique constraint: ${contractUnique.columns.join(", ")}`
150
+ });
151
+ nodes.push({
152
+ status: "fail",
153
+ kind: "unique",
154
+ name: `unique(${contractUnique.columns.join(", ")})`,
155
+ contractPath: uniquePath,
156
+ code: "unique_constraint_mismatch",
157
+ message: "Unique constraint missing",
158
+ expected: contractUnique,
159
+ actual: void 0,
160
+ children: []
161
+ });
162
+ } else {
163
+ if (contractUnique.name && matchingUnique.name && contractUnique.name !== matchingUnique.name) {
164
+ issues.push({
165
+ kind: "unique_constraint_mismatch",
166
+ table: tableName,
167
+ indexOrConstraint: contractUnique.name,
168
+ expected: contractUnique.name,
169
+ actual: matchingUnique.name,
170
+ message: `Table "${tableName}" has unique constraint name mismatch: expected "${contractUnique.name}", got "${matchingUnique.name}"`
171
+ });
172
+ nodes.push({
173
+ status: "fail",
174
+ kind: "unique",
175
+ name: `unique(${contractUnique.columns.join(", ")})`,
176
+ contractPath: uniquePath,
177
+ code: "unique_constraint_mismatch",
178
+ message: "Unique constraint name mismatch",
179
+ expected: contractUnique.name,
180
+ actual: matchingUnique.name,
181
+ children: []
182
+ });
183
+ } else {
184
+ nodes.push({
185
+ status: "pass",
186
+ kind: "unique",
187
+ name: `unique(${contractUnique.columns.join(", ")})`,
188
+ contractPath: uniquePath,
189
+ code: "",
190
+ message: "",
191
+ expected: void 0,
192
+ actual: void 0,
193
+ children: []
194
+ });
195
+ }
196
+ }
197
+ }
198
+ if (strict) {
199
+ for (const schemaUnique of schemaUniques) {
200
+ const matchingUnique = contractUniques.find(
201
+ (u) => arraysEqual(u.columns, schemaUnique.columns)
202
+ );
203
+ if (!matchingUnique) {
204
+ issues.push({
205
+ kind: "extra_unique_constraint",
206
+ table: tableName,
207
+ message: `Extra unique constraint found in database (not in contract): ${schemaUnique.columns.join(", ")}`
208
+ });
209
+ nodes.push({
210
+ status: "fail",
211
+ kind: "unique",
212
+ name: `unique(${schemaUnique.columns.join(", ")})`,
213
+ contractPath: `${tablePath}.uniques[${schemaUnique.columns.join(",")}]`,
214
+ code: "extra_unique_constraint",
215
+ message: "Extra unique constraint found",
216
+ expected: void 0,
217
+ actual: schemaUnique,
218
+ children: []
219
+ });
220
+ }
221
+ }
222
+ }
223
+ return nodes;
224
+ }
225
+ function verifyIndexes(contractIndexes, schemaIndexes, tableName, tablePath, issues, strict) {
226
+ const nodes = [];
227
+ for (const contractIndex of contractIndexes) {
228
+ const indexPath = `${tablePath}.indexes[${contractIndex.columns.join(",")}]`;
229
+ const matchingIndex = schemaIndexes.find(
230
+ (idx) => arraysEqual(idx.columns, contractIndex.columns) && idx.unique === false
231
+ );
232
+ if (!matchingIndex) {
233
+ issues.push({
234
+ kind: "index_mismatch",
235
+ table: tableName,
236
+ expected: contractIndex.columns.join(", "),
237
+ message: `Table "${tableName}" is missing index: ${contractIndex.columns.join(", ")}`
238
+ });
239
+ nodes.push({
240
+ status: "fail",
241
+ kind: "index",
242
+ name: `index(${contractIndex.columns.join(", ")})`,
243
+ contractPath: indexPath,
244
+ code: "index_mismatch",
245
+ message: "Index missing",
246
+ expected: contractIndex,
247
+ actual: void 0,
248
+ children: []
249
+ });
250
+ } else {
251
+ if (contractIndex.name && matchingIndex.name && contractIndex.name !== matchingIndex.name) {
252
+ issues.push({
253
+ kind: "index_mismatch",
254
+ table: tableName,
255
+ indexOrConstraint: contractIndex.name,
256
+ expected: contractIndex.name,
257
+ actual: matchingIndex.name,
258
+ message: `Table "${tableName}" has index name mismatch: expected "${contractIndex.name}", got "${matchingIndex.name}"`
259
+ });
260
+ nodes.push({
261
+ status: "fail",
262
+ kind: "index",
263
+ name: `index(${contractIndex.columns.join(", ")})`,
264
+ contractPath: indexPath,
265
+ code: "index_mismatch",
266
+ message: "Index name mismatch",
267
+ expected: contractIndex.name,
268
+ actual: matchingIndex.name,
269
+ children: []
270
+ });
271
+ } else {
272
+ nodes.push({
273
+ status: "pass",
274
+ kind: "index",
275
+ name: `index(${contractIndex.columns.join(", ")})`,
276
+ contractPath: indexPath,
277
+ code: "",
278
+ message: "",
279
+ expected: void 0,
280
+ actual: void 0,
281
+ children: []
282
+ });
283
+ }
284
+ }
285
+ }
286
+ if (strict) {
287
+ for (const schemaIndex of schemaIndexes) {
288
+ if (schemaIndex.unique) {
289
+ continue;
290
+ }
291
+ const matchingIndex = contractIndexes.find(
292
+ (idx) => arraysEqual(idx.columns, schemaIndex.columns)
293
+ );
294
+ if (!matchingIndex) {
295
+ issues.push({
296
+ kind: "extra_index",
297
+ table: tableName,
298
+ message: `Extra index found in database (not in contract): ${schemaIndex.columns.join(", ")}`
299
+ });
300
+ nodes.push({
301
+ status: "fail",
302
+ kind: "index",
303
+ name: `index(${schemaIndex.columns.join(", ")})`,
304
+ contractPath: `${tablePath}.indexes[${schemaIndex.columns.join(",")}]`,
305
+ code: "extra_index",
306
+ message: "Extra index found",
307
+ expected: void 0,
308
+ actual: schemaIndex,
309
+ children: []
310
+ });
311
+ }
312
+ }
313
+ }
314
+ return nodes;
315
+ }
316
+ function verifyExtensions(contractExtensions, schemaExtensions, contractTarget, issues, _strict) {
317
+ const nodes = [];
318
+ if (!contractExtensions) {
319
+ return nodes;
320
+ }
321
+ const contractExtensionNames = Object.keys(contractExtensions).filter(
322
+ (name) => name !== contractTarget
323
+ );
324
+ for (const extName of contractExtensionNames) {
325
+ const extPath = `extensions.${extName}`;
326
+ const normalizedExtName = extName.toLowerCase().replace(/^pg/, "");
327
+ const matchingExt = schemaExtensions.find((e) => {
328
+ const normalizedE = e.toLowerCase();
329
+ if (normalizedE === normalizedExtName || normalizedE === extName.toLowerCase()) {
330
+ return true;
331
+ }
332
+ if (normalizedE.includes(normalizedExtName) || normalizedExtName.includes(normalizedE)) {
333
+ return true;
334
+ }
335
+ return false;
336
+ });
337
+ const extensionLabels = {
338
+ pg: "database is postgres",
339
+ pgvector: "vector extension is enabled",
340
+ vector: "vector extension is enabled"
341
+ };
342
+ const extensionLabel = extensionLabels[extName] ?? `extension "${extName}" is enabled`;
343
+ if (!matchingExt) {
344
+ issues.push({
345
+ kind: "extension_missing",
346
+ table: "",
347
+ message: `Extension "${extName}" is missing from database`
348
+ });
349
+ nodes.push({
350
+ status: "fail",
351
+ kind: "extension",
352
+ name: extensionLabel,
353
+ contractPath: extPath,
354
+ code: "extension_missing",
355
+ message: `Extension "${extName}" is missing`,
356
+ expected: void 0,
357
+ actual: void 0,
358
+ children: []
359
+ });
360
+ } else {
361
+ nodes.push({
362
+ status: "pass",
363
+ kind: "extension",
364
+ name: extensionLabel,
365
+ contractPath: extPath,
366
+ code: "",
367
+ message: "",
368
+ expected: void 0,
369
+ actual: void 0,
370
+ children: []
371
+ });
372
+ }
373
+ }
374
+ return nodes;
375
+ }
376
+ function computeCounts(node) {
377
+ let pass = 0;
378
+ let warn = 0;
379
+ let fail = 0;
380
+ function traverse(n) {
381
+ if (n.status === "pass") {
382
+ pass++;
383
+ } else if (n.status === "warn") {
384
+ warn++;
385
+ } else if (n.status === "fail") {
386
+ fail++;
387
+ }
388
+ if (n.children) {
389
+ for (const child of n.children) {
390
+ traverse(child);
391
+ }
392
+ }
393
+ }
394
+ traverse(node);
395
+ return {
396
+ pass,
397
+ warn,
398
+ fail,
399
+ totalNodes: pass + warn + fail
400
+ };
401
+ }
402
+
403
+ // src/core/schema-verify/verify-sql-schema.ts
404
+ function verifySqlSchema(options) {
405
+ const { contract, schema, strict, context, typeMetadataRegistry } = options;
406
+ const startTime = Date.now();
407
+ const contractCoreHash = contract.coreHash;
408
+ const contractProfileHash = "profileHash" in contract && typeof contract.profileHash === "string" ? contract.profileHash : void 0;
409
+ const contractTarget = contract.target;
410
+ const issues = [];
411
+ const rootChildren = [];
412
+ const contractTables = contract.storage.tables;
413
+ const schemaTables = schema.tables;
414
+ for (const [tableName, contractTable] of Object.entries(contractTables)) {
415
+ const schemaTable = schemaTables[tableName];
416
+ const tablePath = `storage.tables.${tableName}`;
417
+ if (!schemaTable) {
418
+ issues.push({
419
+ kind: "missing_table",
420
+ table: tableName,
421
+ message: `Table "${tableName}" is missing from database`
422
+ });
423
+ rootChildren.push({
424
+ status: "fail",
425
+ kind: "table",
426
+ name: `table ${tableName}`,
427
+ contractPath: tablePath,
428
+ code: "missing_table",
429
+ message: `Table "${tableName}" is missing`,
430
+ expected: void 0,
431
+ actual: void 0,
432
+ children: []
433
+ });
434
+ continue;
435
+ }
436
+ const tableChildren = [];
437
+ const columnNodes = [];
438
+ for (const [columnName, contractColumn] of Object.entries(contractTable.columns)) {
439
+ const schemaColumn = schemaTable.columns[columnName];
440
+ const columnPath = `${tablePath}.columns.${columnName}`;
441
+ if (!schemaColumn) {
442
+ issues.push({
443
+ kind: "missing_column",
444
+ table: tableName,
445
+ column: columnName,
446
+ message: `Column "${tableName}"."${columnName}" is missing from database`
447
+ });
448
+ columnNodes.push({
449
+ status: "fail",
450
+ kind: "column",
451
+ name: `${columnName}: missing`,
452
+ contractPath: columnPath,
453
+ code: "missing_column",
454
+ message: `Column "${columnName}" is missing`,
455
+ expected: void 0,
456
+ actual: void 0,
457
+ children: []
458
+ });
459
+ continue;
460
+ }
461
+ const columnChildren = [];
462
+ let columnStatus = "pass";
463
+ const contractNativeType = contractColumn.nativeType;
464
+ const schemaNativeType = schemaColumn.nativeType;
465
+ if (contractNativeType !== schemaNativeType) {
466
+ issues.push({
467
+ kind: "type_mismatch",
468
+ table: tableName,
469
+ column: columnName,
470
+ expected: contractNativeType,
471
+ actual: schemaNativeType,
472
+ message: `Column "${tableName}"."${columnName}" has type mismatch: expected "${contractNativeType}", got "${schemaNativeType}"`
473
+ });
474
+ columnChildren.push({
475
+ status: "fail",
476
+ kind: "type",
477
+ name: "type",
478
+ contractPath: `${columnPath}.nativeType`,
479
+ code: "type_mismatch",
480
+ message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
481
+ expected: contractNativeType,
482
+ actual: schemaNativeType,
483
+ children: []
484
+ });
485
+ columnStatus = "fail";
486
+ }
487
+ if (contractColumn.codecId) {
488
+ const typeMetadata = typeMetadataRegistry.get(contractColumn.codecId);
489
+ if (!typeMetadata) {
490
+ columnChildren.push({
491
+ status: "warn",
492
+ kind: "type",
493
+ name: "type_metadata_missing",
494
+ contractPath: `${columnPath}.codecId`,
495
+ code: "type_metadata_missing",
496
+ message: `codecId "${contractColumn.codecId}" not found in type metadata registry`,
497
+ expected: contractColumn.codecId,
498
+ actual: void 0,
499
+ children: []
500
+ });
501
+ } else if (typeMetadata.nativeType && typeMetadata.nativeType !== contractNativeType) {
502
+ columnChildren.push({
503
+ status: "warn",
504
+ kind: "type",
505
+ name: "type_consistency",
506
+ contractPath: `${columnPath}.codecId`,
507
+ code: "type_consistency_warning",
508
+ message: `codecId "${contractColumn.codecId}" maps to nativeType "${typeMetadata.nativeType}" in registry, but contract has "${contractNativeType}"`,
509
+ expected: typeMetadata.nativeType,
510
+ actual: contractNativeType,
511
+ children: []
512
+ });
513
+ }
514
+ }
515
+ if (contractColumn.nullable !== schemaColumn.nullable) {
516
+ issues.push({
517
+ kind: "nullability_mismatch",
518
+ table: tableName,
519
+ column: columnName,
520
+ expected: String(contractColumn.nullable),
521
+ actual: String(schemaColumn.nullable),
522
+ message: `Column "${tableName}"."${columnName}" has nullability mismatch: expected ${contractColumn.nullable ? "nullable" : "not null"}, got ${schemaColumn.nullable ? "nullable" : "not null"}`
523
+ });
524
+ columnChildren.push({
525
+ status: "fail",
526
+ kind: "nullability",
527
+ name: "nullability",
528
+ contractPath: `${columnPath}.nullable`,
529
+ code: "nullability_mismatch",
530
+ message: `Nullability mismatch: expected ${contractColumn.nullable ? "nullable" : "not null"}, got ${schemaColumn.nullable ? "nullable" : "not null"}`,
531
+ expected: contractColumn.nullable,
532
+ actual: schemaColumn.nullable,
533
+ children: []
534
+ });
535
+ columnStatus = "fail";
536
+ }
537
+ const computedColumnStatus = columnChildren.some((c) => c.status === "fail") ? "fail" : columnChildren.some((c) => c.status === "warn") ? "warn" : "pass";
538
+ const finalColumnStatus = columnChildren.length > 0 ? computedColumnStatus : columnStatus;
539
+ const nullableText = contractColumn.nullable ? "nullable" : "not nullable";
540
+ const columnTypeDisplay = contractColumn.codecId ? `${contractNativeType} (${contractColumn.codecId})` : contractNativeType;
541
+ const failureMessages = columnChildren.filter((child) => child.status === "fail" && child.message).map((child) => child.message).filter((msg) => typeof msg === "string" && msg.length > 0);
542
+ const columnMessage = finalColumnStatus === "fail" && failureMessages.length > 0 ? failureMessages.join("; ") : "";
543
+ const columnCode = (finalColumnStatus === "fail" || finalColumnStatus === "warn") && columnChildren[0] ? columnChildren[0].code : "";
544
+ columnNodes.push({
545
+ status: finalColumnStatus,
546
+ kind: "column",
547
+ name: `${columnName}: ${columnTypeDisplay} (${nullableText})`,
548
+ contractPath: columnPath,
549
+ code: columnCode,
550
+ message: columnMessage,
551
+ expected: void 0,
552
+ actual: void 0,
553
+ children: columnChildren
554
+ });
555
+ }
556
+ if (columnNodes.length > 0) {
557
+ const columnsStatus = columnNodes.some((c) => c.status === "fail") ? "fail" : columnNodes.some((c) => c.status === "warn") ? "warn" : "pass";
558
+ tableChildren.push({
559
+ status: columnsStatus,
560
+ kind: "columns",
561
+ name: "columns",
562
+ contractPath: `${tablePath}.columns`,
563
+ code: "",
564
+ message: "",
565
+ expected: void 0,
566
+ actual: void 0,
567
+ children: columnNodes
568
+ });
569
+ }
570
+ if (strict) {
571
+ for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) {
572
+ if (!contractTable.columns[columnName]) {
573
+ issues.push({
574
+ kind: "extra_column",
575
+ table: tableName,
576
+ column: columnName,
577
+ message: `Extra column "${tableName}"."${columnName}" found in database (not in contract)`
578
+ });
579
+ columnNodes.push({
580
+ status: "fail",
581
+ kind: "column",
582
+ name: `${columnName}: extra`,
583
+ contractPath: `${tablePath}.columns.${columnName}`,
584
+ code: "extra_column",
585
+ message: `Extra column "${columnName}" found`,
586
+ expected: void 0,
587
+ actual: nativeType,
588
+ children: []
589
+ });
590
+ }
591
+ }
592
+ }
593
+ if (contractTable.primaryKey) {
594
+ const pkStatus = verifyPrimaryKey(
595
+ contractTable.primaryKey,
596
+ schemaTable.primaryKey,
597
+ tableName,
598
+ issues
599
+ );
600
+ if (pkStatus === "fail") {
601
+ tableChildren.push({
602
+ status: "fail",
603
+ kind: "primaryKey",
604
+ name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
605
+ contractPath: `${tablePath}.primaryKey`,
606
+ code: "primary_key_mismatch",
607
+ message: "Primary key mismatch",
608
+ expected: contractTable.primaryKey,
609
+ actual: schemaTable.primaryKey,
610
+ children: []
611
+ });
612
+ } else {
613
+ tableChildren.push({
614
+ status: "pass",
615
+ kind: "primaryKey",
616
+ name: `primary key: ${contractTable.primaryKey.columns.join(", ")}`,
617
+ contractPath: `${tablePath}.primaryKey`,
618
+ code: "",
619
+ message: "",
620
+ expected: void 0,
621
+ actual: void 0,
622
+ children: []
623
+ });
624
+ }
625
+ } else if (schemaTable.primaryKey && strict) {
626
+ issues.push({
627
+ kind: "extra_primary_key",
628
+ table: tableName,
629
+ message: "Extra primary key found in database (not in contract)"
630
+ });
631
+ tableChildren.push({
632
+ status: "fail",
633
+ kind: "primaryKey",
634
+ name: `primary key: ${schemaTable.primaryKey.columns.join(", ")}`,
635
+ contractPath: `${tablePath}.primaryKey`,
636
+ code: "extra_primary_key",
637
+ message: "Extra primary key found",
638
+ expected: void 0,
639
+ actual: schemaTable.primaryKey,
640
+ children: []
641
+ });
642
+ }
643
+ const fkStatuses = verifyForeignKeys(
644
+ contractTable.foreignKeys,
645
+ schemaTable.foreignKeys,
646
+ tableName,
647
+ tablePath,
648
+ issues,
649
+ strict
650
+ );
651
+ tableChildren.push(...fkStatuses);
652
+ const uniqueStatuses = verifyUniqueConstraints(
653
+ contractTable.uniques,
654
+ schemaTable.uniques,
655
+ tableName,
656
+ tablePath,
657
+ issues,
658
+ strict
659
+ );
660
+ tableChildren.push(...uniqueStatuses);
661
+ const indexStatuses = verifyIndexes(
662
+ contractTable.indexes,
663
+ schemaTable.indexes,
664
+ tableName,
665
+ tablePath,
666
+ issues,
667
+ strict
668
+ );
669
+ tableChildren.push(...indexStatuses);
670
+ const tableStatus = tableChildren.some((c) => c.status === "fail") ? "fail" : tableChildren.some((c) => c.status === "warn") ? "warn" : "pass";
671
+ const tableFailureMessages = tableChildren.filter((child) => child.status === "fail" && child.message).map((child) => child.message).filter((msg) => typeof msg === "string" && msg.length > 0);
672
+ const tableMessage = tableStatus === "fail" && tableFailureMessages.length > 0 ? `${tableFailureMessages.length} issue${tableFailureMessages.length === 1 ? "" : "s"}` : "";
673
+ const tableCode = tableStatus === "fail" && tableChildren.length > 0 && tableChildren[0] ? tableChildren[0].code : "";
674
+ rootChildren.push({
675
+ status: tableStatus,
676
+ kind: "table",
677
+ name: `table ${tableName}`,
678
+ contractPath: tablePath,
679
+ code: tableCode,
680
+ message: tableMessage,
681
+ expected: void 0,
682
+ actual: void 0,
683
+ children: tableChildren
684
+ });
685
+ }
686
+ if (strict) {
687
+ for (const tableName of Object.keys(schemaTables)) {
688
+ if (!contractTables[tableName]) {
689
+ issues.push({
690
+ kind: "extra_table",
691
+ table: tableName,
692
+ message: `Extra table "${tableName}" found in database (not in contract)`
693
+ });
694
+ rootChildren.push({
695
+ status: "fail",
696
+ kind: "table",
697
+ name: `table ${tableName}`,
698
+ contractPath: `storage.tables.${tableName}`,
699
+ code: "extra_table",
700
+ message: `Extra table "${tableName}" found`,
701
+ expected: void 0,
702
+ actual: void 0,
703
+ children: []
704
+ });
705
+ }
706
+ }
707
+ }
708
+ const extensionStatuses = verifyExtensions(
709
+ contract.extensions,
710
+ schema.extensions,
711
+ contractTarget,
712
+ issues,
713
+ strict
714
+ );
715
+ rootChildren.push(...extensionStatuses);
716
+ const rootStatus = rootChildren.some((c) => c.status === "fail") ? "fail" : rootChildren.some((c) => c.status === "warn") ? "warn" : "pass";
717
+ const root = {
718
+ status: rootStatus,
719
+ kind: "contract",
720
+ name: "contract",
721
+ contractPath: "",
722
+ code: "",
723
+ message: "",
724
+ expected: void 0,
725
+ actual: void 0,
726
+ children: rootChildren
727
+ };
728
+ const counts = computeCounts(root);
729
+ const ok = counts.fail === 0;
730
+ const code = ok ? void 0 : "PN-SCHEMA-0001";
731
+ const summary = ok ? "Database schema satisfies contract" : `Database schema does not satisfy contract (${counts.fail} failure${counts.fail === 1 ? "" : "s"})`;
732
+ const totalTime = Date.now() - startTime;
733
+ return {
734
+ ok,
735
+ ...ifDefined("code", code),
736
+ summary,
737
+ contract: {
738
+ coreHash: contractCoreHash,
739
+ ...ifDefined("profileHash", contractProfileHash)
740
+ },
741
+ target: {
742
+ expected: contractTarget,
743
+ actual: contractTarget
744
+ },
745
+ schema: {
746
+ issues,
747
+ root,
748
+ counts
749
+ },
750
+ meta: {
751
+ strict,
752
+ ...ifDefined("contractPath", context?.contractPath),
753
+ ...ifDefined("configPath", context?.configPath)
754
+ },
755
+ timings: {
756
+ total: totalTime
757
+ }
758
+ };
759
+ }
760
+
761
+ export {
762
+ verifySqlSchema
763
+ };
764
+ //# sourceMappingURL=chunk-I7QWTBWS.js.map