@prisma-next/family-sql 0.3.0-dev.2 → 0.3.0-dev.21

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 (69) hide show
  1. package/dist/{exports/chunk-6P44BVZ4.js → chunk-F27CR6XZ.js} +12 -3
  2. package/dist/chunk-F27CR6XZ.js.map +1 -0
  3. package/dist/{exports/chunk-C3GKWCKA.js → chunk-SU7LN2UH.js} +1 -1
  4. package/dist/chunk-SU7LN2UH.js.map +1 -0
  5. package/dist/{exports/chunk-F252JMEU.js → chunk-XH2Y5NTD.js} +59 -116
  6. package/dist/chunk-XH2Y5NTD.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 +24 -0
  12. package/dist/core/descriptor.d.ts.map +1 -0
  13. package/dist/{exports/instance-DiZi2k_2.d.ts → core/instance.d.ts} +28 -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 +96 -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 -160
  34. package/dist/exports/control.d.ts.map +1 -0
  35. package/dist/exports/control.js +7 -7
  36. package/dist/exports/control.js.map +1 -1
  37. package/dist/exports/runtime.d.ts +3 -61
  38. package/dist/exports/runtime.d.ts.map +1 -0
  39. package/dist/exports/schema-verify.d.ts +8 -72
  40. package/dist/exports/schema-verify.d.ts.map +1 -0
  41. package/dist/exports/schema-verify.js +5 -1
  42. package/dist/exports/test-utils.d.ts +5 -31
  43. package/dist/exports/test-utils.d.ts.map +1 -0
  44. package/dist/exports/test-utils.js +3 -3
  45. package/dist/exports/verify.d.ts +2 -28
  46. package/dist/exports/verify.d.ts.map +1 -0
  47. package/dist/exports/verify.js +1 -1
  48. package/package.json +24 -25
  49. package/src/core/assembly.ts +117 -0
  50. package/src/core/control-adapter.ts +52 -0
  51. package/src/core/descriptor.ts +33 -0
  52. package/src/core/instance.ts +903 -0
  53. package/src/core/migrations/plan-helpers.ts +164 -0
  54. package/src/core/migrations/policies.ts +8 -0
  55. package/src/core/migrations/types.ts +380 -0
  56. package/src/core/runtime-descriptor.ts +45 -0
  57. package/src/core/runtime-instance.ts +144 -0
  58. package/src/core/schema-verify/verify-helpers.ts +532 -0
  59. package/src/core/schema-verify/verify-sql-schema.ts +588 -0
  60. package/src/core/verify.ts +177 -0
  61. package/src/exports/control-adapter.ts +1 -0
  62. package/src/exports/control.ts +56 -0
  63. package/src/exports/runtime.ts +7 -0
  64. package/src/exports/schema-verify.ts +16 -0
  65. package/src/exports/test-utils.ts +11 -0
  66. package/src/exports/verify.ts +1 -0
  67. package/dist/exports/chunk-6P44BVZ4.js.map +0 -1
  68. package/dist/exports/chunk-C3GKWCKA.js.map +0 -1
  69. package/dist/exports/chunk-F252JMEU.js.map +0 -1
