@prisma-next/family-sql 0.14.0 → 0.15.0-dev.1

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 (80) hide show
  1. package/dist/{authoring-type-constructors-CjFfO6LM.mjs → authoring-type-constructors-CXd-8ydc.mjs} +7 -30
  2. package/dist/authoring-type-constructors-CXd-8ydc.mjs.map +1 -0
  3. package/dist/{control-adapter-Cmw9LvEP.d.mts → control-adapter-BQgad8Zc.d.mts} +30 -8
  4. package/dist/control-adapter-BQgad8Zc.d.mts.map +1 -0
  5. package/dist/control-adapter.d.mts +1 -1
  6. package/dist/control.d.mts +279 -34
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +364 -1005
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/diff.d.mts +102 -0
  11. package/dist/diff.d.mts.map +1 -0
  12. package/dist/diff.mjs +14 -0
  13. package/dist/diff.mjs.map +1 -0
  14. package/dist/ir.d.mts +26 -9
  15. package/dist/ir.d.mts.map +1 -1
  16. package/dist/ir.mjs +2 -2
  17. package/dist/ir.mjs.map +1 -1
  18. package/dist/migration.d.mts +11 -2
  19. package/dist/migration.d.mts.map +1 -1
  20. package/dist/migration.mjs +7 -0
  21. package/dist/migration.mjs.map +1 -1
  22. package/dist/pack.mjs +1 -1
  23. package/dist/psl-infer.d.mts +119 -0
  24. package/dist/psl-infer.d.mts.map +1 -0
  25. package/dist/psl-infer.mjs +366 -0
  26. package/dist/psl-infer.mjs.map +1 -0
  27. package/dist/schema-differ-DnoopSXm.d.mts +45 -0
  28. package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
  29. package/dist/schema-verify-W3r631Jh.mjs +226 -0
  30. package/dist/schema-verify-W3r631Jh.mjs.map +1 -0
  31. package/dist/{sql-contract-serializer-BR2vC7Z-.mjs → sql-contract-serializer-C75cfMSS.mjs} +46 -20
  32. package/dist/sql-contract-serializer-C75cfMSS.mjs.map +1 -0
  33. package/dist/{types-kgstZ_Zd.d.mts → types-BPv_y7iS.d.mts} +36 -142
  34. package/dist/types-BPv_y7iS.d.mts.map +1 -0
  35. package/package.json +26 -26
  36. package/src/core/authoring-entity-types.ts +12 -37
  37. package/src/core/control-adapter.ts +11 -5
  38. package/src/core/control-instance.ts +212 -42
  39. package/src/core/control-target-descriptor.ts +98 -0
  40. package/src/core/diff/schema-verify.ts +324 -0
  41. package/src/core/diff/sql-schema-diff.ts +41 -0
  42. package/src/core/diff/verifier-disposition.ts +30 -0
  43. package/src/core/ir/sql-contract-serializer-base.ts +70 -56
  44. package/src/core/ir/sql-contract-serializer.ts +5 -7
  45. package/src/core/ir/sql-schema-verifier-base.ts +5 -5
  46. package/src/core/migrations/contract-to-schema-ir.ts +137 -51
  47. package/src/core/migrations/control-policy.ts +66 -115
  48. package/src/core/migrations/field-event-planner.ts +2 -2
  49. package/src/core/migrations/native-type-expander.ts +28 -0
  50. package/src/core/migrations/schema-differ.ts +41 -0
  51. package/src/core/migrations/types.ts +33 -37
  52. package/src/core/psl-contract-infer/name-transforms.ts +15 -0
  53. package/src/core/psl-contract-infer/printer-config.ts +14 -6
  54. package/src/core/psl-contract-infer/relation-inference.ts +30 -1
  55. package/src/core/sql-migration.ts +12 -1
  56. package/src/exports/control.ts +12 -2
  57. package/src/exports/diff.ts +25 -0
  58. package/src/exports/psl-infer.ts +40 -0
  59. package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +0 -1
  60. package/dist/control-adapter-Cmw9LvEP.d.mts.map +0 -1
  61. package/dist/schema-verify.d.mts +0 -39
  62. package/dist/schema-verify.d.mts.map +0 -1
  63. package/dist/schema-verify.mjs +0 -2
  64. package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +0 -1
  65. package/dist/test-utils.d.mts +0 -2
  66. package/dist/test-utils.mjs +0 -2
  67. package/dist/types-kgstZ_Zd.d.mts.map +0 -1
  68. package/dist/verify-sql-schema-thU-jKpf.d.mts +0 -66
  69. package/dist/verify-sql-schema-thU-jKpf.d.mts.map +0 -1
  70. package/dist/verify-sql-schema-xT4udQLQ.mjs +0 -1501
  71. package/dist/verify-sql-schema-xT4udQLQ.mjs.map +0 -1
  72. package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
  73. package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
  74. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
  75. package/src/core/schema-verify/control-verify-emit.ts +0 -46
  76. package/src/core/schema-verify/verifier-disposition.ts +0 -58
  77. package/src/core/schema-verify/verify-helpers.ts +0 -820
  78. package/src/core/schema-verify/verify-sql-schema.ts +0 -1311
  79. package/src/exports/schema-verify.ts +0 -18
  80. package/src/exports/test-utils.ts +0 -9
