@prisma-next/family-sql 0.14.0-dev.39 → 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/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-B2q9WN1V.d.mts → control-adapter-DHYFuOBy.d.mts} +14 -17
- package/dist/{control-adapter-B2q9WN1V.d.mts.map → control-adapter-DHYFuOBy.d.mts.map} +1 -1
- package/dist/control-adapter.d.mts +1 -1
- package/dist/control.d.mts +17 -2
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +42 -943
- package/dist/control.mjs.map +1 -1
- package/dist/{schema-verify.d.mts → diff.d.mts} +4 -4
- package/dist/diff.d.mts.map +1 -0
- package/dist/{verify-sql-schema-CEY7b78t.mjs → diff.mjs} +154 -244
- package/dist/diff.mjs.map +1 -0
- package/dist/migration.d.mts +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/{verify-sql-schema-B8uEjw2s.d.mts → sql-schema-diff-6z36dZt6.d.mts} +43 -4
- package/dist/sql-schema-diff-6z36dZt6.d.mts.map +1 -0
- package/dist/{types-BxsFkHYv.d.mts → types-CkOIJXxU.d.mts} +83 -16
- package/dist/types-CkOIJXxU.d.mts.map +1 -0
- package/package.json +23 -22
- package/src/core/control-adapter.ts +12 -24
- package/src/core/control-instance.ts +113 -45
- package/src/core/{schema-verify/verify-sql-schema.ts → diff/sql-schema-diff.ts} +196 -8
- package/src/core/{schema-verify → diff}/verify-helpers.ts +12 -0
- package/src/core/migrations/contract-to-schema-ir.ts +48 -13
- package/src/core/migrations/field-event-planner.ts +3 -8
- 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/exports/control.ts +1 -0
- package/src/exports/diff.ts +18 -0
- package/src/exports/psl-infer.ts +36 -0
- package/dist/schema-verify.d.mts.map +0 -1
- package/dist/schema-verify.mjs +0 -2
- package/dist/types-BxsFkHYv.d.mts.map +0 -1
- package/dist/verify-sql-schema-B8uEjw2s.d.mts.map +0 -1
- package/dist/verify-sql-schema-CEY7b78t.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/core/{schema-verify → diff}/control-verify-emit.ts +0 -0
- /package/src/core/{schema-verify → diff}/verifier-disposition.ts +0 -0
|
@@ -18,15 +18,15 @@ import type {
|
|
|
18
18
|
} from '@prisma-next/framework-components/control';
|
|
19
19
|
|
|
20
20
|
import {
|
|
21
|
-
isStorageTable,
|
|
22
21
|
isStorageTypeInstance,
|
|
23
22
|
type SqlStorage,
|
|
24
23
|
type StorageColumn,
|
|
25
|
-
|
|
24
|
+
StorageTable,
|
|
26
25
|
type StorageTypeInstance,
|
|
27
26
|
} from '@prisma-next/sql-contract/types';
|
|
28
|
-
import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
|
|
27
|
+
import type { SqlSchemaIR, SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
|
|
29
28
|
import { canonicalStringify } from '@prisma-next/utils/canonical-stringify';
|
|
29
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
30
30
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
31
31
|
import { extractCodecControlHooks } from '../assembly';
|
|
32
32
|
import { resolveValueSetValues } from '../migrations/contract-to-schema-ir';
|
|
@@ -43,6 +43,30 @@ import {
|
|
|
43
43
|
verifyUniqueConstraints,
|
|
44
44
|
} from './verify-helpers';
|
|
45
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
|
+
|
|
46
70
|
/**
|
|
47
71
|
* Function type for normalizing raw database default expressions into ColumnDefault.
|
|
48
72
|
* Target-specific implementations handle database dialect differences.
|
|
@@ -90,6 +114,15 @@ export interface VerifySqlSchemaOptions {
|
|
|
90
114
|
* with contract native types (e.g., Postgres 'varchar' → 'character varying').
|
|
91
115
|
*/
|
|
92
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>;
|
|
93
126
|
}
|
|
94
127
|
|
|
95
128
|
/**
|
|
@@ -129,6 +162,7 @@ export function verifySqlSchema(options: VerifySqlSchemaOptions): VerifyDatabase
|
|
|
129
162
|
storageTypes,
|
|
130
163
|
...ifDefined('normalizeDefault', normalizeDefault),
|
|
131
164
|
...ifDefined('normalizeNativeType', normalizeNativeType),
|
|
165
|
+
...ifDefined('restrictToNamespaceIds', options.restrictToNamespaceIds),
|
|
132
166
|
});
|
|
133
167
|
|
|
134
168
|
validateFrameworkComponentsForExtensions(contract, options.frameworkComponents);
|
|
@@ -276,6 +310,7 @@ function verifySchemaTables(options: {
|
|
|
276
310
|
storageTypes: Readonly<Record<string, StorageTypeInstance>>;
|
|
277
311
|
normalizeDefault?: DefaultNormalizer;
|
|
278
312
|
normalizeNativeType?: NativeTypeNormalizer;
|
|
313
|
+
restrictToNamespaceIds?: ReadonlySet<string>;
|
|
279
314
|
}): { issues: SchemaIssue[]; rootChildren: SchemaVerificationNode[] } {
|
|
280
315
|
const {
|
|
281
316
|
contract,
|
|
@@ -286,6 +321,7 @@ function verifySchemaTables(options: {
|
|
|
286
321
|
storageTypes,
|
|
287
322
|
normalizeDefault,
|
|
288
323
|
normalizeNativeType,
|
|
324
|
+
restrictToNamespaceIds,
|
|
289
325
|
} = options;
|
|
290
326
|
const contractDefaultControl = contract.defaultControlPolicy;
|
|
291
327
|
const issues: SchemaIssue[] = [];
|
|
@@ -296,14 +332,17 @@ function verifySchemaTables(options: {
|
|
|
296
332
|
);
|
|
297
333
|
|
|
298
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;
|
|
299
339
|
const ns = contract.storage.namespaces[namespaceId];
|
|
300
340
|
if (!ns) continue;
|
|
301
341
|
for (const [tableName, contractTableRaw] of Object.entries(ns.entries.table ?? {})) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
342
|
+
StorageTable.assert(
|
|
343
|
+
contractTableRaw,
|
|
344
|
+
`storage.namespaces.${namespaceId}.entries.table.${tableName}`,
|
|
345
|
+
);
|
|
307
346
|
const contractTable = contractTableRaw;
|
|
308
347
|
const tableControlPolicy = effectiveControlPolicy(
|
|
309
348
|
contractTable.control,
|
|
@@ -315,6 +354,7 @@ function verifySchemaTables(options: {
|
|
|
315
354
|
if (!schemaTable) {
|
|
316
355
|
const issue: SchemaIssue = {
|
|
317
356
|
kind: 'missing_table',
|
|
357
|
+
reason: 'not-found',
|
|
318
358
|
table: tableName,
|
|
319
359
|
namespaceId,
|
|
320
360
|
message: `Table "${tableName}" is missing from database`,
|
|
@@ -369,6 +409,7 @@ function verifySchemaTables(options: {
|
|
|
369
409
|
const extraTableControlPolicy = effectiveControlPolicy(undefined, contractDefaultControl);
|
|
370
410
|
const issue: SchemaIssue = {
|
|
371
411
|
kind: 'extra_table',
|
|
412
|
+
reason: 'not-expected',
|
|
372
413
|
table: tableName,
|
|
373
414
|
message: `Extra table "${tableName}" found in database (not in contract)`,
|
|
374
415
|
};
|
|
@@ -385,6 +426,7 @@ function verifySchemaTables(options: {
|
|
|
385
426
|
expected: undefined,
|
|
386
427
|
actual: undefined,
|
|
387
428
|
children: [],
|
|
429
|
+
reason: 'not-expected',
|
|
388
430
|
},
|
|
389
431
|
issues,
|
|
390
432
|
rootChildren,
|
|
@@ -509,6 +551,7 @@ function verifyTableChildren(options: {
|
|
|
509
551
|
} else if (schemaTable.primaryKey && strict) {
|
|
510
552
|
const issue: SchemaIssue = {
|
|
511
553
|
kind: 'extra_primary_key',
|
|
554
|
+
reason: 'not-expected',
|
|
512
555
|
table: tableName,
|
|
513
556
|
namespaceId,
|
|
514
557
|
message: 'Extra primary key found in database (not in contract)',
|
|
@@ -652,6 +695,7 @@ function collectContractColumnNodes(options: {
|
|
|
652
695
|
if (!schemaColumn) {
|
|
653
696
|
const issue: SchemaIssue = {
|
|
654
697
|
kind: 'missing_column',
|
|
698
|
+
reason: 'not-found',
|
|
655
699
|
table: tableName,
|
|
656
700
|
namespaceId,
|
|
657
701
|
column: columnName,
|
|
@@ -724,6 +768,7 @@ function appendExtraColumnNodes(options: {
|
|
|
724
768
|
if (!contractTable.columns[columnName]) {
|
|
725
769
|
const issue: SchemaIssue = {
|
|
726
770
|
kind: 'extra_column',
|
|
771
|
+
reason: 'not-expected',
|
|
727
772
|
table: tableName,
|
|
728
773
|
namespaceId,
|
|
729
774
|
column: columnName,
|
|
@@ -801,6 +846,7 @@ function verifyColumn(options: {
|
|
|
801
846
|
if (!typesMatch) {
|
|
802
847
|
const issue: SchemaIssue = {
|
|
803
848
|
kind: 'type_mismatch',
|
|
849
|
+
reason: 'not-equal',
|
|
804
850
|
table: tableName,
|
|
805
851
|
namespaceId,
|
|
806
852
|
column: columnName,
|
|
@@ -861,6 +907,7 @@ function verifyColumn(options: {
|
|
|
861
907
|
if (contractColumn.nullable !== schemaColumn.nullable) {
|
|
862
908
|
const issue: SchemaIssue = {
|
|
863
909
|
kind: 'nullability_mismatch',
|
|
910
|
+
reason: 'not-equal',
|
|
864
911
|
table: tableName,
|
|
865
912
|
namespaceId,
|
|
866
913
|
column: columnName,
|
|
@@ -891,6 +938,7 @@ function verifyColumn(options: {
|
|
|
891
938
|
const defaultDescription = describeColumnDefault(contractColumn.default);
|
|
892
939
|
const issue: SchemaIssue = {
|
|
893
940
|
kind: 'default_missing',
|
|
941
|
+
reason: 'not-found',
|
|
894
942
|
table: tableName,
|
|
895
943
|
namespaceId,
|
|
896
944
|
column: columnName,
|
|
@@ -925,6 +973,7 @@ function verifyColumn(options: {
|
|
|
925
973
|
const actualDescription = schemaColumn.default;
|
|
926
974
|
const issue: SchemaIssue = {
|
|
927
975
|
kind: 'default_mismatch',
|
|
976
|
+
reason: 'not-equal',
|
|
928
977
|
table: tableName,
|
|
929
978
|
namespaceId,
|
|
930
979
|
column: columnName,
|
|
@@ -952,6 +1001,7 @@ function verifyColumn(options: {
|
|
|
952
1001
|
} else if (strict && schemaColumn.default) {
|
|
953
1002
|
const issue: SchemaIssue = {
|
|
954
1003
|
kind: 'extra_default',
|
|
1004
|
+
reason: 'not-expected',
|
|
955
1005
|
table: tableName,
|
|
956
1006
|
namespaceId,
|
|
957
1007
|
column: columnName,
|
|
@@ -1313,3 +1363,141 @@ function formatLiteralValue(value: unknown): string {
|
|
|
1313
1363
|
}
|
|
1314
1364
|
return JSON.stringify(value);
|
|
1315
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,
|
|
@@ -4,11 +4,10 @@ import {
|
|
|
4
4
|
type CheckConstraint,
|
|
5
5
|
type ForeignKey,
|
|
6
6
|
type Index,
|
|
7
|
-
isStorageTable,
|
|
8
7
|
isStorageTypeInstance,
|
|
9
8
|
type SqlStorage,
|
|
10
9
|
type StorageColumn,
|
|
11
|
-
|
|
10
|
+
StorageTable,
|
|
12
11
|
type StorageTypeInstance,
|
|
13
12
|
type UniqueConstraint,
|
|
14
13
|
} from '@prisma-next/sql-contract/types';
|
|
@@ -305,7 +304,7 @@ export function detectDestructiveChanges(
|
|
|
305
304
|
|
|
306
305
|
for (const tableName of Object.keys(fromTables)) {
|
|
307
306
|
const toTableRaw = toNs?.entries.table?.[tableName];
|
|
308
|
-
if (!
|
|
307
|
+
if (!StorageTable.is(toTableRaw)) {
|
|
309
308
|
conflicts.push({
|
|
310
309
|
kind: 'tableRemoved',
|
|
311
310
|
summary: `Table "${tableName}" was removed`,
|
|
@@ -315,7 +314,7 @@ export function detectDestructiveChanges(
|
|
|
315
314
|
const toTable = toTableRaw;
|
|
316
315
|
|
|
317
316
|
const fromTableRaw = fromTables[tableName];
|
|
318
|
-
if (!
|
|
317
|
+
if (!StorageTable.is(fromTableRaw)) continue;
|
|
319
318
|
const fromTable = fromTableRaw;
|
|
320
319
|
|
|
321
320
|
for (const columnName of Object.keys(fromTable.columns)) {
|
|
@@ -360,6 +359,48 @@ export interface ContractToSchemaIROptions {
|
|
|
360
359
|
*
|
|
361
360
|
* Returns an empty schema IR when `contract` is `null` (new project).
|
|
362
361
|
*/
|
|
362
|
+
/**
|
|
363
|
+
* Converts the tables of a single namespace into a `SqlSchemaIR`, keyed by
|
|
364
|
+
* table name within that namespace. Unlike {@link contractToSchemaIR}, which
|
|
365
|
+
* flattens every namespace's tables into one bare-keyed record (and throws on a
|
|
366
|
+
* cross-namespace name collision), this scopes the table iteration to one
|
|
367
|
+
* namespace so the same table name can exist in two schemas.
|
|
368
|
+
*
|
|
369
|
+
* The full `storage` is still passed to `convertTable`, so value-set / enum /
|
|
370
|
+
* type resolution that legitimately spans namespaces is unaffected. Foreign
|
|
371
|
+
* keys are built purely from the FK descriptor (`fk.target`), so cross-namespace
|
|
372
|
+
* FKs survive per-namespace conversion. The `annotations` block (storage-type
|
|
373
|
+
* derived) is omitted here — the per-namespace tree consumer reads only the
|
|
374
|
+
* per-table fields.
|
|
375
|
+
*/
|
|
376
|
+
export function contractNamespaceToSchemaIR(
|
|
377
|
+
storage: SqlStorage,
|
|
378
|
+
namespaceId: string,
|
|
379
|
+
options: ContractToSchemaIROptions,
|
|
380
|
+
): SqlSchemaIR {
|
|
381
|
+
if (options.annotationNamespace.length === 0) {
|
|
382
|
+
throw new Error('annotationNamespace must be a non-empty string');
|
|
383
|
+
}
|
|
384
|
+
const namespace = storage.namespaces[namespaceId];
|
|
385
|
+
if (!namespace) {
|
|
386
|
+
return { tables: {} };
|
|
387
|
+
}
|
|
388
|
+
const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
|
|
389
|
+
const tables: Record<string, SqlTableIR> = {};
|
|
390
|
+
for (const [tableName, tableDefRaw] of Object.entries(namespace.entries.table ?? {})) {
|
|
391
|
+
StorageTable.assert(tableDefRaw, `namespaces.${namespaceId}.entries.table.${tableName}`);
|
|
392
|
+
tables[tableName] = convertTable(
|
|
393
|
+
tableName,
|
|
394
|
+
tableDefRaw,
|
|
395
|
+
storageTypes,
|
|
396
|
+
options.expandNativeType,
|
|
397
|
+
options.renderDefault,
|
|
398
|
+
storage,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
return { tables };
|
|
402
|
+
}
|
|
403
|
+
|
|
363
404
|
export function contractToSchemaIR(
|
|
364
405
|
contract: Contract<SqlStorage> | null,
|
|
365
406
|
options: ContractToSchemaIROptions,
|
|
@@ -373,17 +414,11 @@ export function contractToSchemaIR(
|
|
|
373
414
|
}
|
|
374
415
|
|
|
375
416
|
const storage = contract.storage;
|
|
376
|
-
const storageTypes: ResolvedStorageTypes = {
|
|
377
|
-
...((storage.types ?? {}) as ResolvedStorageTypes),
|
|
378
|
-
};
|
|
417
|
+
const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
|
|
379
418
|
const tables: Record<string, SqlTableIR> = {};
|
|
380
419
|
for (const ns of Object.values(storage.namespaces)) {
|
|
381
420
|
for (const [tableName, tableDefRaw] of Object.entries(ns.entries.table ?? {})) {
|
|
382
|
-
|
|
383
|
-
throw new Error(
|
|
384
|
-
`contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.entries.table.${tableName}`,
|
|
385
|
-
);
|
|
386
|
-
}
|
|
421
|
+
StorageTable.assert(tableDefRaw, `namespaces.${ns.id}.entries.table.${tableName}`);
|
|
387
422
|
const tableDef = tableDefRaw;
|
|
388
423
|
if (tables[tableName] !== undefined) {
|
|
389
424
|
throw new Error(
|
|
@@ -420,7 +455,7 @@ function deriveAnnotations(
|
|
|
420
455
|
): SqlAnnotations | undefined {
|
|
421
456
|
const storageTypes: Record<string, StorageTypeInstance> = {};
|
|
422
457
|
|
|
423
|
-
for (const typeInstance of Object.values(
|
|
458
|
+
for (const typeInstance of Object.values(storage.types ?? {})) {
|
|
424
459
|
if (isStorageTypeInstance(typeInstance)) {
|
|
425
460
|
storageTypes[typeInstance.nativeType] = typeInstance;
|
|
426
461
|
}
|
|
@@ -24,12 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import type { Contract } from '@prisma-next/contract/types';
|
|
26
26
|
import type { OpFactoryCall } from '@prisma-next/framework-components/control';
|
|
27
|
-
import {
|
|
28
|
-
isStorageTable,
|
|
29
|
-
type SqlStorage,
|
|
30
|
-
type StorageColumn,
|
|
31
|
-
type StorageTable,
|
|
32
|
-
} from '@prisma-next/sql-contract/types';
|
|
27
|
+
import { type SqlStorage, type StorageColumn, StorageTable } from '@prisma-next/sql-contract/types';
|
|
33
28
|
import type { CodecControlHooks, FieldEvent, FieldEventContext } from './types';
|
|
34
29
|
|
|
35
30
|
export interface PlanFieldEventOperationsOptions {
|
|
@@ -93,8 +88,8 @@ export function planFieldEventOperations(
|
|
|
93
88
|
for (const tableName of tableNames) {
|
|
94
89
|
const priorTableRaw = priorTables?.[tableName];
|
|
95
90
|
const newTableRaw = newTables?.[tableName];
|
|
96
|
-
const priorTable =
|
|
97
|
-
const newTable =
|
|
91
|
+
const priorTable = StorageTable.is(priorTableRaw) ? priorTableRaw : undefined;
|
|
92
|
+
const newTable = StorageTable.is(newTableRaw) ? newTableRaw : undefined;
|
|
98
93
|
const fieldNames = unionSorted(
|
|
99
94
|
priorTable ? Object.keys(priorTable.columns) : [],
|
|
100
95
|
newTable ? Object.keys(newTable.columns) : [],
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
2
|
+
import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
|
|
3
|
+
import type {
|
|
4
|
+
SchemaDiffer,
|
|
5
|
+
VerifyDatabaseSchemaResult,
|
|
6
|
+
} from '@prisma-next/framework-components/control';
|
|
7
|
+
import type { SqlStorage } from '@prisma-next/sql-contract/types';
|
|
8
|
+
import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Inputs to a SQL target's schema-differ (`diffDatabaseSchema` /
|
|
12
|
+
* `verifyDatabaseSchema` on the descriptor): the contract (the expected side
|
|
13
|
+
* derives from it), the introspected actual schema node, and the resolution
|
|
14
|
+
* context the relational diff needs.
|
|
15
|
+
*/
|
|
16
|
+
export interface DiffDatabaseSchemaInput {
|
|
17
|
+
readonly contract: Contract<SqlStorage>;
|
|
18
|
+
readonly schema: SqlSchemaIRNode;
|
|
19
|
+
readonly strict: boolean;
|
|
20
|
+
readonly typeMetadataRegistry: ReadonlyMap<string, { readonly nativeType?: string }>;
|
|
21
|
+
readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The `SchemaDiffer` a SQL target implements: the black-box comparison of the
|
|
26
|
+
* contract's expected schema against the introspected actual schema, projected
|
|
27
|
+
* to the two issue lists. How it computes them is private to the target.
|
|
28
|
+
*/
|
|
29
|
+
export type SqlDiffDatabaseSchema = SchemaDiffer<DiffDatabaseSchemaInput>['diff'];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The same combined comparison as {@link SqlDiffDatabaseSchema}, wrapped in the
|
|
33
|
+
* verify envelope (`ok`/`summary`/`code`/`target`/`timings`) plus the
|
|
34
|
+
* pass/warn/fail tree the CLI renders. Verify calls this instead of the diff so
|
|
35
|
+
* the relational walk that produces the tree runs once per verify, not once for
|
|
36
|
+
* the diff and again for the tree.
|
|
37
|
+
*/
|
|
38
|
+
export type SqlVerifyDatabaseSchema = (
|
|
39
|
+
input: DiffDatabaseSchemaInput,
|
|
40
|
+
) => VerifyDatabaseSchemaResult;
|