@@ -0,0 +1,588 @@
1
+ /**
2
+ * Pure SQL schema verification function.
3
+ *
4
+ * This module provides a pure function that verifies a SqlSchemaIR against
5
+ * a SqlContract without requiring a database connection. It can be reused
6
+ * by migration planners and other tools that need to compare schema states.
7
+ */
8
+
9
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/contract/framework-components';
10
+ import type {
11
+ OperationContext,
12
+ SchemaIssue,
13
+ SchemaVerificationNode,
14
+ VerifyDatabaseSchemaResult,
15
+ } from '@prisma-next/core-control-plane/types';
16
+ import type { SqlContract, SqlStorage } from '@prisma-next/sql-contract/types';
17
+ import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
18
+ import { ifDefined } from '@prisma-next/utils/defined';
19
+ import type { ComponentDatabaseDependency } from '../migrations/types';
20
+ import {
21
+ computeCounts,
22
+ verifyDatabaseDependencies,
23
+ verifyForeignKeys,
24
+ verifyIndexes,
25
+ verifyPrimaryKey,
26
+ verifyUniqueConstraints,
27
+ } from './verify-helpers';
28
+
29
+ /**
30
+ * Options for the pure schema verification function.
31
+ */
32
+ export interface VerifySqlSchemaOptions {
33
+ /** The validated SQL contract to verify against */
34
+ readonly contract: SqlContract<SqlStorage>;
35
+ /** The schema IR from introspection (or another source) */
36
+ readonly schema: SqlSchemaIR;
37
+ /** Whether to run in strict mode (detects extra tables/columns) */
38
+ readonly strict: boolean;
39
+ /** Optional operation context for metadata */
40
+ readonly context?: OperationContext;
41
+ /** Type metadata registry for codec consistency warnings */
42
+ readonly typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
43
+ /**
44
+ * Active framework components participating in this composition.
45
+ * All components must have matching familyId ('sql') and targetId.
46
+ */
47
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
48
+ }
49
+
50
+ /**
51
+ * Verifies that a SqlSchemaIR matches a SqlContract.
52
+ *
53
+ * This is a pure function that does NOT perform any database I/O.
54
+ * It takes an already-introspected schema IR and compares it against
55
+ * the contract requirements.
56
+ *
57
+ * @param options - Verification options
58
+ * @returns VerifyDatabaseSchemaResult with verification tree and issues
59
+ */
60
+ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabaseSchemaResult {
61
+ const { contract, schema, strict, context, typeMetadataRegistry } = options;
62
+ const startTime = Date.now();
63
+
64
+ // Extract contract hashes and target
65
+ const contractCoreHash = contract.coreHash;
66
+ const contractProfileHash =
67
+ 'profileHash' in contract && typeof contract.profileHash === 'string'
68
+ ? contract.profileHash
69
+ : undefined;
70
+ const contractTarget = contract.target;
71
+
72
+ // Compare contract vs schema IR
73
+ const issues: SchemaIssue[] = [];
74
+ const rootChildren: SchemaVerificationNode[] = [];
75
+
76
+ // Compare tables
77
+ const contractTables = contract.storage.tables;
78
+ const schemaTables = schema.tables;
79
+
80
+ for (const [tableName, contractTable] of Object.entries(contractTables)) {
81
+ const schemaTable = schemaTables[tableName];
82
+ const tablePath = `storage.tables.${tableName}`;
83
+
84
+ if (!schemaTable) {
85
+ // Missing table
86
+ issues.push({
87
+ kind: 'missing_table',
88
+ table: tableName,
89
+ message: `Table "${tableName}" is missing from database`,
90
+ });
91
+ rootChildren.push({
92
+ status: 'fail',
93
+ kind: 'table',
94
+ name: `table ${tableName}`,
95
+ contractPath: tablePath,
96
+ code: 'missing_table',
97
+ message: `Table "${tableName}" is missing`,
98
+ expected: undefined,
99
+ actual: undefined,
100
+ children: [],
101
+ });
102
+ continue;
103
+ }
104
+
105
+ // Table exists - compare columns, constraints, etc.
106
+ const tableChildren: SchemaVerificationNode[] = [];
107
+ const columnNodes: SchemaVerificationNode[] = [];
108
+
109
+ // Compare columns
110
+ for (const [columnName, contractColumn] of Object.entries(contractTable.columns)) {
111
+ const schemaColumn = schemaTable.columns[columnName];
112
+ const columnPath = `${tablePath}.columns.${columnName}`;
113
+
114
+ if (!schemaColumn) {
115
+ // Missing column
116
+ issues.push({
117
+ kind: 'missing_column',
118
+ table: tableName,
119
+ column: columnName,
120
+ message: `Column "${tableName}"."${columnName}" is missing from database`,
121
+ });
122
+ columnNodes.push({
123
+ status: 'fail',
124
+ kind: 'column',
125
+ name: `${columnName}: missing`,
126
+ contractPath: columnPath,
127
+ code: 'missing_column',
128
+ message: `Column "${columnName}" is missing`,
129
+ expected: undefined,
130
+ actual: undefined,
131
+ children: [],
132
+ });
133
+ continue;
134
+ }
135
+
136
+ // Column exists - compare type and nullability
137
+ const columnChildren: SchemaVerificationNode[] = [];
138
+ let columnStatus: 'pass' | 'warn' | 'fail' = 'pass';
139
+
140
+ // Compare type using nativeType directly
141
+ // Both contractColumn.nativeType and schemaColumn.nativeType are required by their types
142
+ const contractNativeType = contractColumn.nativeType;
143
+ const schemaNativeType = schemaColumn.nativeType;
144
+
145
+ if (contractNativeType !== schemaNativeType) {
146
+ // Compare native types directly
147
+ issues.push({
148
+ kind: 'type_mismatch',
149
+ table: tableName,
150
+ column: columnName,
151
+ expected: contractNativeType,
152
+ actual: schemaNativeType,
153
+ message: `Column "${tableName}"."${columnName}" has type mismatch: expected "${contractNativeType}", got "${schemaNativeType}"`,
154
+ });
155
+ columnChildren.push({
156
+ status: 'fail',
157
+ kind: 'type',
158
+ name: 'type',
159
+ contractPath: `${columnPath}.nativeType`,
160
+ code: 'type_mismatch',
161
+ message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
162
+ expected: contractNativeType,
163
+ actual: schemaNativeType,
164
+ children: [],
165
+ });
166
+ columnStatus = 'fail';
167
+ }
168
+
169
+ // Optionally validate that codecId (if present) and nativeType agree with registry
170
+ if (contractColumn.codecId) {
171
+ const typeMetadata = typeMetadataRegistry.get(contractColumn.codecId);
172
+ if (!typeMetadata) {
173
+ // Warning: codecId not found in registry
174
+ columnChildren.push({
175
+ status: 'warn',
176
+ kind: 'type',
177
+ name: 'type_metadata_missing',
178
+ contractPath: `${columnPath}.codecId`,
179
+ code: 'type_metadata_missing',
180
+ message: `codecId "${contractColumn.codecId}" not found in type metadata registry`,
181
+ expected: contractColumn.codecId,
182
+ actual: undefined,
183
+ children: [],
184
+ });
185
+ } else if (typeMetadata.nativeType && typeMetadata.nativeType !== contractNativeType) {
186
+ // Warning: codecId and nativeType don't agree with registry
187
+ columnChildren.push({
188
+ status: 'warn',
189
+ kind: 'type',
190
+ name: 'type_consistency',
191
+ contractPath: `${columnPath}.codecId`,
192
+ code: 'type_consistency_warning',
193
+ message: `codecId "${contractColumn.codecId}" maps to nativeType "${typeMetadata.nativeType}" in registry, but contract has "${contractNativeType}"`,
194
+ expected: typeMetadata.nativeType,
195
+ actual: contractNativeType,
196
+ children: [],
197
+ });
198
+ }
199
+ }
200
+
201
+ // Compare nullability
202
+ if (contractColumn.nullable !== schemaColumn.nullable) {
203
+ issues.push({
204
+ kind: 'nullability_mismatch',
205
+ table: tableName,
206
+ column: columnName,
207
+ expected: String(contractColumn.nullable),
208
+ actual: String(schemaColumn.nullable),
209
+ message: `Column "${tableName}"."${columnName}" has nullability mismatch: expected ${contractColumn.nullable ? 'nullable' : 'not null'}, got ${schemaColumn.nullable ? 'nullable' : 'not null'}`,
210
+ });
211
+ columnChildren.push({
212
+ status: 'fail',
213
+ kind: 'nullability',
214
+ name: 'nullability',
215
+ contractPath: `${columnPath}.nullable`,
216
+ code: 'nullability_mismatch',
217
+ message: `Nullability mismatch: expected ${contractColumn.nullable ? 'nullable' : 'not null'}, got ${schemaColumn.nullable ? 'nullable' : 'not null'}`,
218
+ expected: contractColumn.nullable,
219
+ actual: schemaColumn.nullable,
220
+ children: [],
221
+ });
222
+ columnStatus = 'fail';
223
+ }
224
+
225
+ // Compute column status from children (fail > warn > pass)
226
+ const computedColumnStatus = columnChildren.some((c) => c.status === 'fail')
227
+ ? 'fail'
228
+ : columnChildren.some((c) => c.status === 'warn')
229
+ ? 'warn'
230
+ : 'pass';
231
+ // Use computed status if we have children, otherwise use the manually set status
232
+ const finalColumnStatus = columnChildren.length > 0 ? computedColumnStatus : columnStatus;
233
+
234
+ // Build column node
235
+ const nullableText = contractColumn.nullable ? 'nullable' : 'not nullable';
236
+ const columnTypeDisplay = contractColumn.codecId
237
+ ? `${contractNativeType} (${contractColumn.codecId})`
238
+ : contractNativeType;
239
+ // Collect failure messages from children to create a summary message
240
+ const failureMessages = columnChildren
241
+ .filter((child) => child.status === 'fail' && child.message)
242
+ .map((child) => child.message)
243
+ .filter((msg): msg is string => typeof msg === 'string' && msg.length > 0);
244
+ const columnMessage =
245
+ finalColumnStatus === 'fail' && failureMessages.length > 0
246
+ ? failureMessages.join('; ')
247
+ : '';
248
+ // Extract code from first child if status indicates an issue
249
+ const columnCode =
250
+ (finalColumnStatus === 'fail' || finalColumnStatus === 'warn') && columnChildren[0]
251
+ ? columnChildren[0].code
252
+ : '';
253
+ columnNodes.push({
254
+ status: finalColumnStatus,
255
+ kind: 'column',
256
+ name: `${columnName}: ${columnTypeDisplay} (${nullableText})`,
257
+ contractPath: columnPath,
258
+ code: columnCode,
259
+ message: columnMessage,
260
+ expected: undefined,
261
+ actual: undefined,
262
+ children: columnChildren,
263
+ });
264
+ }
265
+
266
+ // Group columns under a "columns" header if we have any columns
267
+ if (columnNodes.length > 0) {
268
+ const columnsStatus = columnNodes.some((c) => c.status === 'fail')
269
+ ? 'fail'
270
+ : columnNodes.some((c) => c.status === 'warn')
271
+ ? 'warn'
272
+ : 'pass';
273
+ tableChildren.push({
274
+ status: columnsStatus,
275
+ kind: 'columns',
276
+ name: 'columns',
277
+ contractPath: `${tablePath}.columns`,
278
+ code: '',
279
+ message: '',
280
+ expected: undefined,
281
+ actual: undefined,
282
+ children: columnNodes,
283
+ });
284
+ }
285
+
286
+ // Check for extra columns in strict mode
287
+ if (strict) {
288
+ for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) {
289
+ if (!contractTable.columns[columnName]) {
290
+ issues.push({
291
+ kind: 'extra_column',
292
+ table: tableName,
293
+ column: columnName,
294
+ message: `Extra column "${tableName}"."${columnName}" found in database (not in contract)`,
295
+ });
296
+ columnNodes.push({
297
+ status: 'fail',
298
+ kind: 'column',
299
+ name: `${columnName}: extra`,
300
+ contractPath: `${tablePath}.columns.${columnName}`,
301
+ code: 'extra_column',
302
+ message: `Extra column "${columnName}" found`,
303
+ expected: undefined,
304
+ actual: nativeType,
305
+ children: [],
306
+ });
307
+ }
308
+ }
309
+ }
310
+
311
+ // Compare primary key
312
+ if (contractTable.primaryKey) {
313
+ const pkStatus = verifyPrimaryKey(
314
+ contractTable.primaryKey,
315
+ schemaTable.primaryKey,
316
+ tableName,
317
+ issues,
318
+ );
319
+ if (pkStatus === 'fail') {
320
+ tableChildren.push({
321
+ status: 'fail',
322
+ kind: 'primaryKey',
323
+ name: `primary key: ${contractTable.primaryKey.columns.join(', ')}`,
324
+ contractPath: `${tablePath}.primaryKey`,
325
+ code: 'primary_key_mismatch',
326
+ message: 'Primary key mismatch',
327
+ expected: contractTable.primaryKey,
328
+ actual: schemaTable.primaryKey,
329
+ children: [],
330
+ });
331
+ } else {
332
+ tableChildren.push({
333
+ status: 'pass',
334
+ kind: 'primaryKey',
335
+ name: `primary key: ${contractTable.primaryKey.columns.join(', ')}`,
336
+ contractPath: `${tablePath}.primaryKey`,
337
+ code: '',
338
+ message: '',
339
+ expected: undefined,
340
+ actual: undefined,
341
+ children: [],
342
+ });
343
+ }
344
+ } else if (schemaTable.primaryKey && strict) {
345
+ // Extra primary key in strict mode
346
+ issues.push({
347
+ kind: 'extra_primary_key',
348
+ table: tableName,
349
+ message: 'Extra primary key found in database (not in contract)',
350
+ });
351
+ tableChildren.push({
352
+ status: 'fail',
353
+ kind: 'primaryKey',
354
+ name: `primary key: ${schemaTable.primaryKey.columns.join(', ')}`,
355
+ contractPath: `${tablePath}.primaryKey`,
356
+ code: 'extra_primary_key',
357
+ message: 'Extra primary key found',
358
+ expected: undefined,
359
+ actual: schemaTable.primaryKey,
360
+ children: [],
361
+ });
362
+ }
363
+
364
+ // Compare foreign keys
365
+ const fkStatuses = verifyForeignKeys(
366
+ contractTable.foreignKeys,
367
+ schemaTable.foreignKeys,
368
+ tableName,
369
+ tablePath,
370
+ issues,
371
+ strict,
372
+ );
373
+ tableChildren.push(...fkStatuses);
374
+
375
+ // Compare unique constraints
376
+ // Pass schemaIndexes so unique indexes can satisfy unique constraint requirements
377
+ const uniqueStatuses = verifyUniqueConstraints(
378
+ contractTable.uniques,
379
+ schemaTable.uniques,
380
+ schemaTable.indexes,
381
+ tableName,
382
+ tablePath,
383
+ issues,
384
+ strict,
385
+ );
386
+ tableChildren.push(...uniqueStatuses);
387
+
388
+ // Compare indexes
389
+ // Pass schemaUniques so unique constraints can satisfy index requirements
390
+ const indexStatuses = verifyIndexes(
391
+ contractTable.indexes,
392
+ schemaTable.indexes,
393
+ schemaTable.uniques,
394
+ tableName,
395
+ tablePath,
396
+ issues,
397
+ strict,
398
+ );
399
+ tableChildren.push(...indexStatuses);
400
+
401
+ // Build table node
402
+ const tableStatus = tableChildren.some((c) => c.status === 'fail')
403
+ ? 'fail'
404
+ : tableChildren.some((c) => c.status === 'warn')
405
+ ? 'warn'
406
+ : 'pass';
407
+ // Collect failure messages from children to create a summary message
408
+ const tableFailureMessages = tableChildren
409
+ .filter((child) => child.status === 'fail' && child.message)
410
+ .map((child) => child.message)
411
+ .filter((msg): msg is string => typeof msg === 'string' && msg.length > 0);
412
+ const tableMessage =
413
+ tableStatus === 'fail' && tableFailureMessages.length > 0
414
+ ? `${tableFailureMessages.length} issue${tableFailureMessages.length === 1 ? '' : 's'}`
415
+ : '';
416
+ const tableCode =
417
+ tableStatus === 'fail' && tableChildren.length > 0 && tableChildren[0]
418
+ ? tableChildren[0].code
419
+ : '';
420
+ rootChildren.push({
421
+ status: tableStatus,
422
+ kind: 'table',
423
+ name: `table ${tableName}`,
424
+ contractPath: tablePath,
425
+ code: tableCode,
426
+ message: tableMessage,
427
+ expected: undefined,
428
+ actual: undefined,
429
+ children: tableChildren,
430
+ });
431
+ }
432
+
433
+ // Check for extra tables in strict mode
434
+ if (strict) {
435
+ for (const tableName of Object.keys(schemaTables)) {
436
+ if (!contractTables[tableName]) {
437
+ issues.push({
438
+ kind: 'extra_table',
439
+ table: tableName,
440
+ message: `Extra table "${tableName}" found in database (not in contract)`,
441
+ });
442
+ rootChildren.push({
443
+ status: 'fail',
444
+ kind: 'table',
445
+ name: `table ${tableName}`,
446
+ contractPath: `storage.tables.${tableName}`,
447
+ code: 'extra_table',
448
+ message: `Extra table "${tableName}" found`,
449
+ expected: undefined,
450
+ actual: undefined,
451
+ children: [],
452
+ });
453
+ }
454
+ }
455
+ }
456
+
457
+ // Validate that all extension packs declared in the contract are present in frameworkComponents
458
+ // This is a configuration integrity check - if the contract was emitted with an extension,
459
+ // that extension must be provided in the current configuration.
460
+ // Note: contract.extensionPacks includes adapter.id and target.id (from extractExtensionIds),
461
+ // so we check for matches as extension, adapter, or target components.
462
+ const contractExtensionPacks = contract.extensionPacks ?? {};
463
+ for (const extensionNamespace of Object.keys(contractExtensionPacks)) {
464
+ const hasComponent = options.frameworkComponents.some(
465
+ (component) =>
466
+ component.id === extensionNamespace &&
467
+ (component.kind === 'extension' ||
468
+ component.kind === 'adapter' ||
469
+ component.kind === 'target'),
470
+ );
471
+ if (!hasComponent) {
472
+ throw new Error(
473
+ `Extension pack '${extensionNamespace}' is declared in the contract but not found in framework components. ` +
474
+ 'This indicates a configuration mismatch - the contract was emitted with this extension pack, ' +
475
+ 'but it is not provided in the current configuration.',
476
+ );
477
+ }
478
+ }
479
+
480
+ // Compare component-owned database dependencies (pure, deterministic)
481
+ // Per ADR 154: We do NOT infer dependencies from contract extension packs.
482
+ // Dependencies are only collected from frameworkComponents provided by the CLI.
483
+ const databaseDependencies = collectDependenciesFromFrameworkComponents(
484
+ options.frameworkComponents,
485
+ );
486
+ const dependencyStatuses = verifyDatabaseDependencies(databaseDependencies, schema, issues);
487
+ rootChildren.push(...dependencyStatuses);
488
+
489
+ // Build root node
490
+ const rootStatus = rootChildren.some((c) => c.status === 'fail')
491
+ ? 'fail'
492
+ : rootChildren.some((c) => c.status === 'warn')
493
+ ? 'warn'
494
+ : 'pass';
495
+ const root: SchemaVerificationNode = {
496
+ status: rootStatus,
497
+ kind: 'contract',
498
+ name: 'contract',
499
+ contractPath: '',
500
+ code: '',
501
+ message: '',
502
+ expected: undefined,
503
+ actual: undefined,
504
+ children: rootChildren,
505
+ };
506
+
507
+ // Compute counts
508
+ const counts = computeCounts(root);
509
+
510
+ // Set ok flag
511
+ const ok = counts.fail === 0;
512
+
513
+ // Set code
514
+ const code = ok ? undefined : 'PN-SCHEMA-0001';
515
+
516
+ // Set summary
517
+ const summary = ok
518
+ ? 'Database schema satisfies contract'
519
+ : `Database schema does not satisfy contract (${counts.fail} failure${counts.fail === 1 ? '' : 's'})`;
520
+
521
+ const totalTime = Date.now() - startTime;
522
+
523
+ return {
524
+ ok,
525
+ ...ifDefined('code', code),
526
+ summary,
527
+ contract: {
528
+ coreHash: contractCoreHash,
529
+ ...ifDefined('profileHash', contractProfileHash),
530
+ },
531
+ target: {
532
+ expected: contractTarget,
533
+ actual: contractTarget,
534
+ },
535
+ schema: {
536
+ issues,
537
+ root,
538
+ counts,
539
+ },
540
+ meta: {
541
+ strict,
542
+ ...ifDefined('contractPath', context?.contractPath),
543
+ ...ifDefined('configPath', context?.configPath),
544
+ },
545
+ timings: {
546
+ total: totalTime,
547
+ },
548
+ };
549
+ }
550
+
551
+ /**
552
+ * Type predicate to check if a component has database dependencies with an init array.
553
+ * The familyId check is redundant since TargetBoundComponentDescriptor<'sql', T> already
554
+ * guarantees familyId is 'sql' at the type level, so we don't need runtime checks for it.
555
+ */
556
+ function hasDatabaseDependenciesInit<T extends string>(
557
+ component: TargetBoundComponentDescriptor<'sql', T>,
558
+ ): component is TargetBoundComponentDescriptor<'sql', T> & {
559
+ readonly databaseDependencies: {
560
+ readonly init: readonly ComponentDatabaseDependency<T>[];
561
+ };
562
+ } {
563
+ if (!('databaseDependencies' in component)) {
564
+ return false;
565
+ }
566
+ const dbDeps = (component as Record<string, unknown>)['databaseDependencies'];
567
+ if (dbDeps === undefined || dbDeps === null || typeof dbDeps !== 'object') {
568
+ return false;
569
+ }
570
+ const depsRecord = dbDeps as Record<string, unknown>;
571
+ const init = depsRecord['init'];
572
+ if (init === undefined || !Array.isArray(init)) {
573
+ return false;
574
+ }
575
+ return true;
576
+ }
577
+
578
+ function collectDependenciesFromFrameworkComponents<T extends string>(
579
+ components: ReadonlyArray<TargetBoundComponentDescriptor<'sql', T>>,
580
+ ): ReadonlyArray<ComponentDatabaseDependency<T>> {
581
+ const dependencies: ComponentDatabaseDependency<T>[] = [];
582
+ for (const component of components) {
583
+ if (hasDatabaseDependenciesInit(component)) {
584
+ dependencies.push(...component.databaseDependencies.init);
585
+ }
586
+ }
587
+ return dependencies;
588
+ }