@@ -1,1311 +0,0 @@
1
- /**
2
- * Pure SQL schema verification function.
3
- *
4
- * This module provides a pure function that verifies a SqlSchemaIR against
5
- * a Contract 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 { ColumnDefault, Contract, ControlPolicy } from '@prisma-next/contract/types';
10
- import { effectiveControlPolicy } from '@prisma-next/contract/types';
11
- import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
12
- import type {
13
- OperationContext,
14
- SchemaIssue,
15
- SchemaVerificationNode,
16
- VerificationStatus,
17
- VerifyDatabaseSchemaResult,
18
- } from '@prisma-next/framework-components/control';
19
-
20
- import {
21
- isStorageTypeInstance,
22
- type SqlStorage,
23
- type StorageColumn,
24
- StorageTable,
25
- type StorageTypeInstance,
26
- } from '@prisma-next/sql-contract/types';
27
- import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
28
- import { canonicalStringify } from '@prisma-next/utils/canonical-stringify';
29
- import { ifDefined } from '@prisma-next/utils/defined';
30
- import { extractCodecControlHooks } from '../assembly';
31
- import { resolveValueSetValues } from '../migrations/contract-to-schema-ir';
32
- import type { CodecControlHooks } from '../migrations/types';
33
- import { emitIssueAndNodeUnderControlPolicy } from './control-verify-emit';
34
- import { verifierDisposition } from './verifier-disposition';
35
- import {
36
- arraysEqual,
37
- computeCounts,
38
- verifyCheckConstraints,
39
- verifyForeignKeys,
40
- verifyIndexes,
41
- verifyPrimaryKey,
42
- verifyUniqueConstraints,
43
- } from './verify-helpers';
44
-
45
- /**
46
- * Function type for normalizing raw database default expressions into ColumnDefault.
47
- * Target-specific implementations handle database dialect differences.
48
- */
49
- export type DefaultNormalizer = (
50
- rawDefault: string,
51
- nativeType: string,
52
- ) => ColumnDefault | undefined;
53
-
54
- /**
55
- * Function type for normalizing schema native types to canonical form for comparison.
56
- * Target-specific implementations handle dialect-specific type name variations
57
- * (e.g., Postgres 'varchar' → 'character varying', 'timestamptz' normalization).
58
- */
59
- export type NativeTypeNormalizer = (nativeType: string) => string;
60
-
61
- /**
62
- * Options for the pure schema verification function.
63
- */
64
- export interface VerifySqlSchemaOptions {
65
- /** The validated SQL contract to verify against */
66
- readonly contract: Contract<SqlStorage>;
67
- /** The schema IR from introspection (or another source) */
68
- readonly schema: SqlSchemaIR;
69
- /** Whether to run in strict mode (detects extra tables/columns) */
70
- readonly strict: boolean;
71
- /** Optional operation context for metadata */
72
- readonly context?: OperationContext;
73
- /** Type metadata registry for codec consistency warnings */
74
- readonly typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
75
- /**
76
- * Active framework components participating in this composition.
77
- * All components must have matching familyId ('sql') and targetId.
78
- */
79
- readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
80
- /**
81
- * Optional target-specific normalizer for raw database default expressions.
82
- * When provided, schema defaults (raw strings) are normalized before comparison
83
- * with contract defaults (ColumnDefault objects).
84
- */
85
- readonly normalizeDefault?: DefaultNormalizer;
86
- /**
87
- * Optional target-specific normalizer for schema native type names.
88
- * When provided, schema native types are normalized before comparison
89
- * with contract native types (e.g., Postgres 'varchar' → 'character varying').
90
- */
91
- readonly normalizeNativeType?: NativeTypeNormalizer;
92
- }
93
-
94
- /**
95
- * Verifies that a SqlSchemaIR matches a Contract.
96
- *
97
- * This is a pure function that does NOT perform any database I/O.
98
- * It takes an already-introspected schema IR and compares it against
99
- * the contract requirements.
100
- *
101
- * @param options - Verification options
102
- * @returns VerifyDatabaseSchemaResult with verification tree and issues
103
- */
104
- export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabaseSchemaResult {
105
- const {
106
- contract,
107
- schema,
108
- strict,
109
- context,
110
- typeMetadataRegistry,
111
- normalizeDefault,
112
- normalizeNativeType,
113
- } = options;
114
- const startTime = Date.now();
115
-
116
- // Extract codec control hooks once at entry point for reuse
117
- const codecHooks = extractCodecControlHooks(options.frameworkComponents);
118
-
119
- const { contractStorageHash, contractProfileHash, contractTarget } =
120
- extractContractMetadata(contract);
121
- const storageTypes: Readonly<Record<string, StorageTypeInstance>> = contract.storage.types ?? {};
122
- const { issues, rootChildren } = verifySchemaTables({
123
- contract,
124
- schema,
125
- strict,
126
- typeMetadataRegistry,
127
- codecHooks,
128
- storageTypes,
129
- ...ifDefined('normalizeDefault', normalizeDefault),
130
- ...ifDefined('normalizeNativeType', normalizeNativeType),
131
- });
132
-
133
- validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
134
-
135
- const typeNodes: SchemaVerificationNode[] = [];
136
- // Storage-type findings dispatch through the same control policy as tables
137
- // and columns: each issue's disposition (fail / warn / suppress) is resolved
138
- // from the type's effective control so an `external`/`observed` enum no longer
139
- // hard-fails on value drift. `suppress` drops the issue entirely; the node
140
- // status is the worst surviving disposition.
141
- const pushTypeNode = (
142
- typeName: string,
143
- contractPath: string,
144
- typeIssues: readonly SchemaIssue[],
145
- controlPolicy: ControlPolicy,
146
- ): void => {
147
- let status: VerificationStatus = 'pass';
148
- let code = '';
149
- let emitted = 0;
150
- for (const issue of typeIssues) {
151
- const disposition = verifierDisposition(controlPolicy, issue.kind);
152
- if (disposition === 'suppress') continue;
153
- issues.push(issue);
154
- emitted += 1;
155
- if (code === '') code = issue.kind;
156
- if (disposition === 'fail') {
157
- status = 'fail';
158
- } else if (disposition === 'warn' && status !== 'fail') {
159
- status = 'warn';
160
- }
161
- }
162
- typeNodes.push({
163
- status,
164
- kind: 'storageType',
165
- name: `type ${typeName}`,
166
- contractPath,
167
- code: status === 'pass' ? '' : code,
168
- message: emitted > 0 ? `${emitted} issue${emitted === 1 ? '' : 's'}` : '',
169
- expected: undefined,
170
- actual: undefined,
171
- children: [],
172
- });
173
- };
174
-
175
- // Top-level `storage.types`: codec-typed entries via codec hooks.
176
- for (const [typeName, typeInstance] of Object.entries(contract.storage.types ?? {})) {
177
- if (isStorageTypeInstance(typeInstance)) {
178
- const hook = codecHooks.get(typeInstance.codecId);
179
- pushTypeNode(
180
- typeName,
181
- `storage.types.${typeName}`,
182
- hook?.verifyType ? hook.verifyType({ typeName, typeInstance, schema }) : [],
183
- effectiveControlPolicy(undefined, contract.defaultControlPolicy),
184
- );
185
- }
186
- }
187
-
188
- if (typeNodes.length > 0) {
189
- const typesStatus: VerificationStatus = typeNodes.some((n) => n.status === 'fail')
190
- ? 'fail'
191
- : typeNodes.some((n) => n.status === 'warn')
192
- ? 'warn'
193
- : 'pass';
194
- rootChildren.push({
195
- status: typesStatus,
196
- kind: 'storageTypes',
197
- name: 'types',
198
- contractPath: 'storage.types',
199
- code: typesStatus === 'fail' ? 'type_mismatch' : '',
200
- message: '',
201
- expected: undefined,
202
- actual: undefined,
203
- children: typeNodes,
204
- });
205
- }
206
-
207
- const root = buildRootNode(rootChildren);
208
-
209
- // Compute counts
210
- const counts = computeCounts(root);
211
-
212
- // Set ok flag
213
- const ok = counts.fail === 0;
214
-
215
- // Set code
216
- const code = ok ? undefined : 'PN-SCHEMA-0001';
217
-
218
- // Set summary
219
- const summary = ok
220
- ? 'Database schema satisfies contract'
221
- : `Database schema does not satisfy contract (${counts.fail} failure${counts.fail === 1 ? '' : 's'})`;
222
-
223
- const totalTime = Date.now() - startTime;
224
-
225
- return {
226
- ok,
227
- ...ifDefined('code', code),
228
- summary,
229
- contract: {
230
- storageHash: contractStorageHash,
231
- ...ifDefined('profileHash', contractProfileHash),
232
- },
233
- target: {
234
- expected: contractTarget,
235
- actual: contractTarget,
236
- },
237
- schema: {
238
- issues,
239
- root,
240
- counts,
241
- },
242
- meta: {
243
- strict,
244
- ...ifDefined('contractPath', context?.contractPath),
245
- ...ifDefined('configPath', context?.configPath),
246
- },
247
- timings: {
248
- total: totalTime,
249
- },
250
- };
251
- }
252
-
253
- function extractContractMetadata(contract: Contract<SqlStorage>): {
254
- contractStorageHash: SqlStorage['storageHash'];
255
- contractProfileHash?: Contract<SqlStorage>['profileHash'] | undefined;
256
- contractTarget: Contract<SqlStorage>['target'];
257
- } {
258
- return {
259
- contractStorageHash: contract.storage.storageHash,
260
- contractProfileHash:
261
- 'profileHash' in contract && typeof contract.profileHash === 'string'
262
- ? contract.profileHash
263
- : undefined,
264
- contractTarget: contract.target,
265
- };
266
- }
267
-
268
- function verifySchemaTables(options: {
269
- contract: Contract<SqlStorage>;
270
- schema: SqlSchemaIR;
271
- strict: boolean;
272
- typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
273
- codecHooks: Map<string, CodecControlHooks>;
274
- storageTypes: Readonly<Record<string, StorageTypeInstance>>;
275
- normalizeDefault?: DefaultNormalizer;
276
- normalizeNativeType?: NativeTypeNormalizer;
277
- }): { issues: SchemaIssue[]; rootChildren: SchemaVerificationNode[] } {
278
- const {
279
- contract,
280
- schema,
281
- strict,
282
- typeMetadataRegistry,
283
- codecHooks,
284
- storageTypes,
285
- normalizeDefault,
286
- normalizeNativeType,
287
- } = options;
288
- const contractDefaultControl = contract.defaultControlPolicy;
289
- const issues: SchemaIssue[] = [];
290
- const rootChildren: SchemaVerificationNode[] = [];
291
- const schemaTables = schema.tables;
292
- const namespaceIds = Object.keys(contract.storage.namespaces).sort((a, b) =>
293
- a < b ? -1 : a > b ? 1 : 0,
294
- );
295
-
296
- for (const namespaceId of namespaceIds) {
297
- const ns = contract.storage.namespaces[namespaceId];
298
- if (!ns) continue;
299
- for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table ?? {})) {
300
- if (!(contractTableRaw instanceof StorageTable)) {
301
- throw new Error(
302
- `verifySqlSchema: expected StorageTable at storage.namespaces.${namespaceId}.entries.table.${tableName}`,
303
- );
304
- }
305
- const contractTable = contractTableRaw;
306
- const tableControlPolicy = effectiveControlPolicy(
307
- contractTable.control,
308
- contractDefaultControl,
309
- );
310
- const schemaTable = schemaTables[tableName];
311
- const tablePath = `storage.namespaces.${namespaceId}.entries.table.${tableName}`;
312
-
313
- if (!schemaTable) {
314
- const issue: SchemaIssue = {
315
- kind: 'missing_table',
316
- table: tableName,
317
- namespaceId,
318
- message: `Table "${tableName}" is missing from database`,
319
- };
320
- emitIssueAndNodeUnderControlPolicy(
321
- tableControlPolicy,
322
- issue,
323
- {
324
- status: 'fail',
325
- kind: 'table',
326
- name: `table ${tableName}`,
327
- contractPath: tablePath,
328
- code: 'missing_table',
329
- message: `Table "${tableName}" is missing`,
330
- expected: undefined,
331
- actual: undefined,
332
- children: [],
333
- },
334
- issues,
335
- rootChildren,
336
- );
337
- continue;
338
- }
339
-
340
- const tableChildren = verifyTableChildren({
341
- contractTable,
342
- schemaTable,
343
- tableName,
344
- namespaceId,
345
- tablePath,
346
- tableControlPolicy,
347
- issues,
348
- strict,
349
- typeMetadataRegistry,
350
- codecHooks,
351
- storageTypes,
352
- contractStorage: contract.storage,
353
- ...ifDefined('normalizeDefault', normalizeDefault),
354
- ...ifDefined('normalizeNativeType', normalizeNativeType),
355
- });
356
- rootChildren.push(buildTableNode(tableName, tablePath, tableChildren));
357
- }
358
- }
359
-
360
- if (strict) {
361
- for (const tableName of Object.keys(schemaTables)) {
362
- const claimed = namespaceIds.some(
363
- (namespaceId) =>
364
- contract.storage.namespaces[namespaceId]?.entries.table?.[tableName] !== undefined,
365
- );
366
- if (!claimed) {
367
- const extraTableControlPolicy = effectiveControlPolicy(undefined, contractDefaultControl);
368
- const issue: SchemaIssue = {
369
- kind: 'extra_table',
370
- table: tableName,
371
- message: `Extra table "${tableName}" found in database (not in contract)`,
372
- };
373
- emitIssueAndNodeUnderControlPolicy(
374
- extraTableControlPolicy,
375
- issue,
376
- {
377
- status: 'fail',
378
- kind: 'table',
379
- name: `table ${tableName}`,
380
- contractPath: `storage.namespaces.*.entries.table.${tableName}`,
381
- code: 'extra_table',
382
- message: `Extra table "${tableName}" found`,
383
- expected: undefined,
384
- actual: undefined,
385
- children: [],
386
- },
387
- issues,
388
- rootChildren,
389
- );
390
- }
391
- }
392
- }
393
-
394
- return { issues, rootChildren };
395
- }
396
-
397
- function verifyTableChildren(options: {
398
- contractTable: StorageTable;
399
- schemaTable: SqlSchemaIR['tables'][string];
400
- tableName: string;
401
- namespaceId: string;
402
- tablePath: string;
403
- tableControlPolicy: ControlPolicy;
404
- issues: SchemaIssue[];
405
- strict: boolean;
406
- typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
407
- codecHooks: Map<string, CodecControlHooks>;
408
- storageTypes: Readonly<Record<string, StorageTypeInstance>>;
409
- normalizeDefault?: DefaultNormalizer;
410
- normalizeNativeType?: NativeTypeNormalizer;
411
- contractStorage: SqlStorage;
412
- }): SchemaVerificationNode[] {
413
- const {
414
- contractTable,
415
- schemaTable,
416
- tableName,
417
- namespaceId,
418
- tablePath,
419
- tableControlPolicy,
420
- issues,
421
- strict,
422
- typeMetadataRegistry,
423
- codecHooks,
424
- storageTypes,
425
- normalizeDefault,
426
- normalizeNativeType,
427
- contractStorage,
428
- } = options;
429
- const tableChildren: SchemaVerificationNode[] = [];
430
- const columnNodes = collectContractColumnNodes({
431
- contractTable,
432
- schemaTable,
433
- tableName,
434
- namespaceId,
435
- tablePath,
436
- tableControlPolicy,
437
- issues,
438
- strict,
439
- typeMetadataRegistry,
440
- codecHooks,
441
- storageTypes,
442
- ...ifDefined('normalizeDefault', normalizeDefault),
443
- ...ifDefined('normalizeNativeType', normalizeNativeType),
444
- });
445
- if (columnNodes.length > 0) {
446
- tableChildren.push(buildColumnsNode(tablePath, columnNodes));
447
- }
448
- if (strict) {
449
- appendExtraColumnNodes({
450
- contractTable,
451
- schemaTable,
452
- tableName,
453
- namespaceId,
454
- tablePath,
455
- tableControlPolicy,
456
- issues,
457
- columnNodes,
458
- });
459
- }
460
-
461
- if (contractTable.primaryKey) {
462
- const pkStatus = verifyPrimaryKey(
463
- contractTable.primaryKey,
464
- schemaTable.primaryKey,
465
- tableName,
466
- namespaceId,
467
- tableControlPolicy,
468
- issues,
469
- );
470
- if (pkStatus === 'fail') {
471
- tableChildren.push({
472
- status: 'fail',
473
- kind: 'primaryKey',
474
- name: `primary key: ${contractTable.primaryKey.columns.join(', ')}`,
475
- contractPath: `${tablePath}.primaryKey`,
476
- code: 'primary_key_mismatch',
477
- message: 'Primary key mismatch',
478
- expected: contractTable.primaryKey,
479
- actual: schemaTable.primaryKey,
480
- children: [],
481
- });
482
- } else if (pkStatus === 'warn') {
483
- tableChildren.push({
484
- status: 'warn',
485
- kind: 'primaryKey',
486
- name: `primary key: ${contractTable.primaryKey.columns.join(', ')}`,
487
- contractPath: `${tablePath}.primaryKey`,
488
- code: 'primary_key_mismatch',
489
- message: 'Primary key mismatch',
490
- expected: contractTable.primaryKey,
491
- actual: schemaTable.primaryKey,
492
- children: [],
493
- });
494
- } else {
495
- tableChildren.push({
496
- status: 'pass',
497
- kind: 'primaryKey',
498
- name: `primary key: ${contractTable.primaryKey.columns.join(', ')}`,
499
- contractPath: `${tablePath}.primaryKey`,
500
- code: '',
501
- message: '',
502
- expected: undefined,
503
- actual: undefined,
504
- children: [],
505
- });
506
- }
507
- } else if (schemaTable.primaryKey && strict) {
508
- const issue: SchemaIssue = {
509
- kind: 'extra_primary_key',
510
- table: tableName,
511
- namespaceId,
512
- message: 'Extra primary key found in database (not in contract)',
513
- };
514
- emitIssueAndNodeUnderControlPolicy(
515
- tableControlPolicy,
516
- issue,
517
- {
518
- status: 'fail',
519
- kind: 'primaryKey',
520
- name: `primary key: ${schemaTable.primaryKey.columns.join(', ')}`,
521
- contractPath: `${tablePath}.primaryKey`,
522
- code: 'extra_primary_key',
523
- message: 'Extra primary key found',
524
- expected: undefined,
525
- actual: schemaTable.primaryKey,
526
- children: [],
527
- },
528
- issues,
529
- tableChildren,
530
- );
531
- }
532
-
533
- // Verify FK constraints only for FKs with constraint: true.
534
- // Always call when strict mode is on so extra-FK detection runs even if
535
- // the contract has no FKs for this table.
536
- const constraintFks = contractTable.foreignKeys.filter((fk) => fk.constraint === true);
537
- if (constraintFks.length > 0 || strict) {
538
- const fkStatuses = verifyForeignKeys(
539
- constraintFks,
540
- schemaTable.foreignKeys,
541
- tableName,
542
- namespaceId,
543
- tablePath,
544
- tableControlPolicy,
545
- issues,
546
- strict,
547
- );
548
- tableChildren.push(...fkStatuses);
549
- }
550
-
551
- const uniqueStatuses = verifyUniqueConstraints(
552
- contractTable.uniques,
553
- schemaTable.uniques,
554
- schemaTable.indexes,
555
- tableName,
556
- namespaceId,
557
- tablePath,
558
- tableControlPolicy,
559
- issues,
560
- strict,
561
- );
562
- tableChildren.push(...uniqueStatuses);
563
-
564
- // Combine user-declared indexes with FK-backing indexes (from FKs with index: true)
565
- // so the verifier treats FK-backing indexes as expected, not "extra".
566
- // Deduplicate: skip FK-backing indexes already covered by a user-declared index.
567
- const fkBackingIndexes = contractTable.foreignKeys
568
- .filter(
569
- (fk) =>
570
- fk.index === true &&
571
- !contractTable.indexes.some((idx) => arraysEqual(idx.columns, fk.source.columns)),
572
- )
573
- .map((fk) => ({ columns: fk.source.columns }));
574
- const allExpectedIndexes = [...contractTable.indexes, ...fkBackingIndexes];
575
-
576
- const indexStatuses = verifyIndexes(
577
- allExpectedIndexes,
578
- schemaTable.indexes,
579
- schemaTable.uniques,
580
- tableName,
581
- namespaceId,
582
- tablePath,
583
- tableControlPolicy,
584
- issues,
585
- strict,
586
- );
587
- tableChildren.push(...indexStatuses);
588
-
589
- // Verify check constraints when the contract declares checks for this table OR
590
- // when strict mode is on (so extra live checks on zero-check tables are detected).
591
- // schemaTable.checks carries the introspected live checks (parsed value sets).
592
- const contractCheckIRs = (contractTable.checks ?? []).map((c) => ({
593
- name: c.name,
594
- column: c.column,
595
- permittedValues: resolveValueSetValues(c.valueSet, contractStorage, `check "${c.name}"`),
596
- }));
597
- if (strict || contractCheckIRs.length > 0) {
598
- const checkStatuses = verifyCheckConstraints(
599
- contractCheckIRs,
600
- schemaTable.checks ?? [],
601
- tableName,
602
- namespaceId,
603
- tablePath,
604
- tableControlPolicy,
605
- issues,
606
- strict,
607
- );
608
- tableChildren.push(...checkStatuses);
609
- }
610
-
611
- return tableChildren;
612
- }
613
-
614
- function collectContractColumnNodes(options: {
615
- contractTable: StorageTable;
616
- schemaTable: SqlSchemaIR['tables'][string];
617
- tableName: string;
618
- namespaceId: string;
619
- tablePath: string;
620
- tableControlPolicy: ControlPolicy;
621
- issues: SchemaIssue[];
622
- strict: boolean;
623
- typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
624
- codecHooks: Map<string, CodecControlHooks>;
625
- storageTypes: Readonly<Record<string, StorageTypeInstance>>;
626
- normalizeDefault?: DefaultNormalizer;
627
- normalizeNativeType?: NativeTypeNormalizer;
628
- }): SchemaVerificationNode[] {
629
- const {
630
- contractTable,
631
- schemaTable,
632
- tableName,
633
- namespaceId,
634
- tablePath,
635
- tableControlPolicy,
636
- issues,
637
- strict,
638
- typeMetadataRegistry,
639
- codecHooks,
640
- storageTypes,
641
- normalizeDefault,
642
- normalizeNativeType,
643
- } = options;
644
- const columnNodes: SchemaVerificationNode[] = [];
645
-
646
- for (const [columnName, contractColumn] of Object.entries(contractTable.columns)) {
647
- const schemaColumn = schemaTable.columns[columnName];
648
- const columnPath = `${tablePath}.columns.${columnName}`;
649
-
650
- if (!schemaColumn) {
651
- const issue: SchemaIssue = {
652
- kind: 'missing_column',
653
- table: tableName,
654
- namespaceId,
655
- column: columnName,
656
- message: `Column "${tableName}"."${columnName}" is missing from database`,
657
- };
658
- emitIssueAndNodeUnderControlPolicy(
659
- tableControlPolicy,
660
- issue,
661
- {
662
- status: 'fail',
663
- kind: 'column',
664
- name: `${columnName}: missing`,
665
- contractPath: columnPath,
666
- code: 'missing_column',
667
- message: `Column "${columnName}" is missing`,
668
- expected: undefined,
669
- actual: undefined,
670
- children: [],
671
- },
672
- issues,
673
- columnNodes,
674
- );
675
- continue;
676
- }
677
-
678
- columnNodes.push(
679
- verifyColumn({
680
- tableName,
681
- namespaceId,
682
- columnName,
683
- contractColumn,
684
- schemaColumn,
685
- columnPath,
686
- tableControlPolicy,
687
- issues,
688
- strict,
689
- typeMetadataRegistry,
690
- codecHooks,
691
- storageTypes,
692
- ...ifDefined('normalizeDefault', normalizeDefault),
693
- ...ifDefined('normalizeNativeType', normalizeNativeType),
694
- }),
695
- );
696
- }
697
-
698
- return columnNodes;
699
- }
700
-
701
- function appendExtraColumnNodes(options: {
702
- contractTable: StorageTable;
703
- schemaTable: SqlSchemaIR['tables'][string];
704
- tableName: string;
705
- namespaceId: string;
706
- tablePath: string;
707
- tableControlPolicy: ControlPolicy;
708
- issues: SchemaIssue[];
709
- columnNodes: SchemaVerificationNode[];
710
- }): void {
711
- const {
712
- contractTable,
713
- schemaTable,
714
- tableName,
715
- namespaceId,
716
- tablePath,
717
- tableControlPolicy,
718
- issues,
719
- columnNodes,
720
- } = options;
721
- for (const [columnName, { nativeType }] of Object.entries(schemaTable.columns)) {
722
- if (!contractTable.columns[columnName]) {
723
- const issue: SchemaIssue = {
724
- kind: 'extra_column',
725
- table: tableName,
726
- namespaceId,
727
- column: columnName,
728
- message: `Extra column "${tableName}"."${columnName}" found in database (not in contract)`,
729
- };
730
- emitIssueAndNodeUnderControlPolicy(
731
- tableControlPolicy,
732
- issue,
733
- {
734
- status: 'fail',
735
- kind: 'column',
736
- name: `${columnName}: extra`,
737
- contractPath: `${tablePath}.columns.${columnName}`,
738
- code: 'extra_column',
739
- message: `Extra column "${columnName}" found`,
740
- expected: undefined,
741
- actual: nativeType,
742
- children: [],
743
- },
744
- issues,
745
- columnNodes,
746
- );
747
- }
748
- }
749
- }
750
-
751
- function verifyColumn(options: {
752
- tableName: string;
753
- namespaceId: string;
754
- columnName: string;
755
- contractColumn: StorageTable['columns'][string];
756
- schemaColumn: SqlSchemaIR['tables'][string]['columns'][string];
757
- columnPath: string;
758
- tableControlPolicy: ControlPolicy;
759
- issues: SchemaIssue[];
760
- strict: boolean;
761
- typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
762
- codecHooks: Map<string, CodecControlHooks>;
763
- storageTypes: Readonly<Record<string, StorageTypeInstance>>;
764
- normalizeDefault?: DefaultNormalizer;
765
- normalizeNativeType?: NativeTypeNormalizer;
766
- }): SchemaVerificationNode {
767
- const {
768
- tableName,
769
- namespaceId,
770
- columnName,
771
- contractColumn,
772
- schemaColumn,
773
- columnPath,
774
- tableControlPolicy,
775
- issues,
776
- strict,
777
- codecHooks,
778
- storageTypes,
779
- normalizeDefault,
780
- normalizeNativeType,
781
- } = options;
782
- const columnChildren: SchemaVerificationNode[] = [];
783
- let columnStatus: VerificationStatus = 'pass';
784
-
785
- const resolvedContractColumn = resolveContractColumnTypeMetadata(contractColumn, storageTypes, {
786
- tableName,
787
- columnName,
788
- });
789
- const contractNativeType = renderExpectedNativeType(contractColumn, storageTypes, codecHooks, {
790
- tableName,
791
- columnName,
792
- });
793
- const schemaNativeType =
794
- normalizeNativeType?.(schemaColumn.nativeType) ?? schemaColumn.nativeType;
795
-
796
- const typesMatch = contractNativeType === schemaNativeType;
797
-
798
- if (!typesMatch) {
799
- const issue: SchemaIssue = {
800
- kind: 'type_mismatch',
801
- table: tableName,
802
- namespaceId,
803
- column: columnName,
804
- expected: contractNativeType,
805
- actual: schemaNativeType,
806
- message: `Column "${tableName}"."${columnName}" has type mismatch: expected "${contractNativeType}", got "${schemaNativeType}"`,
807
- };
808
- const disposition = verifierDisposition(tableControlPolicy, issue.kind);
809
- if (disposition !== 'suppress') {
810
- issues.push(issue);
811
- columnChildren.push({
812
- status: disposition,
813
- kind: 'type',
814
- name: 'type',
815
- contractPath: `${columnPath}.nativeType`,
816
- code: 'type_mismatch',
817
- message: `Type mismatch: expected ${contractNativeType}, got ${schemaNativeType}`,
818
- expected: contractNativeType,
819
- actual: schemaNativeType,
820
- children: [],
821
- });
822
- columnStatus = disposition;
823
- }
824
- }
825
-
826
- if (resolvedContractColumn.codecId) {
827
- const typeMetadata = options.typeMetadataRegistry.get(resolvedContractColumn.codecId);
828
- if (!typeMetadata) {
829
- columnChildren.push({
830
- status: 'warn',
831
- kind: 'type',
832
- name: 'type_metadata_missing',
833
- contractPath: `${columnPath}.codecId`,
834
- code: 'type_metadata_missing',
835
- message: `codecId "${resolvedContractColumn.codecId}" not found in type metadata registry`,
836
- expected: resolvedContractColumn.codecId,
837
- actual: undefined,
838
- children: [],
839
- });
840
- } else if (
841
- typeMetadata.nativeType &&
842
- typeMetadata.nativeType !== resolvedContractColumn.nativeType
843
- ) {
844
- columnChildren.push({
845
- status: 'warn',
846
- kind: 'type',
847
- name: 'type_consistency',
848
- contractPath: `${columnPath}.codecId`,
849
- code: 'type_consistency_warning',
850
- message: `codecId "${resolvedContractColumn.codecId}" maps to nativeType "${typeMetadata.nativeType}" in registry, but contract has "${resolvedContractColumn.nativeType}"`,
851
- expected: typeMetadata.nativeType,
852
- actual: resolvedContractColumn.nativeType,
853
- children: [],
854
- });
855
- }
856
- }
857
-
858
- if (contractColumn.nullable !== schemaColumn.nullable) {
859
- const issue: SchemaIssue = {
860
- kind: 'nullability_mismatch',
861
- table: tableName,
862
- namespaceId,
863
- column: columnName,
864
- expected: String(contractColumn.nullable),
865
- actual: String(schemaColumn.nullable),
866
- message: `Column "${tableName}"."${columnName}" has nullability mismatch: expected ${contractColumn.nullable ? 'nullable' : 'not null'}, got ${schemaColumn.nullable ? 'nullable' : 'not null'}`,
867
- };
868
- const disposition = verifierDisposition(tableControlPolicy, issue.kind);
869
- if (disposition !== 'suppress') {
870
- issues.push(issue);
871
- columnChildren.push({
872
- status: disposition,
873
- kind: 'nullability',
874
- name: 'nullability',
875
- contractPath: `${columnPath}.nullable`,
876
- code: 'nullability_mismatch',
877
- message: `Nullability mismatch: expected ${contractColumn.nullable ? 'nullable' : 'not null'}, got ${schemaColumn.nullable ? 'nullable' : 'not null'}`,
878
- expected: contractColumn.nullable,
879
- actual: schemaColumn.nullable,
880
- children: [],
881
- });
882
- columnStatus = disposition;
883
- }
884
- }
885
-
886
- if (contractColumn.default) {
887
- if (!schemaColumn.default) {
888
- const defaultDescription = describeColumnDefault(contractColumn.default);
889
- const issue: SchemaIssue = {
890
- kind: 'default_missing',
891
- table: tableName,
892
- namespaceId,
893
- column: columnName,
894
- expected: defaultDescription,
895
- message: `Column "${tableName}"."${columnName}" should have default ${defaultDescription} but database has no default`,
896
- };
897
- const disposition = verifierDisposition(tableControlPolicy, issue.kind);
898
- if (disposition !== 'suppress') {
899
- issues.push(issue);
900
- columnChildren.push({
901
- status: disposition,
902
- kind: 'default',
903
- name: 'default',
904
- contractPath: `${columnPath}.default`,
905
- code: 'default_missing',
906
- message: `Default missing: expected ${defaultDescription}`,
907
- expected: defaultDescription,
908
- actual: undefined,
909
- children: [],
910
- });
911
- columnStatus = disposition;
912
- }
913
- } else if (
914
- !columnDefaultsEqual(
915
- contractColumn.default,
916
- schemaColumn.default,
917
- normalizeDefault,
918
- schemaNativeType,
919
- )
920
- ) {
921
- const expectedDescription = describeColumnDefault(contractColumn.default);
922
- const actualDescription = schemaColumn.default;
923
- const issue: SchemaIssue = {
924
- kind: 'default_mismatch',
925
- table: tableName,
926
- namespaceId,
927
- column: columnName,
928
- expected: expectedDescription,
929
- actual: actualDescription,
930
- message: `Column "${tableName}"."${columnName}" has default mismatch: expected ${expectedDescription}, got ${actualDescription}`,
931
- };
932
- const disposition = verifierDisposition(tableControlPolicy, issue.kind);
933
- if (disposition !== 'suppress') {
934
- issues.push(issue);
935
- columnChildren.push({
936
- status: disposition,
937
- kind: 'default',
938
- name: 'default',
939
- contractPath: `${columnPath}.default`,
940
- code: 'default_mismatch',
941
- message: `Default mismatch: expected ${expectedDescription}, got ${actualDescription}`,
942
- expected: expectedDescription,
943
- actual: actualDescription,
944
- children: [],
945
- });
946
- columnStatus = disposition;
947
- }
948
- }
949
- } else if (strict && schemaColumn.default) {
950
- const issue: SchemaIssue = {
951
- kind: 'extra_default',
952
- table: tableName,
953
- namespaceId,
954
- column: columnName,
955
- actual: schemaColumn.default,
956
- message: `Column "${tableName}"."${columnName}" has default ${schemaColumn.default} in database but contract specifies no default`,
957
- };
958
- const disposition = verifierDisposition(tableControlPolicy, issue.kind);
959
- if (disposition !== 'suppress') {
960
- issues.push(issue);
961
- columnChildren.push({
962
- status: disposition,
963
- kind: 'default',
964
- name: 'default',
965
- contractPath: `${columnPath}.default`,
966
- code: 'extra_default',
967
- message: `Extra default: ${schemaColumn.default}`,
968
- expected: undefined,
969
- actual: schemaColumn.default,
970
- children: [],
971
- });
972
- columnStatus = disposition;
973
- }
974
- }
975
-
976
- // Single-pass aggregation for better performance
977
- const aggregated = aggregateChildState(columnChildren, columnStatus);
978
- const nullableText = contractColumn.nullable ? 'nullable' : 'not nullable';
979
- const columnTypeDisplay = resolvedContractColumn.codecId
980
- ? `${contractNativeType} (${resolvedContractColumn.codecId})`
981
- : contractNativeType;
982
- const columnMessage = aggregated.failureMessages.join('; ');
983
-
984
- return {
985
- status: aggregated.status,
986
- kind: 'column',
987
- name: `${columnName}: ${columnTypeDisplay} (${nullableText})`,
988
- contractPath: columnPath,
989
- code: aggregated.firstCode,
990
- message: columnMessage,
991
- expected: undefined,
992
- actual: undefined,
993
- children: columnChildren,
994
- };
995
- }
996
-
997
- function buildColumnsNode(
998
- tablePath: string,
999
- columnNodes: SchemaVerificationNode[],
1000
- ): SchemaVerificationNode {
1001
- return {
1002
- status: aggregateChildState(columnNodes, 'pass').status,
1003
- kind: 'columns',
1004
- name: 'columns',
1005
- contractPath: `${tablePath}.columns`,
1006
- code: '',
1007
- message: '',
1008
- expected: undefined,
1009
- actual: undefined,
1010
- children: columnNodes,
1011
- };
1012
- }
1013
-
1014
- function buildTableNode(
1015
- tableName: string,
1016
- tablePath: string,
1017
- tableChildren: SchemaVerificationNode[],
1018
- ): SchemaVerificationNode {
1019
- const tableStatus = aggregateChildState(tableChildren, 'pass').status;
1020
- const tableFailureMessages = tableChildren
1021
- .filter((child) => child.status === 'fail' && child.message)
1022
- .map((child) => child.message)
1023
- .filter((msg): msg is string => typeof msg === 'string' && msg.length > 0);
1024
- const tableMessage =
1025
- tableStatus === 'fail' && tableFailureMessages.length > 0
1026
- ? `${tableFailureMessages.length} issue${tableFailureMessages.length === 1 ? '' : 's'}`
1027
- : '';
1028
- const tableCode =
1029
- tableStatus === 'fail' && tableChildren.length > 0 && tableChildren[0]
1030
- ? tableChildren[0].code
1031
- : '';
1032
-
1033
- return {
1034
- status: tableStatus,
1035
- kind: 'table',
1036
- name: `table ${tableName}`,
1037
- contractPath: tablePath,
1038
- code: tableCode,
1039
- message: tableMessage,
1040
- expected: undefined,
1041
- actual: undefined,
1042
- children: tableChildren,
1043
- };
1044
- }
1045
-
1046
- function buildRootNode(rootChildren: SchemaVerificationNode[]): SchemaVerificationNode {
1047
- return {
1048
- status: aggregateChildState(rootChildren, 'pass').status,
1049
- kind: 'contract',
1050
- name: 'contract',
1051
- contractPath: '',
1052
- code: '',
1053
- message: '',
1054
- expected: undefined,
1055
- actual: undefined,
1056
- children: rootChildren,
1057
- };
1058
- }
1059
-
1060
- /**
1061
- * Aggregated state from child nodes, computed in a single pass.
1062
- */
1063
- interface AggregatedChildState {
1064
- readonly status: VerificationStatus;
1065
- readonly failureMessages: readonly string[];
1066
- readonly firstCode: string;
1067
- }
1068
-
1069
- /**
1070
- * Aggregates status, failure messages, and code from children in a single pass.
1071
- * This is more efficient than calling separate functions that each iterate the array.
1072
- */
1073
- function aggregateChildState(
1074
- children: SchemaVerificationNode[],
1075
- fallback: VerificationStatus,
1076
- ): AggregatedChildState {
1077
- let status: VerificationStatus = fallback;
1078
- const failureMessages: string[] = [];
1079
- let firstCode = '';
1080
-
1081
- for (const child of children) {
1082
- if (child.status === 'fail') {
1083
- status = 'fail';
1084
- if (!firstCode) {
1085
- firstCode = child.code;
1086
- }
1087
- if (child.message && typeof child.message === 'string' && child.message.length > 0) {
1088
- failureMessages.push(child.message);
1089
- }
1090
- } else if (child.status === 'warn' && status !== 'fail') {
1091
- status = 'warn';
1092
- if (!firstCode) {
1093
- firstCode = child.code;
1094
- }
1095
- }
1096
- }
1097
-
1098
- return { status, failureMessages, firstCode };
1099
- }
1100
-
1101
- function validateFrameworkComponentsForExtensions(
1102
- contract: Contract<SqlStorage>,
1103
- frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>,
1104
- ): void {
1105
- const contractExtensionPacks = contract.extensionPacks ?? {};
1106
- for (const extensionNamespace of Object.keys(contractExtensionPacks)) {
1107
- const hasComponent = frameworkComponents.some(
1108
- (component) =>
1109
- component.id === extensionNamespace &&
1110
- (component.kind === 'extension' ||
1111
- component.kind === 'adapter' ||
1112
- component.kind === 'target'),
1113
- );
1114
- if (!hasComponent) {
1115
- throw new Error(
1116
- `Extension pack '${extensionNamespace}' is declared in the contract but not found in framework components. ` +
1117
- 'This indicates a configuration mismatch - the contract was emitted with this extension pack, ' +
1118
- 'but it is not provided in the current configuration.',
1119
- );
1120
- }
1121
- }
1122
- }
1123
-
1124
- /**
1125
- * Renders the expected native type for a contract column, expanding parameterized types
1126
- * using codec control hooks when available.
1127
- *
1128
- * This function delegates to the `expandNativeType` hook if the codec provides one,
1129
- * ensuring that the SQL family layer remains dialect-agnostic while allowing
1130
- * target-specific adapters (like Postgres) to provide their own expansion logic.
1131
- */
1132
- function renderExpectedNativeType(
1133
- contractColumn: StorageColumn,
1134
- storageTypes: Readonly<Record<string, StorageTypeInstance>>,
1135
- codecHooks: Map<string, CodecControlHooks>,
1136
- context?: {
1137
- readonly tableName: string;
1138
- readonly columnName: string;
1139
- },
1140
- ): string {
1141
- const { codecId, nativeType, typeParams } = resolveContractColumnTypeMetadata(
1142
- contractColumn,
1143
- storageTypes,
1144
- context,
1145
- );
1146
-
1147
- // If no typeParams or codecId, return the base native type
1148
- if (!typeParams || !codecId) {
1149
- return nativeType;
1150
- }
1151
-
1152
- // Try to use the codec's expandNativeType hook if available
1153
- const hooks = codecHooks.get(codecId);
1154
- if (hooks?.expandNativeType) {
1155
- return hooks.expandNativeType({ nativeType, codecId, typeParams });
1156
- }
1157
-
1158
- // Fallback: return base native type if no hook is available
1159
- return nativeType;
1160
- }
1161
-
1162
- function resolveContractColumnTypeMetadata(
1163
- contractColumn: StorageColumn,
1164
- storageTypes: Readonly<Record<string, StorageTypeInstance>>,
1165
- context?: {
1166
- readonly tableName: string;
1167
- readonly columnName: string;
1168
- },
1169
- ): Pick<StorageColumn, 'codecId' | 'nativeType' | 'typeParams'> {
1170
- if (!contractColumn.typeRef) {
1171
- return contractColumn;
1172
- }
1173
-
1174
- const referencedType = storageTypes[contractColumn.typeRef];
1175
- if (!referencedType) {
1176
- const columnLabel = context
1177
- ? `Column "${context.tableName}"."${context.columnName}"`
1178
- : 'Column';
1179
- throw new Error(
1180
- `${columnLabel} references storage type "${contractColumn.typeRef}" but it is not defined in storage.types.`,
1181
- );
1182
- }
1183
-
1184
- if (isStorageTypeInstance(referencedType)) {
1185
- return {
1186
- codecId: referencedType.codecId,
1187
- nativeType: referencedType.nativeType,
1188
- typeParams: referencedType.typeParams,
1189
- };
1190
- }
1191
- throw new Error(
1192
- `Storage type "${contractColumn.typeRef}" has an unknown kind; expected a codec-typed StorageTypeInstance.`,
1193
- );
1194
- }
1195
-
1196
- /**
1197
- * Describes a column default for display purposes.
1198
- */
1199
- function describeColumnDefault(columnDefault: ColumnDefault): string {
1200
- switch (columnDefault.kind) {
1201
- case 'literal':
1202
- return `literal(${formatLiteralValue(columnDefault.value)})`;
1203
- case 'function':
1204
- return columnDefault.expression;
1205
- }
1206
- }
1207
-
1208
- /**
1209
- * Compares a contract ColumnDefault against a schema raw default string for semantic equality.
1210
- *
1211
- * When a normalizer is provided, the raw schema default is first normalized to a ColumnDefault
1212
- * before comparison. Without a normalizer, falls back to direct string comparison against
1213
- * the contract expression.
1214
- *
1215
- * @param contractDefault - The expected default from the contract (normalized ColumnDefault)
1216
- * @param schemaDefault - The raw default expression from the database (string)
1217
- * @param normalizer - Optional target-specific normalizer to convert raw defaults
1218
- * @param nativeType - The column's native type, passed to normalizer for context
1219
- */
1220
- function columnDefaultsEqual(
1221
- contractDefault: ColumnDefault,
1222
- schemaDefault: string,
1223
- normalizer?: DefaultNormalizer,
1224
- nativeType?: string,
1225
- ): boolean {
1226
- // If no normalizer provided, fall back to direct string comparison
1227
- if (!normalizer) {
1228
- if (contractDefault.kind === 'function') {
1229
- return contractDefault.expression === schemaDefault;
1230
- }
1231
- const normalizedValue = normalizeLiteralValue(contractDefault.value, nativeType);
1232
- if (typeof normalizedValue === 'string') {
1233
- return normalizedValue === schemaDefault || `'${normalizedValue}'` === schemaDefault;
1234
- }
1235
- return String(normalizedValue) === schemaDefault;
1236
- }
1237
-
1238
- // Normalize the raw schema default using target-specific logic
1239
- const normalizedSchema = normalizer(schemaDefault, nativeType ?? '');
1240
- if (!normalizedSchema) {
1241
- // Normalizer couldn't parse the expression - treat as mismatch
1242
- return false;
1243
- }
1244
-
1245
- // Compare normalized defaults
1246
- if (contractDefault.kind !== normalizedSchema.kind) {
1247
- return false;
1248
- }
1249
- if (contractDefault.kind === 'literal' && normalizedSchema.kind === 'literal') {
1250
- const contractValue = normalizeLiteralValue(contractDefault.value, nativeType);
1251
- const schemaValue = normalizeLiteralValue(normalizedSchema.value, nativeType);
1252
- return literalValuesEqual(contractValue, schemaValue);
1253
- }
1254
- if (contractDefault.kind === 'function' && normalizedSchema.kind === 'function') {
1255
- // Normalize function expressions for comparison (case-insensitive, whitespace-tolerant)
1256
- const normalizeExpr = (expr: string) => expr.toLowerCase().replace(/\s+/g, '');
1257
- return normalizeExpr(contractDefault.expression) === normalizeExpr(normalizedSchema.expression);
1258
- }
1259
- return false;
1260
- }
1261
-
1262
- function isTemporalNativeType(nativeType?: string): boolean {
1263
- if (!nativeType) return false;
1264
- const normalized = nativeType.toLowerCase();
1265
- return normalized.includes('timestamp') || normalized === 'date';
1266
- }
1267
-
1268
- function normalizeLiteralValue(value: unknown, nativeType?: string): unknown {
1269
- if (value instanceof Date) {
1270
- return value.toISOString();
1271
- }
1272
- if (typeof value === 'string' && isTemporalNativeType(nativeType)) {
1273
- const parsed = new Date(value);
1274
- if (!Number.isNaN(parsed.getTime())) {
1275
- return parsed.toISOString();
1276
- }
1277
- }
1278
- return value;
1279
- }
1280
-
1281
- function literalValuesEqual(a: unknown, b: unknown): boolean {
1282
- if (a === b) return true;
1283
- if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
1284
- return canonicalStringify(a) === canonicalStringify(b);
1285
- }
1286
- if (typeof a === 'object' && a !== null && typeof b === 'string') {
1287
- try {
1288
- return canonicalStringify(a) === canonicalStringify(JSON.parse(b));
1289
- } catch {
1290
- return false;
1291
- }
1292
- }
1293
- if (typeof a === 'string' && typeof b === 'object' && b !== null) {
1294
- try {
1295
- return canonicalStringify(JSON.parse(a)) === canonicalStringify(b);
1296
- } catch {
1297
- return false;
1298
- }
1299
- }
1300
- return false;
1301
- }
1302
-
1303
- function formatLiteralValue(value: unknown): string {
1304
- if (value instanceof Date) {
1305
- return value.toISOString();
1306
- }
1307
- if (typeof value === 'string') {
1308
- return value;
1309
- }
1310
- return JSON.stringify(value);
1311
- }