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