@prisma-next/family-sql 0.9.0 → 0.10.0-dev.2
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/control.d.mts +1 -1
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +99 -55
- package/dist/control.mjs.map +1 -1
- package/dist/ir.d.mts +5 -41
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +1 -1
- package/dist/migration.d.mts +1 -1
- package/dist/schema-verify.d.mts.map +1 -1
- package/dist/schema-verify.mjs +1 -1
- package/dist/{sql-contract-serializer-qUQCnP-k.mjs → sql-contract-serializer-C6ypWJYR.mjs} +22 -43
- package/dist/sql-contract-serializer-C6ypWJYR.mjs.map +1 -0
- package/dist/{types-DMINfGUO.d.mts → types-hQoMXr54.d.mts} +4 -3
- package/dist/types-hQoMXr54.d.mts.map +1 -0
- package/dist/verify-sql-schema-Bfvz07Ik.d.mts.map +1 -1
- package/dist/{verify-sql-schema-BXw7yx6L.mjs → verify-sql-schema-Bj4Wqe2c.mjs} +99 -63
- package/dist/verify-sql-schema-Bj4Wqe2c.mjs.map +1 -0
- package/package.json +21 -21
- package/src/core/control-instance.ts +11 -4
- package/src/core/ir/sql-contract-serializer-base.ts +58 -47
- package/src/core/migrations/contract-to-schema-ir.ts +82 -33
- package/src/core/migrations/field-event-planner.ts +53 -29
- package/src/core/migrations/types.ts +2 -1
- package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +17 -3
- package/src/core/schema-verify/verify-helpers.ts +44 -14
- package/src/core/schema-verify/verify-sql-schema.ts +106 -47
- package/dist/sql-contract-serializer-qUQCnP-k.mjs.map +0 -1
- package/dist/types-DMINfGUO.d.mts.map +0 -1
- package/dist/verify-sql-schema-BXw7yx6L.mjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prisma-next/family-sql",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0-dev.2",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"description": "SQL family descriptor for Prisma Next",
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@prisma-next/contract": "0.
|
|
10
|
-
"@prisma-next/emitter": "0.
|
|
11
|
-
"@prisma-next/framework-components": "0.
|
|
12
|
-
"@prisma-next/migration-tools": "0.
|
|
13
|
-
"@prisma-next/operations": "0.
|
|
14
|
-
"@prisma-next/sql-contract": "0.
|
|
15
|
-
"@prisma-next/sql-contract-emitter": "0.
|
|
16
|
-
"@prisma-next/sql-contract-ts": "0.
|
|
17
|
-
"@prisma-next/sql-operations": "0.
|
|
18
|
-
"@prisma-next/sql-relational-core": "0.
|
|
19
|
-
"@prisma-next/sql-runtime": "0.
|
|
20
|
-
"@prisma-next/sql-schema-ir": "0.
|
|
21
|
-
"@prisma-next/utils": "0.
|
|
9
|
+
"@prisma-next/contract": "0.10.0-dev.2",
|
|
10
|
+
"@prisma-next/emitter": "0.10.0-dev.2",
|
|
11
|
+
"@prisma-next/framework-components": "0.10.0-dev.2",
|
|
12
|
+
"@prisma-next/migration-tools": "0.10.0-dev.2",
|
|
13
|
+
"@prisma-next/operations": "0.10.0-dev.2",
|
|
14
|
+
"@prisma-next/sql-contract": "0.10.0-dev.2",
|
|
15
|
+
"@prisma-next/sql-contract-emitter": "0.10.0-dev.2",
|
|
16
|
+
"@prisma-next/sql-contract-ts": "0.10.0-dev.2",
|
|
17
|
+
"@prisma-next/sql-operations": "0.10.0-dev.2",
|
|
18
|
+
"@prisma-next/sql-relational-core": "0.10.0-dev.2",
|
|
19
|
+
"@prisma-next/sql-runtime": "0.10.0-dev.2",
|
|
20
|
+
"@prisma-next/sql-schema-ir": "0.10.0-dev.2",
|
|
21
|
+
"@prisma-next/utils": "0.10.0-dev.2",
|
|
22
22
|
"arktype": "^2.2.0"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@prisma-next/driver-postgres": "0.
|
|
26
|
-
"@prisma-next/psl-parser": "0.
|
|
27
|
-
"@prisma-next/psl-printer": "0.
|
|
28
|
-
"@prisma-next/sql-contract-psl": "0.
|
|
29
|
-
"@prisma-next/test-utils": "0.
|
|
30
|
-
"@prisma-next/tsconfig": "0.
|
|
31
|
-
"@prisma-next/tsdown": "0.
|
|
25
|
+
"@prisma-next/driver-postgres": "0.10.0-dev.2",
|
|
26
|
+
"@prisma-next/psl-parser": "0.10.0-dev.2",
|
|
27
|
+
"@prisma-next/psl-printer": "0.10.0-dev.2",
|
|
28
|
+
"@prisma-next/sql-contract-psl": "0.10.0-dev.2",
|
|
29
|
+
"@prisma-next/test-utils": "0.10.0-dev.2",
|
|
30
|
+
"@prisma-next/tsconfig": "0.10.0-dev.2",
|
|
31
|
+
"@prisma-next/tsdown": "0.10.0-dev.2",
|
|
32
32
|
"tsdown": "0.22.0",
|
|
33
33
|
"typescript": "5.9.3",
|
|
34
34
|
"vitest": "4.1.6"
|
|
@@ -62,11 +62,18 @@ function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
|
|
|
62
62
|
'storage' in contract &&
|
|
63
63
|
typeof contract.storage === 'object' &&
|
|
64
64
|
contract.storage !== null &&
|
|
65
|
-
'
|
|
65
|
+
'namespaces' in contract.storage &&
|
|
66
|
+
typeof contract.storage.namespaces === 'object' &&
|
|
67
|
+
contract.storage.namespaces !== null
|
|
66
68
|
) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
const namespaces = contract.storage.namespaces as Record<
|
|
70
|
+
string,
|
|
71
|
+
{ readonly tables?: Readonly<Record<string, unknown>> }
|
|
72
|
+
>;
|
|
73
|
+
for (const ns of Object.values(namespaces)) {
|
|
74
|
+
const tbls = ns.tables;
|
|
75
|
+
if (typeof tbls !== 'object' || tbls === null) continue;
|
|
76
|
+
for (const table of Object.values(tbls)) {
|
|
70
77
|
if (
|
|
71
78
|
typeof table === 'object' &&
|
|
72
79
|
table !== null &&
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import { ContractValidationError } from '@prisma-next/contract/contract-validation-error';
|
|
1
2
|
import type { Contract } from '@prisma-next/contract/types';
|
|
2
3
|
import type { ContractSerializer } from '@prisma-next/framework-components/control';
|
|
3
|
-
import {
|
|
4
|
+
import { type Namespace, NamespaceBase } from '@prisma-next/framework-components/ir';
|
|
5
|
+
import {
|
|
6
|
+
type SqlNamespaceTablesInput,
|
|
7
|
+
SqlStorage,
|
|
8
|
+
type SqlStorageTypeEntry,
|
|
9
|
+
StorageTable,
|
|
10
|
+
type StorageTableInput,
|
|
11
|
+
} from '@prisma-next/sql-contract/types';
|
|
4
12
|
import { validateSqlContractFully } from '@prisma-next/sql-contract/validators';
|
|
5
13
|
import type { JsonObject } from '@prisma-next/utils/json';
|
|
6
14
|
|
|
@@ -37,51 +45,20 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
37
45
|
private readonly entityTypeRegistry: ReadonlyMap<string, SqlEntityHydrationFactory>,
|
|
38
46
|
) {}
|
|
39
47
|
|
|
40
|
-
deserializeContract(json: unknown):
|
|
48
|
+
deserializeContract<T extends TContract = TContract>(json: unknown): T {
|
|
41
49
|
const validated = this.parseSqlContractStructure(json);
|
|
42
50
|
const hydrated = this.hydrateSqlStorage(validated);
|
|
43
|
-
return this.constructTargetContract(hydrated);
|
|
51
|
+
return this.constructTargetContract(hydrated) as T;
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
serializeContract(contract: TContract): JsonObject {
|
|
47
|
-
// Targets that ship enumerable runtime-only fields must override
|
|
48
|
-
// this method (mirroring `MongoTargetContractSerializer.serializeContract`)
|
|
49
|
-
// to construct the persisted envelope explicitly; the default identity
|
|
50
|
-
// works only when every enumerable own property belongs in the JSON.
|
|
51
55
|
return contract as unknown as JsonObject;
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
/**
|
|
55
|
-
* Family-shared validation pipeline (delegates to
|
|
56
|
-
* `validateSqlContractFully` in `@prisma-next/sql-contract/validators`):
|
|
57
|
-
* structural arktype + framework-shared domain + SQL storage
|
|
58
|
-
* logical-consistency + SQL storage semantic + model ↔ storage
|
|
59
|
-
* reference checks. Subclasses override to add target-specific
|
|
60
|
-
* structural checks before hydration; the family default suffices
|
|
61
|
-
* for targets whose contract shape is the SQL-shared shape
|
|
62
|
-
* (Postgres, SQLite today).
|
|
63
|
-
*/
|
|
64
58
|
protected parseSqlContractStructure(json: unknown): Contract<SqlStorage> {
|
|
65
59
|
return validateSqlContractFully<Contract<SqlStorage>>(json);
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
/**
|
|
69
|
-
* Family-shared hydration walker. Lifts the validated flat-data
|
|
70
|
-
* `storage` subtree into the SQL Contract IR class hierarchy by
|
|
71
|
-
* constructing a single `SqlStorage` instance — its constructor
|
|
72
|
-
* cascades nested instantiation of `StorageTable`, `StorageColumn`,
|
|
73
|
-
* `PrimaryKey`, `UniqueConstraint`, `Index`, `ForeignKey`,
|
|
74
|
-
* `ForeignKeyReferences`, and `StorageTypeInstance`. The rest of the
|
|
75
|
-
* contract envelope (target identity, hashes, capabilities, models,
|
|
76
|
-
* meta, …) is JSON-clean primitive data and passes through unchanged.
|
|
77
|
-
*
|
|
78
|
-
* Polymorphic `storage.types` entries are normalised before the
|
|
79
|
-
* `SqlStorage` constructor runs: when an entry carries an enumerable
|
|
80
|
-
* string `kind`, the serializer looks up a pack-registered hydration
|
|
81
|
-
* factory for that discriminator and delegates reconstruction. Entries
|
|
82
|
-
* with no registered factory pass through unchanged (codec-typed JSON
|
|
83
|
-
* stays codec-typed until `SqlStorage` normalises it).
|
|
84
|
-
*/
|
|
85
62
|
protected hydrateSqlStorage(validated: Contract<SqlStorage>): Contract<SqlStorage> {
|
|
86
63
|
const types = validated.storage.types;
|
|
87
64
|
const hydratedTypes =
|
|
@@ -94,21 +71,61 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
94
71
|
)
|
|
95
72
|
: undefined;
|
|
96
73
|
|
|
74
|
+
const rawNamespaces = validated.storage.namespaces;
|
|
75
|
+
const hydratedNamespaces =
|
|
76
|
+
rawNamespaces !== undefined ? this.hydrateSqlNamespaceMap(rawNamespaces) : undefined;
|
|
77
|
+
|
|
97
78
|
return {
|
|
98
79
|
...validated,
|
|
99
80
|
storage: new SqlStorage({
|
|
100
|
-
|
|
81
|
+
storageHash: validated.storage.storageHash,
|
|
101
82
|
...(hydratedTypes !== undefined ? { types: hydratedTypes } : {}),
|
|
83
|
+
...(hydratedNamespaces !== undefined ? { namespaces: hydratedNamespaces } : {}),
|
|
102
84
|
}),
|
|
103
85
|
};
|
|
104
86
|
}
|
|
105
87
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
88
|
+
protected hydrateSqlNamespaceMap(
|
|
89
|
+
namespaces: Readonly<Record<string, Namespace | Record<string, unknown>>>,
|
|
90
|
+
): Readonly<Record<string, Namespace | SqlNamespaceTablesInput>> {
|
|
91
|
+
return Object.fromEntries(
|
|
92
|
+
Object.entries(namespaces).map(([nsId, raw]) => [
|
|
93
|
+
nsId,
|
|
94
|
+
this.hydrateSqlNamespaceEntry(nsId, raw),
|
|
95
|
+
]),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
protected hydrateSqlNamespaceEntry(
|
|
100
|
+
nsId: string,
|
|
101
|
+
raw: Namespace | Record<string, unknown>,
|
|
102
|
+
): Namespace | SqlNamespaceTablesInput {
|
|
103
|
+
if (raw instanceof NamespaceBase) {
|
|
104
|
+
return raw;
|
|
105
|
+
}
|
|
106
|
+
const obj = raw as {
|
|
107
|
+
id?: string;
|
|
108
|
+
tables?: Record<string, unknown>;
|
|
109
|
+
types?: Record<string, unknown>;
|
|
110
|
+
};
|
|
111
|
+
if (obj.types !== undefined && Object.keys(obj.types).length > 0) {
|
|
112
|
+
throw new ContractValidationError(
|
|
113
|
+
'Per-schema database types (e.g. postgres-enum) under storage.namespaces[..].types require PostgresContractSerializer.',
|
|
114
|
+
'structural',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const tables = Object.fromEntries(
|
|
118
|
+
Object.entries(obj.tables ?? {}).map(([tableName, table]) => [
|
|
119
|
+
tableName,
|
|
120
|
+
table instanceof StorageTable ? table : new StorageTable(table as StorageTableInput),
|
|
121
|
+
]),
|
|
122
|
+
);
|
|
123
|
+
return {
|
|
124
|
+
id: obj.id ?? nsId,
|
|
125
|
+
tables,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
112
129
|
protected hydrateStorageTypeEntry(entry: SqlStorageTypeEntry): SqlStorageTypeEntry {
|
|
113
130
|
if (typeof entry !== 'object' || entry === null) {
|
|
114
131
|
return entry;
|
|
@@ -124,12 +141,6 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
|
|
|
124
141
|
return factory(entry);
|
|
125
142
|
}
|
|
126
143
|
|
|
127
|
-
/**
|
|
128
|
-
* Target-specific construction hook. Defaults to identity; targets
|
|
129
|
-
* that need to wrap the hydrated contract (e.g. attach target-only
|
|
130
|
-
* derived fields, narrow the contract type to a target-specific
|
|
131
|
-
* subtype) override.
|
|
132
|
-
*/
|
|
133
144
|
protected constructTargetContract(hydrated: Contract<SqlStorage>): TContract {
|
|
134
145
|
return hydrated as TContract;
|
|
135
146
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type PostgresEnumStorageEntry,
|
|
9
9
|
type SqlStorage,
|
|
10
10
|
type StorageColumn,
|
|
11
|
-
|
|
11
|
+
StorageTable,
|
|
12
12
|
type StorageTypeInstance,
|
|
13
13
|
toStorageTypeInstance,
|
|
14
14
|
type UniqueConstraint,
|
|
@@ -147,9 +147,10 @@ function convertIndex(index: Index): SqlIndexIR {
|
|
|
147
147
|
|
|
148
148
|
function convertForeignKey(fk: ForeignKey): SqlForeignKeyIR {
|
|
149
149
|
return {
|
|
150
|
-
columns: fk.columns,
|
|
151
|
-
referencedTable: fk.
|
|
152
|
-
|
|
150
|
+
columns: fk.source.columns,
|
|
151
|
+
referencedTable: fk.target.tableName,
|
|
152
|
+
referencedSchema: fk.target.namespaceId,
|
|
153
|
+
referencedColumns: fk.target.columns,
|
|
153
154
|
...ifDefined('name', fk.name),
|
|
154
155
|
};
|
|
155
156
|
}
|
|
@@ -180,12 +181,12 @@ function convertTable(
|
|
|
180
181
|
const fkBackingIndexes: SqlIndexIR[] = [];
|
|
181
182
|
for (const fk of table.foreignKeys) {
|
|
182
183
|
if (fk.index === false) continue;
|
|
183
|
-
const key = fk.columns.join(',');
|
|
184
|
+
const key = fk.source.columns.join(',');
|
|
184
185
|
if (satisfiedIndexColumns.has(key)) continue;
|
|
185
186
|
fkBackingIndexes.push({
|
|
186
|
-
columns: fk.columns,
|
|
187
|
+
columns: fk.source.columns,
|
|
187
188
|
unique: false,
|
|
188
|
-
name: defaultIndexName(name, fk.columns),
|
|
189
|
+
name: defaultIndexName(name, fk.source.columns),
|
|
189
190
|
});
|
|
190
191
|
satisfiedIndexColumns.add(key);
|
|
191
192
|
}
|
|
@@ -219,25 +220,38 @@ export function detectDestructiveChanges(
|
|
|
219
220
|
|
|
220
221
|
const conflicts: MigrationPlannerConflict[] = [];
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
kind: 'tableRemoved',
|
|
226
|
-
summary: `Table "${tableName}" was removed`,
|
|
227
|
-
});
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
223
|
+
const namespaceIds = [
|
|
224
|
+
...new Set([...Object.keys(from.namespaces), ...Object.keys(to.namespaces)]),
|
|
225
|
+
].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
230
226
|
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
227
|
+
for (const namespaceId of namespaceIds) {
|
|
228
|
+
const fromNs = from.namespaces[namespaceId];
|
|
229
|
+
const toNs = to.namespaces[namespaceId];
|
|
230
|
+
const fromTables = fromNs?.tables;
|
|
231
|
+
if (!fromTables) continue;
|
|
234
232
|
|
|
235
|
-
for (const
|
|
236
|
-
|
|
233
|
+
for (const tableName of Object.keys(fromTables)) {
|
|
234
|
+
const toTableRaw = toNs?.tables[tableName];
|
|
235
|
+
if (!(toTableRaw instanceof StorageTable)) {
|
|
237
236
|
conflicts.push({
|
|
238
|
-
kind: '
|
|
239
|
-
summary: `
|
|
237
|
+
kind: 'tableRemoved',
|
|
238
|
+
summary: `Table "${tableName}" was removed`,
|
|
240
239
|
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const toTable = toTableRaw;
|
|
243
|
+
|
|
244
|
+
const fromTableRaw = fromTables[tableName];
|
|
245
|
+
if (!(fromTableRaw instanceof StorageTable)) continue;
|
|
246
|
+
const fromTable = fromTableRaw;
|
|
247
|
+
|
|
248
|
+
for (const columnName of Object.keys(fromTable.columns)) {
|
|
249
|
+
if (!hasOwn(toTable.columns, columnName)) {
|
|
250
|
+
conflicts.push({
|
|
251
|
+
kind: 'columnRemoved',
|
|
252
|
+
summary: `Column "${tableName}"."${columnName}" was removed`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
241
255
|
}
|
|
242
256
|
}
|
|
243
257
|
}
|
|
@@ -278,16 +292,40 @@ export function contractToSchemaIR(
|
|
|
278
292
|
}
|
|
279
293
|
|
|
280
294
|
const storage = contract.storage;
|
|
281
|
-
const
|
|
295
|
+
const allTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {
|
|
296
|
+
...((storage.types ?? {}) as ResolvedStorageTypes),
|
|
297
|
+
};
|
|
298
|
+
for (const ns of Object.values(storage.namespaces)) {
|
|
299
|
+
const nsTypes = (ns as { types?: Record<string, PostgresEnumStorageEntry> }).types;
|
|
300
|
+
if (nsTypes) {
|
|
301
|
+
for (const [k, v] of Object.entries(nsTypes)) {
|
|
302
|
+
allTypes[k] = v;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const storageTypes = allTypes as ResolvedStorageTypes;
|
|
282
307
|
const tables: Record<string, SqlTableIR> = {};
|
|
283
|
-
for (const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
308
|
+
for (const ns of Object.values(storage.namespaces)) {
|
|
309
|
+
for (const [tableName, tableDefRaw] of Object.entries(ns.tables)) {
|
|
310
|
+
if (!(tableDefRaw instanceof StorageTable)) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.tables.${tableName}`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const tableDef = tableDefRaw;
|
|
316
|
+
if (tables[tableName] !== undefined) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
`contractToSchemaIR: duplicate SQL table name "${tableName}" across namespaces (ambiguous for flat SqlSchemaIR.tables).`,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
tables[tableName] = convertTable(
|
|
322
|
+
tableName,
|
|
323
|
+
tableDef,
|
|
324
|
+
storageTypes,
|
|
325
|
+
options.expandNativeType,
|
|
326
|
+
options.renderDefault,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
291
329
|
}
|
|
292
330
|
|
|
293
331
|
const annotations = deriveAnnotations(storage, options.annotationNamespace);
|
|
@@ -302,8 +340,19 @@ function deriveAnnotations(
|
|
|
302
340
|
storage: SqlStorage,
|
|
303
341
|
annotationNamespace: string,
|
|
304
342
|
): SqlAnnotations | undefined {
|
|
305
|
-
const
|
|
306
|
-
|
|
343
|
+
const allTypes: Record<string, StorageTypeInstance | PostgresEnumStorageEntry> = {
|
|
344
|
+
...((storage.types ?? {}) as ResolvedStorageTypes),
|
|
345
|
+
};
|
|
346
|
+
for (const ns of Object.values(storage.namespaces)) {
|
|
347
|
+
const nsTypes = (ns as { types?: Record<string, PostgresEnumStorageEntry> }).types;
|
|
348
|
+
if (nsTypes) {
|
|
349
|
+
for (const [k, v] of Object.entries(nsTypes)) {
|
|
350
|
+
allTypes[k] = v;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const types = allTypes as ResolvedStorageTypes;
|
|
355
|
+
if (Object.keys(types).length === 0) return undefined;
|
|
307
356
|
// Re-key by nativeType, normalising every variant to the codec-typed
|
|
308
357
|
// annotation shape `{codecId, nativeType, typeParams}` produced by the
|
|
309
358
|
// adapter introspector (`introspectPostgresEnumTypes` writes that shape;
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* - Events are grouped by phase: `'added'` → `'dropped'` → `'altered'`.
|
|
15
15
|
* - Within each phase, entries are sorted alphabetically by
|
|
16
|
-
* `(tableName, fieldName)`.
|
|
16
|
+
* `(namespaceId, tableName, fieldName)`.
|
|
17
17
|
* - The hook's returned ops are appended in the order the hook returned them.
|
|
18
18
|
*
|
|
19
19
|
* `'altered'` is suppressed when only `codecId` differs (codec rotation is a
|
|
@@ -24,7 +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 type
|
|
27
|
+
import { type SqlStorage, type StorageColumn, StorageTable } from '@prisma-next/sql-contract/types';
|
|
28
28
|
import type { CodecControlHooks, FieldEvent, FieldEventContext } from './types';
|
|
29
29
|
|
|
30
30
|
export interface PlanFieldEventOperationsOptions {
|
|
@@ -50,6 +50,7 @@ export interface PlanFieldEventOperationsOptions {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
interface FieldEntry {
|
|
53
|
+
readonly namespaceId: string;
|
|
53
54
|
readonly tableName: string;
|
|
54
55
|
readonly fieldName: string;
|
|
55
56
|
readonly priorTable: StorageTable | undefined;
|
|
@@ -61,38 +62,57 @@ interface FieldEntry {
|
|
|
61
62
|
export function planFieldEventOperations(
|
|
62
63
|
options: PlanFieldEventOperationsOptions,
|
|
63
64
|
): readonly OpFactoryCall[] {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
65
|
+
const priorContract = options.priorContract;
|
|
66
|
+
const newContract = options.newContract;
|
|
66
67
|
|
|
67
68
|
const added: FieldEntry[] = [];
|
|
68
69
|
const dropped: FieldEntry[] = [];
|
|
69
70
|
const altered: FieldEntry[] = [];
|
|
70
71
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
const namespaceIds = unionSorted(
|
|
73
|
+
priorContract ? Object.keys(priorContract.storage.namespaces) : [],
|
|
74
|
+
Object.keys(newContract.storage.namespaces),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
for (const namespaceId of namespaceIds) {
|
|
78
|
+
const priorNs = priorContract?.storage.namespaces[namespaceId];
|
|
79
|
+
const newNs = newContract.storage.namespaces[namespaceId];
|
|
80
|
+
const priorTables = priorNs?.tables;
|
|
81
|
+
const newTables = newNs?.tables;
|
|
82
|
+
|
|
83
|
+
const tableNames = unionSorted(
|
|
84
|
+
priorTables ? Object.keys(priorTables) : [],
|
|
85
|
+
newTables ? Object.keys(newTables) : [],
|
|
78
86
|
);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
87
|
+
|
|
88
|
+
for (const tableName of tableNames) {
|
|
89
|
+
const priorTableRaw = priorTables?.[tableName];
|
|
90
|
+
const newTableRaw = newTables?.[tableName];
|
|
91
|
+
const priorTable = priorTableRaw instanceof StorageTable ? priorTableRaw : undefined;
|
|
92
|
+
const newTable = newTableRaw instanceof StorageTable ? newTableRaw : undefined;
|
|
93
|
+
const fieldNames = unionSorted(
|
|
94
|
+
priorTable ? Object.keys(priorTable.columns) : [],
|
|
95
|
+
newTable ? Object.keys(newTable.columns) : [],
|
|
96
|
+
);
|
|
97
|
+
for (const fieldName of fieldNames) {
|
|
98
|
+
const priorField = priorTable?.columns[fieldName];
|
|
99
|
+
const newField = newTable?.columns[fieldName];
|
|
100
|
+
const entry: FieldEntry = {
|
|
101
|
+
namespaceId,
|
|
102
|
+
tableName,
|
|
103
|
+
fieldName,
|
|
104
|
+
priorTable,
|
|
105
|
+
newTable,
|
|
106
|
+
priorField,
|
|
107
|
+
newField,
|
|
108
|
+
};
|
|
109
|
+
if (priorField === undefined && newField !== undefined) {
|
|
110
|
+
added.push(entry);
|
|
111
|
+
} else if (priorField !== undefined && newField === undefined) {
|
|
112
|
+
dropped.push(entry);
|
|
113
|
+
} else if (priorField !== undefined && newField !== undefined) {
|
|
114
|
+
if (isAlteration(priorField, newField)) altered.push(entry);
|
|
115
|
+
}
|
|
96
116
|
}
|
|
97
117
|
}
|
|
98
118
|
}
|
|
@@ -130,7 +150,11 @@ function appendCalls(
|
|
|
130
150
|
* - `'altered'` — both sides populated.
|
|
131
151
|
*/
|
|
132
152
|
function buildContext(event: FieldEvent, entry: FieldEntry): FieldEventContext {
|
|
133
|
-
const base = {
|
|
153
|
+
const base = {
|
|
154
|
+
namespaceId: entry.namespaceId,
|
|
155
|
+
tableName: entry.tableName,
|
|
156
|
+
fieldName: entry.fieldName,
|
|
157
|
+
};
|
|
134
158
|
if (event === 'added') {
|
|
135
159
|
return {
|
|
136
160
|
...base,
|
|
@@ -79,7 +79,7 @@ export type FieldEvent = 'added' | 'dropped' | 'altered';
|
|
|
79
79
|
/**
|
|
80
80
|
* Context passed to {@link CodecControlHooks.onFieldEvent}.
|
|
81
81
|
*
|
|
82
|
-
* `tableName
|
|
82
|
+
* `namespaceId`, `tableName`, and `fieldName` are always populated; `priorTable` /
|
|
83
83
|
* `priorField` carry the prior contract's view of the table and column
|
|
84
84
|
* (present for `'dropped'` and `'altered'`); `newTable` / `newField`
|
|
85
85
|
* carry the new contract's view (present for `'added'` and `'altered'`).
|
|
@@ -89,6 +89,7 @@ export type FieldEvent = 'added' | 'dropped' | 'altered';
|
|
|
89
89
|
* application emitter only.
|
|
90
90
|
*/
|
|
91
91
|
export interface FieldEventContext {
|
|
92
|
+
readonly namespaceId: string;
|
|
92
93
|
readonly tableName: string;
|
|
93
94
|
readonly fieldName: string;
|
|
94
95
|
readonly priorTable?: StorageTable;
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
PslSpan,
|
|
13
13
|
PslTypesBlock,
|
|
14
14
|
} from '@prisma-next/framework-components/psl-ast';
|
|
15
|
+
import { UNSPECIFIED_PSL_NAMESPACE_ID } from '@prisma-next/framework-components/psl-ast';
|
|
15
16
|
import type { SqlColumnIR, SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
|
|
16
17
|
import type { DefaultMappingOptions } from './default-mapping';
|
|
17
18
|
import { mapDefault } from './default-mapping';
|
|
@@ -153,12 +154,25 @@ function buildPslDocumentAst(schemaIR: SqlSchemaIR, options: PslPrinterOptions):
|
|
|
153
154
|
}
|
|
154
155
|
: undefined;
|
|
155
156
|
|
|
157
|
+
// Inferred PSL nodes will eventually be routed into per-namespace buckets
|
|
158
|
+
// matching the source storage; for now we synthesise a single
|
|
159
|
+
// `__unspecified__` bucket so round-tripping the AST through the framework
|
|
160
|
+
// printer (which emits the synthesised bucket at top level with no
|
|
161
|
+
// `namespace { … }` wrapper) preserves the existing introspection output
|
|
162
|
+
// verbatim.
|
|
156
163
|
const ast: PslDocumentAst = {
|
|
157
164
|
kind: 'document',
|
|
158
165
|
sourceId: '<sql-schema-ir>',
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
166
|
+
namespaces: [
|
|
167
|
+
{
|
|
168
|
+
kind: 'namespace',
|
|
169
|
+
name: UNSPECIFIED_PSL_NAMESPACE_ID,
|
|
170
|
+
models: sortedModels,
|
|
171
|
+
enums,
|
|
172
|
+
compositeTypes: [],
|
|
173
|
+
span: SYNTHETIC_SPAN,
|
|
174
|
+
},
|
|
175
|
+
],
|
|
162
176
|
...(types ? { types } : {}),
|
|
163
177
|
span: SYNTHETIC_SPAN,
|
|
164
178
|
};
|