@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/package.json CHANGED
@@ -1,34 +1,34 @@
1
1
  {
2
2
  "name": "@prisma-next/family-sql",
3
- "version": "0.9.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.9.0",
10
- "@prisma-next/emitter": "0.9.0",
11
- "@prisma-next/framework-components": "0.9.0",
12
- "@prisma-next/migration-tools": "0.9.0",
13
- "@prisma-next/operations": "0.9.0",
14
- "@prisma-next/sql-contract": "0.9.0",
15
- "@prisma-next/sql-contract-emitter": "0.9.0",
16
- "@prisma-next/sql-contract-ts": "0.9.0",
17
- "@prisma-next/sql-operations": "0.9.0",
18
- "@prisma-next/sql-relational-core": "0.9.0",
19
- "@prisma-next/sql-runtime": "0.9.0",
20
- "@prisma-next/sql-schema-ir": "0.9.0",
21
- "@prisma-next/utils": "0.9.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.9.0",
26
- "@prisma-next/psl-parser": "0.9.0",
27
- "@prisma-next/psl-printer": "0.9.0",
28
- "@prisma-next/sql-contract-psl": "0.9.0",
29
- "@prisma-next/test-utils": "0.9.0",
30
- "@prisma-next/tsconfig": "0.9.0",
31
- "@prisma-next/tsdown": "0.9.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
- 'tables' in contract.storage
65
+ 'namespaces' in contract.storage &&
66
+ typeof contract.storage.namespaces === 'object' &&
67
+ contract.storage.namespaces !== null
66
68
  ) {
67
- const storage = contract.storage as { tables?: Record<string, unknown> };
68
- if (storage.tables && typeof storage.tables === 'object') {
69
- for (const table of Object.values(storage.tables)) {
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 { SqlStorage, type SqlStorageTypeEntry } from '@prisma-next/sql-contract/types';
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): TContract {
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
- ...validated.storage,
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
- * Per-entry hydration dispatcher for `storage.types`. When `kind` is a
108
- * string and the constructor registry supplies a factory for that key,
109
- * the factory returns the hydrated `SqlStorageTypeEntry`. Otherwise the
110
- * entry passes through unchanged for `SqlStorage` to normalise.
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
- type StorageTable,
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.references.table,
152
- referencedColumns: fk.references.columns,
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
- for (const tableName of Object.keys(from.tables)) {
223
- if (!hasOwn(to.tables, tableName)) {
224
- conflicts.push({
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
- const toTable = to.tables[tableName] as StorageTable;
232
- const fromTable = from.tables[tableName];
233
- if (!fromTable) continue;
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 columnName of Object.keys(fromTable.columns)) {
236
- if (!hasOwn(toTable.columns, columnName)) {
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: 'columnRemoved',
239
- summary: `Column "${tableName}"."${columnName}" was removed`,
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 storageTypes = (storage.types ?? {}) as ResolvedStorageTypes;
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 [tableName, tableDef] of Object.entries(storage.tables)) {
284
- tables[tableName] = convertTable(
285
- tableName,
286
- tableDef,
287
- storageTypes,
288
- options.expandNativeType,
289
- options.renderDefault,
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 types = storage.types as ResolvedStorageTypes | undefined;
306
- if (!types || Object.keys(types).length === 0) return undefined;
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 { SqlStorage, StorageColumn, StorageTable } from '@prisma-next/sql-contract/types';
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 priorTables = options.priorContract?.storage.tables ?? {};
65
- const newTables = options.newContract.storage.tables;
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 tableNames = unionSorted(Object.keys(priorTables), Object.keys(newTables));
72
- for (const tableName of tableNames) {
73
- const priorTable = priorTables[tableName];
74
- const newTable = newTables[tableName];
75
- const fieldNames = unionSorted(
76
- priorTable ? Object.keys(priorTable.columns) : [],
77
- newTable ? Object.keys(newTable.columns) : [],
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
- for (const fieldName of fieldNames) {
80
- const priorField = priorTable?.columns[fieldName];
81
- const newField = newTable?.columns[fieldName];
82
- const entry: FieldEntry = {
83
- tableName,
84
- fieldName,
85
- priorTable,
86
- newTable,
87
- priorField,
88
- newField,
89
- };
90
- if (priorField === undefined && newField !== undefined) {
91
- added.push(entry);
92
- } else if (priorField !== undefined && newField === undefined) {
93
- dropped.push(entry);
94
- } else if (priorField !== undefined && newField !== undefined) {
95
- if (isAlteration(priorField, newField)) altered.push(entry);
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 = { tableName: entry.tableName, fieldName: entry.fieldName };
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` and `fieldName` are always populated; `priorTable` /
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
- models: sortedModels,
160
- enums,
161
- compositeTypes: [],
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
  };