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