@prisma-next/family-sql 0.14.0-dev.4 → 0.14.0-dev.40
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/dist/{authoring-type-constructors-CjFfO6LM.mjs → authoring-type-constructors-CXd-8ydc.mjs} +7 -30
- package/dist/authoring-type-constructors-CXd-8ydc.mjs.map +1 -0
- package/dist/contract-to-schema-ir-S-evq8E6.mjs +264 -0
- package/dist/contract-to-schema-ir-S-evq8E6.mjs.map +1 -0
- package/dist/{control-adapter-Cmw9LvEP.d.mts → control-adapter-DHYFuOBy.d.mts} +12 -7
- package/dist/{control-adapter-Cmw9LvEP.d.mts.map → control-adapter-DHYFuOBy.d.mts.map} +1 -1
- package/dist/control-adapter.d.mts +1 -1
- package/dist/control.d.mts +18 -3
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +81 -943
- package/dist/control.mjs.map +1 -1
- package/dist/{schema-verify.d.mts → diff.d.mts} +5 -5
- package/dist/diff.d.mts.map +1 -0
- package/dist/{verify-sql-schema-xT4udQLQ.mjs → diff.mjs} +169 -253
- package/dist/diff.mjs.map +1 -0
- package/dist/ir.d.mts +9 -6
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +1 -1
- package/dist/migration.d.mts +11 -2
- package/dist/migration.d.mts.map +1 -1
- package/dist/migration.mjs +7 -0
- package/dist/migration.mjs.map +1 -1
- package/dist/pack.mjs +1 -1
- package/dist/psl-infer.d.mts +80 -0
- package/dist/psl-infer.d.mts.map +1 -0
- package/dist/psl-infer.mjs +334 -0
- package/dist/psl-infer.mjs.map +1 -0
- package/dist/{sql-contract-serializer-BR2vC7Z-.mjs → sql-contract-serializer-CYtXg_zs.mjs} +17 -20
- package/dist/sql-contract-serializer-CYtXg_zs.mjs.map +1 -0
- package/dist/{verify-sql-schema-thU-jKpf.d.mts → sql-schema-diff-6z36dZt6.d.mts} +44 -5
- package/dist/sql-schema-diff-6z36dZt6.d.mts.map +1 -0
- package/dist/{types-kgstZ_Zd.d.mts → types-CkOIJXxU.d.mts} +84 -17
- package/dist/types-CkOIJXxU.d.mts.map +1 -0
- package/package.json +23 -23
- package/src/core/authoring-entity-types.ts +12 -37
- package/src/core/control-adapter.ts +10 -5
- package/src/core/control-instance.ts +174 -41
- package/src/core/{schema-verify/verify-sql-schema.ts → diff/sql-schema-diff.ts} +208 -16
- package/src/core/{schema-verify → diff}/verify-helpers.ts +12 -0
- package/src/core/ir/sql-contract-serializer-base.ts +23 -55
- package/src/core/ir/sql-contract-serializer.ts +5 -7
- package/src/core/migrations/contract-to-schema-ir.ts +49 -12
- package/src/core/migrations/field-event-planner.ts +2 -2
- package/src/core/migrations/schema-differ.ts +40 -0
- package/src/core/migrations/types.ts +43 -2
- package/src/core/psl-contract-infer/printer-config.ts +0 -6
- package/src/core/sql-migration.ts +12 -1
- package/src/exports/control.ts +1 -0
- package/src/exports/diff.ts +18 -0
- package/src/exports/psl-infer.ts +36 -0
- package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +0 -1
- package/dist/schema-verify.d.mts.map +0 -1
- package/dist/schema-verify.mjs +0 -2
- package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +0 -1
- package/dist/test-utils.d.mts +0 -2
- package/dist/test-utils.mjs +0 -2
- package/dist/types-kgstZ_Zd.d.mts.map +0 -1
- package/dist/verify-sql-schema-thU-jKpf.d.mts.map +0 -1
- package/dist/verify-sql-schema-xT4udQLQ.mjs.map +0 -1
- package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
- package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
- package/src/exports/schema-verify.ts +0 -18
- package/src/exports/test-utils.ts +0 -9
- /package/src/core/{schema-verify → diff}/control-verify-emit.ts +0 -0
- /package/src/core/{schema-verify → diff}/verifier-disposition.ts +0 -0
|
@@ -24,8 +24,9 @@ import {
|
|
|
24
24
|
StorageTable,
|
|
25
25
|
type StorageTypeInstance,
|
|
26
26
|
} from '@prisma-next/sql-contract/types';
|
|
27
|
-
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
27
|
+
import type { SqlSchemaIR, SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
|
|
28
28
|
import { canonicalStringify } from '@prisma-next/utils/canonical-stringify';
|
|
29
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
29
30
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
30
31
|
import { extractCodecControlHooks } from '../assembly';
|
|
31
32
|
import { resolveValueSetValues } from '../migrations/contract-to-schema-ir';
|
|
@@ -42,6 +43,30 @@ import {
|
|
|
42
43
|
verifyUniqueConstraints,
|
|
43
44
|
} from './verify-helpers';
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Returns the per-schema namespace nodes of an introspected schema node, for
|
|
48
|
+
* the relational verify to consume one at a time. Structure-agnostic — imports
|
|
49
|
+
* no target node class. A root exposing a `namespaces` record (Postgres) yields
|
|
50
|
+
* its namespace nodes (never merged, so same-named tables in different schemas
|
|
51
|
+
* cannot collide); a flat schema (SQLite) is its own single namespace and
|
|
52
|
+
* yields itself. Handles spread-flattened input (own-enumerable fields survive).
|
|
53
|
+
*/
|
|
54
|
+
function namespaceSchemaNodes(schema: SqlSchemaIRNode): readonly SqlSchemaIR[] {
|
|
55
|
+
const obj = blindCast<
|
|
56
|
+
{ readonly namespaces?: Readonly<Record<string, SqlSchemaIR>> },
|
|
57
|
+
'structural read of an own-enumerable namespaces record; survives the projectSchemaToSpace spread'
|
|
58
|
+
>(schema);
|
|
59
|
+
if (obj.namespaces !== undefined) {
|
|
60
|
+
return Object.values(obj.namespaces);
|
|
61
|
+
}
|
|
62
|
+
return [
|
|
63
|
+
blindCast<
|
|
64
|
+
SqlSchemaIR,
|
|
65
|
+
'a flat schema node (no namespaces) is its own single namespace, exposing the per-schema { tables } shape'
|
|
66
|
+
>(schema),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
/**
|
|
46
71
|
* Function type for normalizing raw database default expressions into ColumnDefault.
|
|
47
72
|
* Target-specific implementations handle database dialect differences.
|
|
@@ -89,6 +114,15 @@ export interface VerifySqlSchemaOptions {
|
|
|
89
114
|
* with contract native types (e.g., Postgres 'varchar' → 'character varying').
|
|
90
115
|
*/
|
|
91
116
|
readonly normalizeNativeType?: NativeTypeNormalizer;
|
|
117
|
+
/**
|
|
118
|
+
* When set, only the contract tables in these namespace ids are checked
|
|
119
|
+
* against `schema` (the matching actual namespace node). The full contract is
|
|
120
|
+
* still consulted for cross-namespace value-set / control-policy resolution.
|
|
121
|
+
* Used by the multi-schema verify, which pairs each contract namespace to its
|
|
122
|
+
* own actual node. Absent ⇒ all contract namespaces are checked against the
|
|
123
|
+
* single (flat) `schema` — the single-schema / SQLite path.
|
|
124
|
+
*/
|
|
125
|
+
readonly restrictToNamespaceIds?: ReadonlySet<string>;
|
|
92
126
|
}
|
|
93
127
|
|
|
94
128
|
/**
|
|
@@ -128,6 +162,7 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
|
|
|
128
162
|
storageTypes,
|
|
129
163
|
...ifDefined('normalizeDefault', normalizeDefault),
|
|
130
164
|
...ifDefined('normalizeNativeType', normalizeNativeType),
|
|
165
|
+
...ifDefined('restrictToNamespaceIds', options.restrictToNamespaceIds),
|
|
131
166
|
});
|
|
132
167
|
|
|
133
168
|
validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
|
|
@@ -236,6 +271,7 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
|
|
|
236
271
|
},
|
|
237
272
|
schema: {
|
|
238
273
|
issues,
|
|
274
|
+
schemaDiffIssues: [],
|
|
239
275
|
root,
|
|
240
276
|
counts,
|
|
241
277
|
},
|
|
@@ -274,6 +310,7 @@ function verifySchemaTables(options: {
|
|
|
274
310
|
storageTypes: Readonly<Record<string, StorageTypeInstance>>;
|
|
275
311
|
normalizeDefault?: DefaultNormalizer;
|
|
276
312
|
normalizeNativeType?: NativeTypeNormalizer;
|
|
313
|
+
restrictToNamespaceIds?: ReadonlySet<string>;
|
|
277
314
|
}): { issues: SchemaIssue[]; rootChildren: SchemaVerificationNode[] } {
|
|
278
315
|
const {
|
|
279
316
|
contract,
|
|
@@ -284,6 +321,7 @@ function verifySchemaTables(options: {
|
|
|
284
321
|
storageTypes,
|
|
285
322
|
normalizeDefault,
|
|
286
323
|
normalizeNativeType,
|
|
324
|
+
restrictToNamespaceIds,
|
|
287
325
|
} = options;
|
|
288
326
|
const contractDefaultControl = contract.defaultControlPolicy;
|
|
289
327
|
const issues: SchemaIssue[] = [];
|
|
@@ -294,14 +332,17 @@ function verifySchemaTables(options: {
|
|
|
294
332
|
);
|
|
295
333
|
|
|
296
334
|
for (const namespaceId of namespaceIds) {
|
|
335
|
+
// When the caller pairs each contract namespace to its own actual node, it
|
|
336
|
+
// restricts the table check to that namespace; the full contract is still
|
|
337
|
+
// consulted for value-set / control-policy resolution.
|
|
338
|
+
if (restrictToNamespaceIds !== undefined && !restrictToNamespaceIds.has(namespaceId)) continue;
|
|
297
339
|
const ns = contract.storage.namespaces[namespaceId];
|
|
298
340
|
if (!ns) continue;
|
|
299
341
|
for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table ?? {})) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
342
|
+
StorageTable.assert(
|
|
343
|
+
contractTableRaw,
|
|
344
|
+
`storage.namespaces.${namespaceId}.entries.table.${tableName}`,
|
|
345
|
+
);
|
|
305
346
|
const contractTable = contractTableRaw;
|
|
306
347
|
const tableControlPolicy = effectiveControlPolicy(
|
|
307
348
|
contractTable.control,
|
|
@@ -313,6 +354,7 @@ function verifySchemaTables(options: {
|
|
|
313
354
|
if (!schemaTable) {
|
|
314
355
|
const issue: SchemaIssue = {
|
|
315
356
|
kind: 'missing_table',
|
|
357
|
+
reason: 'not-found',
|
|
316
358
|
table: tableName,
|
|
317
359
|
namespaceId,
|
|
318
360
|
message: `Table "${tableName}" is missing from database`,
|
|
@@ -367,6 +409,7 @@ function verifySchemaTables(options: {
|
|
|
367
409
|
const extraTableControlPolicy = effectiveControlPolicy(undefined, contractDefaultControl);
|
|
368
410
|
const issue: SchemaIssue = {
|
|
369
411
|
kind: 'extra_table',
|
|
412
|
+
reason: 'not-expected',
|
|
370
413
|
table: tableName,
|
|
371
414
|
message: `Extra table "${tableName}" found in database (not in contract)`,
|
|
372
415
|
};
|
|
@@ -383,6 +426,7 @@ function verifySchemaTables(options: {
|
|
|
383
426
|
expected: undefined,
|
|
384
427
|
actual: undefined,
|
|
385
428
|
children: [],
|
|
429
|
+
reason: 'not-expected',
|
|
386
430
|
},
|
|
387
431
|
issues,
|
|
388
432
|
rootChildren,
|
|
@@ -507,6 +551,7 @@ function verifyTableChildren(options: {
|
|
|
507
551
|
} else if (schemaTable.primaryKey && strict) {
|
|
508
552
|
const issue: SchemaIssue = {
|
|
509
553
|
kind: 'extra_primary_key',
|
|
554
|
+
reason: 'not-expected',
|
|
510
555
|
table: tableName,
|
|
511
556
|
namespaceId,
|
|
512
557
|
message: 'Extra primary key found in database (not in contract)',
|
|
@@ -650,6 +695,7 @@ function collectContractColumnNodes(options: {
|
|
|
650
695
|
if (!schemaColumn) {
|
|
651
696
|
const issue: SchemaIssue = {
|
|
652
697
|
kind: 'missing_column',
|
|
698
|
+
reason: 'not-found',
|
|
653
699
|
table: tableName,
|
|
654
700
|
namespaceId,
|
|
655
701
|
column: columnName,
|
|
@@ -722,6 +768,7 @@ function appendExtraColumnNodes(options: {
|
|
|
722
768
|
if (!contractTable.columns[columnName]) {
|
|
723
769
|
const issue: SchemaIssue = {
|
|
724
770
|
kind: 'extra_column',
|
|
771
|
+
reason: 'not-expected',
|
|
725
772
|
table: tableName,
|
|
726
773
|
namespaceId,
|
|
727
774
|
column: columnName,
|
|
@@ -790,14 +837,16 @@ function verifyColumn(options: {
|
|
|
790
837
|
tableName,
|
|
791
838
|
columnName,
|
|
792
839
|
});
|
|
793
|
-
const
|
|
840
|
+
const schemaBaseNativeType =
|
|
794
841
|
normalizeNativeType?.(schemaColumn.nativeType) ?? schemaColumn.nativeType;
|
|
842
|
+
const schemaNativeType = schemaColumn.many ? `${schemaBaseNativeType}[]` : schemaBaseNativeType;
|
|
795
843
|
|
|
796
844
|
const typesMatch = contractNativeType === schemaNativeType;
|
|
797
845
|
|
|
798
846
|
if (!typesMatch) {
|
|
799
847
|
const issue: SchemaIssue = {
|
|
800
848
|
kind: 'type_mismatch',
|
|
849
|
+
reason: 'not-equal',
|
|
801
850
|
table: tableName,
|
|
802
851
|
namespaceId,
|
|
803
852
|
column: columnName,
|
|
@@ -858,6 +907,7 @@ function verifyColumn(options: {
|
|
|
858
907
|
if (contractColumn.nullable !== schemaColumn.nullable) {
|
|
859
908
|
const issue: SchemaIssue = {
|
|
860
909
|
kind: 'nullability_mismatch',
|
|
910
|
+
reason: 'not-equal',
|
|
861
911
|
table: tableName,
|
|
862
912
|
namespaceId,
|
|
863
913
|
column: columnName,
|
|
@@ -888,6 +938,7 @@ function verifyColumn(options: {
|
|
|
888
938
|
const defaultDescription = describeColumnDefault(contractColumn.default);
|
|
889
939
|
const issue: SchemaIssue = {
|
|
890
940
|
kind: 'default_missing',
|
|
941
|
+
reason: 'not-found',
|
|
891
942
|
table: tableName,
|
|
892
943
|
namespaceId,
|
|
893
944
|
column: columnName,
|
|
@@ -922,6 +973,7 @@ function verifyColumn(options: {
|
|
|
922
973
|
const actualDescription = schemaColumn.default;
|
|
923
974
|
const issue: SchemaIssue = {
|
|
924
975
|
kind: 'default_mismatch',
|
|
976
|
+
reason: 'not-equal',
|
|
925
977
|
table: tableName,
|
|
926
978
|
namespaceId,
|
|
927
979
|
column: columnName,
|
|
@@ -949,6 +1001,7 @@ function verifyColumn(options: {
|
|
|
949
1001
|
} else if (strict && schemaColumn.default) {
|
|
950
1002
|
const issue: SchemaIssue = {
|
|
951
1003
|
kind: 'extra_default',
|
|
1004
|
+
reason: 'not-expected',
|
|
952
1005
|
table: tableName,
|
|
953
1006
|
namespaceId,
|
|
954
1007
|
column: columnName,
|
|
@@ -1144,19 +1197,20 @@ function renderExpectedNativeType(
|
|
|
1144
1197
|
context,
|
|
1145
1198
|
);
|
|
1146
1199
|
|
|
1200
|
+
let baseType: string;
|
|
1201
|
+
|
|
1147
1202
|
// If no typeParams or codecId, return the base native type
|
|
1148
1203
|
if (!typeParams || !codecId) {
|
|
1149
|
-
|
|
1204
|
+
baseType = nativeType;
|
|
1205
|
+
} else {
|
|
1206
|
+
// Try to use the codec's expandNativeType hook if available
|
|
1207
|
+
const hooks = codecHooks.get(codecId);
|
|
1208
|
+
baseType = hooks?.expandNativeType
|
|
1209
|
+
? hooks.expandNativeType({ nativeType, codecId, typeParams })
|
|
1210
|
+
: nativeType;
|
|
1150
1211
|
}
|
|
1151
1212
|
|
|
1152
|
-
|
|
1153
|
-
const hooks = codecHooks.get(codecId);
|
|
1154
|
-
if (hooks?.expandNativeType) {
|
|
1155
|
-
return hooks.expandNativeType({ nativeType, codecId, typeParams });
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// Fallback: return base native type if no hook is available
|
|
1159
|
-
return nativeType;
|
|
1213
|
+
return contractColumn.many ? `${baseType}[]` : baseType;
|
|
1160
1214
|
}
|
|
1161
1215
|
|
|
1162
1216
|
function resolveContractColumnTypeMetadata(
|
|
@@ -1309,3 +1363,141 @@ function formatLiteralValue(value: unknown): string {
|
|
|
1309
1363
|
}
|
|
1310
1364
|
return JSON.stringify(value);
|
|
1311
1365
|
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Reads a namespace node's DDL schema name. Namespaced target nodes (Postgres
|
|
1369
|
+
* `PostgresNamespaceSchemaNode`) carry `schemaName`; a flat schema (SQLite) has
|
|
1370
|
+
* none and pairs by position as the sole namespace.
|
|
1371
|
+
*/
|
|
1372
|
+
function namespaceSchemaName(node: SqlSchemaIR): string | undefined {
|
|
1373
|
+
return blindCast<
|
|
1374
|
+
{ readonly schemaName?: string },
|
|
1375
|
+
'reading the optional namespace schemaName off a per-schema node'
|
|
1376
|
+
>(node).schemaName;
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/**
|
|
1380
|
+
* Returns a shallow copy of `contract` exposing only the one named namespace —
|
|
1381
|
+
* used solely to resolve that namespace's live DDL schema via the target's
|
|
1382
|
+
* expected-tree projection (never for verification, so global value-sets it may
|
|
1383
|
+
* reference are not consulted).
|
|
1384
|
+
*/
|
|
1385
|
+
function scopeContractToNamespace(
|
|
1386
|
+
contract: Contract<SqlStorage>,
|
|
1387
|
+
namespaceId: string,
|
|
1388
|
+
): Contract<SqlStorage> {
|
|
1389
|
+
const namespace = contract.storage.namespaces[namespaceId];
|
|
1390
|
+
const scopedNamespaces = namespace === undefined ? {} : { [namespaceId]: namespace };
|
|
1391
|
+
return blindCast<
|
|
1392
|
+
Contract<SqlStorage>,
|
|
1393
|
+
'narrowing storage.namespaces to one entry; the rest of the contract is preserved'
|
|
1394
|
+
>({
|
|
1395
|
+
...contract,
|
|
1396
|
+
storage: blindCast<
|
|
1397
|
+
SqlStorage,
|
|
1398
|
+
'shallow storage copy with a single-namespace map; other storage fields are preserved'
|
|
1399
|
+
>({
|
|
1400
|
+
...contract.storage,
|
|
1401
|
+
namespaces: scopedNamespaces,
|
|
1402
|
+
}),
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/**
|
|
1407
|
+
* Combines two `VerifyDatabaseSchemaResult`s by concatenating issues and summing
|
|
1408
|
+
* counts — used to fold the per-namespace pairings of a multi-schema database
|
|
1409
|
+
* into one result. The verification-tree `root` of the first pairing is
|
|
1410
|
+
* retained (multi-schema verify-tree shaping is future work).
|
|
1411
|
+
*/
|
|
1412
|
+
function mergeVerifyResults(
|
|
1413
|
+
a: VerifyDatabaseSchemaResult,
|
|
1414
|
+
b: VerifyDatabaseSchemaResult,
|
|
1415
|
+
): VerifyDatabaseSchemaResult {
|
|
1416
|
+
return {
|
|
1417
|
+
...a,
|
|
1418
|
+
ok: a.ok && b.ok,
|
|
1419
|
+
...ifDefined('code', a.code ?? b.code),
|
|
1420
|
+
schema: {
|
|
1421
|
+
...a.schema,
|
|
1422
|
+
issues: [...a.schema.issues, ...b.schema.issues],
|
|
1423
|
+
schemaDiffIssues: [...a.schema.schemaDiffIssues, ...b.schema.schemaDiffIssues],
|
|
1424
|
+
counts: {
|
|
1425
|
+
pass: a.schema.counts.pass + b.schema.counts.pass,
|
|
1426
|
+
warn: a.schema.counts.warn + b.schema.counts.warn,
|
|
1427
|
+
fail: a.schema.counts.fail + b.schema.counts.fail,
|
|
1428
|
+
totalNodes: a.schema.counts.totalNodes + b.schema.counts.totalNodes,
|
|
1429
|
+
},
|
|
1430
|
+
},
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
/**
|
|
1435
|
+
* The single per-namespace-paired relational verify shared by the migration
|
|
1436
|
+
* planner and the family schema verify — there is exactly one such operation.
|
|
1437
|
+
*
|
|
1438
|
+
* Each contract namespace is paired to the introspected namespace node holding
|
|
1439
|
+
* the same DDL schema, then `verifySqlSchema` checks that namespace's tables
|
|
1440
|
+
* against the matching actual node (a contract table under `auth` is only ever
|
|
1441
|
+
* looked up in the `auth` actual node, so a multi-schema database no longer
|
|
1442
|
+
* reports tables in other schemas as missing). The full contract is passed every
|
|
1443
|
+
* time — `restrictToNamespaceIds` scopes only which tables are checked, so
|
|
1444
|
+
* cross-namespace value-set / control-policy resolution is unaffected.
|
|
1445
|
+
*
|
|
1446
|
+
* The DDL schema of each contract namespace is read from a single-namespace
|
|
1447
|
+
* expected projection (`buildExpectedSchema`), which both callers already build
|
|
1448
|
+
* the same way. Empty contract namespaces verify nothing and are skipped.
|
|
1449
|
+
* Single-schema (one namespace) and SQLite's flat schema are one pairing —
|
|
1450
|
+
* byte-identical to the prior per-node verify.
|
|
1451
|
+
*/
|
|
1452
|
+
export function verifySqlSchemaTree(options: {
|
|
1453
|
+
readonly contract: Contract<SqlStorage>;
|
|
1454
|
+
readonly actualSchema: SqlSchemaIRNode;
|
|
1455
|
+
readonly buildExpectedSchema: (contract: Contract<SqlStorage>) => SqlSchemaIRNode;
|
|
1456
|
+
readonly strict: boolean;
|
|
1457
|
+
readonly typeMetadataRegistry: ReadonlyMap<string, { nativeType?: string }>;
|
|
1458
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
1459
|
+
readonly normalizeDefault?: DefaultNormalizer;
|
|
1460
|
+
readonly normalizeNativeType?: NativeTypeNormalizer;
|
|
1461
|
+
}): VerifyDatabaseSchemaResult {
|
|
1462
|
+
const baseOptions = {
|
|
1463
|
+
contract: options.contract,
|
|
1464
|
+
strict: options.strict,
|
|
1465
|
+
typeMetadataRegistry: options.typeMetadataRegistry,
|
|
1466
|
+
frameworkComponents: options.frameworkComponents,
|
|
1467
|
+
...ifDefined('normalizeDefault', options.normalizeDefault),
|
|
1468
|
+
...ifDefined('normalizeNativeType', options.normalizeNativeType),
|
|
1469
|
+
};
|
|
1470
|
+
|
|
1471
|
+
const actualNodes = namespaceSchemaNodes(options.actualSchema);
|
|
1472
|
+
const actualByName = new Map<string, SqlSchemaIR>();
|
|
1473
|
+
for (const node of actualNodes) {
|
|
1474
|
+
const name = namespaceSchemaName(node);
|
|
1475
|
+
if (name !== undefined) actualByName.set(name, node);
|
|
1476
|
+
}
|
|
1477
|
+
// A flat actual schema (SQLite) has no named namespaces — it is the sole node.
|
|
1478
|
+
const soleFlatActual = actualByName.size === 0 ? actualNodes[0] : undefined;
|
|
1479
|
+
const emptyNamespace: SqlSchemaIR = { tables: {} };
|
|
1480
|
+
|
|
1481
|
+
let combined: VerifyDatabaseSchemaResult | undefined;
|
|
1482
|
+
for (const namespaceId of Object.keys(options.contract.storage.namespaces)) {
|
|
1483
|
+
const namespace = options.contract.storage.namespaces[namespaceId];
|
|
1484
|
+
if (!namespace || Object.keys(namespace.entries.table ?? {}).length === 0) continue;
|
|
1485
|
+
|
|
1486
|
+
const ddlSchema = namespaceSchemaNodes(
|
|
1487
|
+
options.buildExpectedSchema(scopeContractToNamespace(options.contract, namespaceId)),
|
|
1488
|
+
)
|
|
1489
|
+
.map(namespaceSchemaName)
|
|
1490
|
+
.find((name) => name !== undefined);
|
|
1491
|
+
const actualNode =
|
|
1492
|
+
(ddlSchema !== undefined ? actualByName.get(ddlSchema) : soleFlatActual) ?? emptyNamespace;
|
|
1493
|
+
|
|
1494
|
+
const result = verifySqlSchema({
|
|
1495
|
+
...baseOptions,
|
|
1496
|
+
schema: actualNode,
|
|
1497
|
+
restrictToNamespaceIds: new Set([namespaceId]),
|
|
1498
|
+
});
|
|
1499
|
+
combined = combined === undefined ? result : mergeVerifyResults(combined, result);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
return combined ?? verifySqlSchema({ ...baseOptions, schema: emptyNamespace });
|
|
1503
|
+
}
|
|
@@ -151,6 +151,7 @@ export function verifyPrimaryKey(
|
|
|
151
151
|
if (!schemaPK) {
|
|
152
152
|
const issue: SchemaIssue = {
|
|
153
153
|
kind: 'primary_key_mismatch',
|
|
154
|
+
reason: 'not-equal',
|
|
154
155
|
table: tableName,
|
|
155
156
|
namespaceId,
|
|
156
157
|
expected: contractPK.columns.join(', '),
|
|
@@ -163,6 +164,7 @@ export function verifyPrimaryKey(
|
|
|
163
164
|
if (!arraysEqual(contractPK.columns, schemaPK.columns)) {
|
|
164
165
|
const issue: SchemaIssue = {
|
|
165
166
|
kind: 'primary_key_mismatch',
|
|
167
|
+
reason: 'not-equal',
|
|
166
168
|
table: tableName,
|
|
167
169
|
namespaceId,
|
|
168
170
|
expected: contractPK.columns.join(', '),
|
|
@@ -220,6 +222,7 @@ export function verifyForeignKeys(
|
|
|
220
222
|
if (!matchingFK) {
|
|
221
223
|
const issue: SchemaIssue = {
|
|
222
224
|
kind: 'foreign_key_mismatch',
|
|
225
|
+
reason: 'not-equal',
|
|
223
226
|
table: tableName,
|
|
224
227
|
namespaceId,
|
|
225
228
|
expected: `${contractFK.source.columns.join(', ')} -> ${contractFK.target.tableName}(${contractFK.target.columns.join(', ')})`,
|
|
@@ -250,6 +253,7 @@ export function verifyForeignKeys(
|
|
|
250
253
|
const combinedActual = actionMismatches.map((m) => m.actual).join(', ');
|
|
251
254
|
const issue: SchemaIssue = {
|
|
252
255
|
kind: 'foreign_key_mismatch',
|
|
256
|
+
reason: 'not-equal',
|
|
253
257
|
table: tableName,
|
|
254
258
|
namespaceId,
|
|
255
259
|
indexOrConstraint: matchingFK.name ?? `fk(${contractFK.source.columns.join(',')})`,
|
|
@@ -309,6 +313,7 @@ export function verifyForeignKeys(
|
|
|
309
313
|
if (!matchingFK) {
|
|
310
314
|
const issue: SchemaIssue = {
|
|
311
315
|
kind: 'extra_foreign_key',
|
|
316
|
+
reason: 'not-expected',
|
|
312
317
|
table: tableName,
|
|
313
318
|
namespaceId,
|
|
314
319
|
indexOrConstraint: schemaFK.name ?? `fk(${schemaFK.columns.join(',')})`,
|
|
@@ -379,6 +384,7 @@ export function verifyUniqueConstraints(
|
|
|
379
384
|
if (!matchingUnique && !matchingUniqueIndex) {
|
|
380
385
|
const issue: SchemaIssue = {
|
|
381
386
|
kind: 'unique_constraint_mismatch',
|
|
387
|
+
reason: 'not-equal',
|
|
382
388
|
table: tableName,
|
|
383
389
|
namespaceId,
|
|
384
390
|
expected: contractUnique.columns.join(', '),
|
|
@@ -425,6 +431,7 @@ export function verifyUniqueConstraints(
|
|
|
425
431
|
if (!matchingUnique) {
|
|
426
432
|
const issue: SchemaIssue = {
|
|
427
433
|
kind: 'extra_unique_constraint',
|
|
434
|
+
reason: 'not-expected',
|
|
428
435
|
table: tableName,
|
|
429
436
|
namespaceId,
|
|
430
437
|
indexOrConstraint: schemaUnique.name ?? `unique(${schemaUnique.columns.join(',')})`,
|
|
@@ -501,6 +508,7 @@ export function verifyIndexes(
|
|
|
501
508
|
if (!matchingIndex && !matchingUniqueConstraint) {
|
|
502
509
|
const issue: SchemaIssue = {
|
|
503
510
|
kind: 'index_mismatch',
|
|
511
|
+
reason: 'not-equal',
|
|
504
512
|
table: tableName,
|
|
505
513
|
namespaceId,
|
|
506
514
|
expected: contractIndex.columns.join(', '),
|
|
@@ -552,6 +560,7 @@ export function verifyIndexes(
|
|
|
552
560
|
if (!matchingIndex) {
|
|
553
561
|
const issue: SchemaIssue = {
|
|
554
562
|
kind: 'extra_index',
|
|
563
|
+
reason: 'not-expected',
|
|
555
564
|
table: tableName,
|
|
556
565
|
namespaceId,
|
|
557
566
|
indexOrConstraint: schemaIndex.name ?? `idx(${schemaIndex.columns.join(',')})`,
|
|
@@ -718,6 +727,7 @@ export function verifyCheckConstraints(
|
|
|
718
727
|
if (!liveCheck) {
|
|
719
728
|
const issue: SchemaIssue = {
|
|
720
729
|
kind: 'check_missing',
|
|
730
|
+
reason: 'not-found',
|
|
721
731
|
table: tableName,
|
|
722
732
|
namespaceId,
|
|
723
733
|
indexOrConstraint: contractCheck.name,
|
|
@@ -744,6 +754,7 @@ export function verifyCheckConstraints(
|
|
|
744
754
|
} else if (!valueSetsEqual(contractCheck.permittedValues, liveCheck.permittedValues)) {
|
|
745
755
|
const issue: SchemaIssue = {
|
|
746
756
|
kind: 'check_mismatch',
|
|
757
|
+
reason: 'not-equal',
|
|
747
758
|
table: tableName,
|
|
748
759
|
namespaceId,
|
|
749
760
|
indexOrConstraint: contractCheck.name,
|
|
@@ -789,6 +800,7 @@ export function verifyCheckConstraints(
|
|
|
789
800
|
if (!matchingContract) {
|
|
790
801
|
const issue: SchemaIssue = {
|
|
791
802
|
kind: 'check_removed',
|
|
803
|
+
reason: 'not-equal',
|
|
792
804
|
table: tableName,
|
|
793
805
|
namespaceId,
|
|
794
806
|
indexOrConstraint: liveCheck.name,
|
|
@@ -6,18 +6,15 @@ import {
|
|
|
6
6
|
type AnyEntityKindDescriptor,
|
|
7
7
|
hydrateNamespaceEntities,
|
|
8
8
|
type Namespace,
|
|
9
|
-
NamespaceBase,
|
|
10
|
-
UNBOUND_NAMESPACE_ID,
|
|
11
9
|
} from '@prisma-next/framework-components/ir';
|
|
12
10
|
import { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';
|
|
13
11
|
import { composeSqlEntityKinds } from '@prisma-next/sql-contract/entity-kinds';
|
|
14
12
|
import {
|
|
15
|
-
|
|
16
|
-
type
|
|
13
|
+
isMaterializedSqlNamespace,
|
|
14
|
+
type SqlNamespaceInput,
|
|
17
15
|
SqlStorage,
|
|
18
16
|
type SqlStorageInput,
|
|
19
17
|
type SqlStorageTypeEntry,
|
|
20
|
-
SqlUnboundNamespace,
|
|
21
18
|
} from '@prisma-next/sql-contract/types';
|
|
22
19
|
import {
|
|
23
20
|
createSqlContractSchema,
|
|
@@ -120,77 +117,48 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
120
117
|
'structural',
|
|
121
118
|
);
|
|
122
119
|
}
|
|
123
|
-
const hydratedNamespaces = this.hydrateSqlNamespaceMap(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// deserialized JSON (e.g. buildMixedPolyContract) working by providing a slot to
|
|
130
|
-
// write into. Once runtime-qualification routes table lookups by namespace, this
|
|
131
|
-
// shim should be removed.
|
|
132
|
-
//
|
|
133
|
-
// TML-2916: the shim only fires when the target's default namespace IS unbound
|
|
134
|
-
// (SQLite, Mongo). On Postgres (`defaultNamespaceId === 'public'`) injecting an
|
|
135
|
-
// empty `__unbound__` slot violates ADR 223 — un-namespaced PG models belong in
|
|
136
|
-
// `public`, not `__unbound__`.
|
|
137
|
-
const withInjectedUnbound =
|
|
138
|
-
this.defaultNamespaceId === UNBOUND_NAMESPACE_ID
|
|
139
|
-
? {
|
|
140
|
-
...hydratedNamespaces,
|
|
141
|
-
[UNBOUND_NAMESPACE_ID]:
|
|
142
|
-
hydratedNamespaces[UNBOUND_NAMESPACE_ID] ?? SqlUnboundNamespace.instance,
|
|
143
|
-
}
|
|
144
|
-
: hydratedNamespaces;
|
|
120
|
+
const hydratedNamespaces = this.hydrateSqlNamespaceMap(
|
|
121
|
+
blindCast<
|
|
122
|
+
Readonly<Record<string, Record<string, unknown>>>,
|
|
123
|
+
'parseSqlContractStructure validated raw JSON; namespace entries are plain objects, not SqlNamespace instances.'
|
|
124
|
+
>(rawNamespaces),
|
|
125
|
+
);
|
|
145
126
|
|
|
146
127
|
return {
|
|
147
128
|
...validated,
|
|
148
129
|
storage: new SqlStorage({
|
|
149
130
|
storageHash: validated.storage.storageHash,
|
|
150
131
|
...ifDefined('types', hydratedTypes),
|
|
151
|
-
// Cast narrows the result of hydrateSqlNamespaceMap from the wider
|
|
152
|
-
// framework `Namespace` to the SQL-family `SqlNamespace`.
|
|
153
132
|
namespaces: blindCast<
|
|
154
133
|
SqlStorageInput['namespaces'],
|
|
155
|
-
'hydrateSqlNamespaceMap builds each namespace through the
|
|
156
|
-
>(
|
|
134
|
+
'hydrateSqlNamespaceMap builds each namespace through the target serializer override, so every value is a SqlNamespace; the framework return type only promises the base Namespace.'
|
|
135
|
+
>(hydratedNamespaces),
|
|
157
136
|
}),
|
|
158
137
|
};
|
|
159
138
|
}
|
|
160
139
|
|
|
161
|
-
protected abstract get defaultNamespaceId(): string;
|
|
162
|
-
|
|
163
140
|
protected hydrateSqlNamespaceMap(
|
|
164
|
-
namespaces: Readonly<Record<string,
|
|
141
|
+
namespaces: Readonly<Record<string, Record<string, unknown>>>,
|
|
165
142
|
): Readonly<Record<string, Namespace>> {
|
|
166
143
|
return Object.fromEntries(
|
|
167
144
|
Object.entries(namespaces).map(([nsId, namespaceEntryRaw]) => {
|
|
168
|
-
// Raw entries passed structural validation; hydrate materialises family IR class instances.
|
|
169
145
|
const namespaceHydrated = this.hydrateSqlNamespaceEntry(nsId, namespaceEntryRaw);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
'hydrateSqlNamespaceEntry returns SqlNamespaceTablesInput when raw is not a NamespaceBase'
|
|
177
|
-
>(namespaceHydrated),
|
|
178
|
-
);
|
|
179
|
-
return [nsId, namespaceMaterialised];
|
|
146
|
+
if (!isMaterializedSqlNamespace(namespaceHydrated)) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`Target serializer bug: hydrateSqlNamespaceEntry for namespace "${nsId}" returned a non-NamespaceBase value. Override hydrateSqlNamespaceEntry to produce a target namespace concretion.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return [nsId, namespaceHydrated];
|
|
180
152
|
}),
|
|
181
153
|
);
|
|
182
154
|
}
|
|
183
155
|
|
|
184
156
|
protected hydrateSqlNamespaceEntry(
|
|
185
157
|
nsId: string,
|
|
186
|
-
raw:
|
|
187
|
-
): Namespace |
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
const rawRecord = isPlainRecord(raw) ? raw : {};
|
|
192
|
-
const id = typeof rawRecord['id'] === 'string' ? rawRecord['id'] : nsId;
|
|
193
|
-
const parsed = NamespaceRawSchema({ ...rawRecord, id });
|
|
158
|
+
raw: Record<string, unknown>,
|
|
159
|
+
): Namespace | SqlNamespaceInput {
|
|
160
|
+
const id = typeof raw['id'] === 'string' ? raw['id'] : nsId;
|
|
161
|
+
const parsed = NamespaceRawSchema({ ...raw, id });
|
|
194
162
|
if (parsed instanceof type.errors) {
|
|
195
163
|
const messages = parsed.map((p: { message: string }) => p.message).join('; ');
|
|
196
164
|
throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');
|
|
@@ -211,8 +179,8 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
211
179
|
}
|
|
212
180
|
|
|
213
181
|
return blindCast<
|
|
214
|
-
|
|
215
|
-
'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the
|
|
182
|
+
SqlNamespaceInput,
|
|
183
|
+
'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the SqlNamespaceInput the target createNamespace consumes.'
|
|
216
184
|
>({
|
|
217
185
|
id,
|
|
218
186
|
entries: entriesOutput,
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
-
import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
|
|
3
2
|
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
4
3
|
import { SqlContractSerializerBase } from './sql-contract-serializer-base';
|
|
5
4
|
|
|
@@ -11,15 +10,14 @@ import { SqlContractSerializerBase } from './sql-contract-serializer-base';
|
|
|
11
10
|
* codec-typed envelope wire a target-specific subclass with a populated
|
|
12
11
|
* registry (see Postgres). Family-level call sites instantiate this
|
|
13
12
|
* default directly when no target serializer is supplied.
|
|
13
|
+
*
|
|
14
|
+
* Because this serializer has no target concretion, deserialization of
|
|
15
|
+
* contracts that include namespace entries from JSON will throw unless
|
|
16
|
+
* the caller provides pre-hydrated `NamespaceBase` instances. Production
|
|
17
|
+
* paths always supply a target-specific serializer.
|
|
14
18
|
*/
|
|
15
19
|
export class SqlContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {
|
|
16
20
|
constructor() {
|
|
17
21
|
super(new Map());
|
|
18
22
|
}
|
|
19
|
-
|
|
20
|
-
// Family-level fallback when no target descriptor is wired in. Preserves the
|
|
21
|
-
// pre-TML-2916 compatibility-shim behaviour for the unbound slot.
|
|
22
|
-
protected override get defaultNamespaceId(): string {
|
|
23
|
-
return UNBOUND_NAMESPACE_ID;
|
|
24
|
-
}
|
|
25
23
|
}
|