@prisma-next/family-sql 0.13.0-dev.4 → 0.13.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.
Files changed (54) hide show
  1. package/dist/authoring-type-constructors-CjFfO6LM.mjs +342 -0
  2. package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +1 -0
  3. package/dist/{control-adapter-CgIL9Vtx.d.mts → control-adapter-Cmw9LvEP.d.mts} +16 -33
  4. package/dist/control-adapter-Cmw9LvEP.d.mts.map +1 -0
  5. package/dist/control-adapter.d.mts +2 -2
  6. package/dist/control.d.mts +36 -34
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +24 -77
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/ir.d.mts +6 -4
  11. package/dist/ir.d.mts.map +1 -1
  12. package/dist/ir.mjs +1 -1
  13. package/dist/migration.d.mts +1 -1
  14. package/dist/migration.d.mts.map +1 -1
  15. package/dist/migration.mjs +2 -1
  16. package/dist/migration.mjs.map +1 -1
  17. package/dist/pack.d.mts +16 -3
  18. package/dist/pack.d.mts.map +1 -1
  19. package/dist/pack.mjs +4 -2
  20. package/dist/pack.mjs.map +1 -1
  21. package/dist/schema-verify.d.mts +1 -1
  22. package/dist/schema-verify.mjs +1 -1
  23. package/dist/{sql-contract-serializer-CY7qnms7.mjs → sql-contract-serializer-BR2vC7Z-.mjs} +27 -27
  24. package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +1 -0
  25. package/dist/{types-CbwQCzXY.d.mts → types-kgstZ_Zd.d.mts} +5 -5
  26. package/dist/types-kgstZ_Zd.d.mts.map +1 -0
  27. package/dist/{verify-sql-schema-DcMaT5Zj.d.mts → verify-sql-schema-thU-jKpf.d.mts} +2 -14
  28. package/dist/verify-sql-schema-thU-jKpf.d.mts.map +1 -0
  29. package/dist/{verify-sql-schema-DlAgBiT_.mjs → verify-sql-schema-xT4udQLQ.mjs} +25 -118
  30. package/dist/verify-sql-schema-xT4udQLQ.mjs.map +1 -0
  31. package/package.json +21 -21
  32. package/src/core/authoring-entity-types.ts +178 -0
  33. package/src/core/authoring-field-presets.ts +8 -3
  34. package/src/core/control-adapter.ts +18 -49
  35. package/src/core/control-descriptor.ts +3 -0
  36. package/src/core/control-instance.ts +13 -11
  37. package/src/core/ir/sql-contract-serializer-base.ts +44 -75
  38. package/src/core/ir/sql-contract-serializer.ts +7 -0
  39. package/src/core/migrations/contract-to-schema-ir.ts +47 -112
  40. package/src/core/migrations/types.ts +4 -1
  41. package/src/core/psl-contract-infer/postgres-type-map.ts +5 -13
  42. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +17 -70
  43. package/src/core/schema-verify/verify-sql-schema.ts +10 -146
  44. package/src/core/sql-migration.ts +5 -1
  45. package/src/exports/control-adapter.ts +1 -0
  46. package/src/exports/control.ts +1 -1
  47. package/src/exports/pack.ts +3 -0
  48. package/dist/authoring-type-constructors-D4lQ-qpj.mjs +0 -192
  49. package/dist/authoring-type-constructors-D4lQ-qpj.mjs.map +0 -1
  50. package/dist/control-adapter-CgIL9Vtx.d.mts.map +0 -1
  51. package/dist/sql-contract-serializer-CY7qnms7.mjs.map +0 -1
  52. package/dist/types-CbwQCzXY.d.mts.map +0 -1
  53. package/dist/verify-sql-schema-DcMaT5Zj.d.mts.map +0 -1
  54. package/dist/verify-sql-schema-DlAgBiT_.mjs.map +0 -1
@@ -0,0 +1,178 @@
1
+ import type { JsonValue } from '@prisma-next/contract/types';
2
+ import type {
3
+ AuthoringEntityContext,
4
+ AuthoringEntityTypeDescriptor,
5
+ AuthoringEntityTypeNamespace,
6
+ AuthoringPslBlockDescriptorNamespace,
7
+ PslExtensionBlock,
8
+ } from '@prisma-next/framework-components/authoring';
9
+ import { type EnumTypeHandle, enumType } from '@prisma-next/sql-contract-ts/contract-builder';
10
+ import { blindCast } from '@prisma-next/utils/casts';
11
+
12
+ function parseQuotedString(raw: string): string | undefined {
13
+ if (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) {
14
+ return raw.slice(1, -1);
15
+ }
16
+ return undefined;
17
+ }
18
+
19
+ export const sqlFamilyEnumEntityDescriptor = {
20
+ kind: 'entity' as const,
21
+ discriminator: 'enum',
22
+ output: {
23
+ factory: (
24
+ block: PslExtensionBlock,
25
+ ctx: AuthoringEntityContext,
26
+ ): EnumTypeHandle | undefined => {
27
+ const sourceId = ctx.sourceId ?? 'unknown';
28
+ const diagnostics = ctx.diagnostics;
29
+
30
+ const typeAttr = block.blockAttributes.find((a) => a.name === 'type');
31
+ if (!typeAttr) {
32
+ diagnostics?.push({
33
+ code: 'PSL_ENUM_MISSING_TYPE',
34
+ message: `enum "${block.name}" is missing a @@type("codecId") attribute`,
35
+ sourceId,
36
+ span: block.span,
37
+ });
38
+ return undefined;
39
+ }
40
+
41
+ const rawCodecArg = typeAttr.args[0]?.value;
42
+ const codecId = rawCodecArg !== undefined ? parseQuotedString(rawCodecArg) : undefined;
43
+ if (!codecId) {
44
+ diagnostics?.push({
45
+ code: 'PSL_ENUM_MISSING_TYPE',
46
+ message: `enum "${block.name}" @@type attribute must have a quoted codec id argument`,
47
+ sourceId,
48
+ span: typeAttr.span,
49
+ });
50
+ return undefined;
51
+ }
52
+
53
+ const nativeType = ctx.codecLookup?.targetTypesFor(codecId)?.[0];
54
+ if (nativeType === undefined) {
55
+ const typeArgSpan = typeAttr.args[0]?.span ?? typeAttr.span;
56
+ diagnostics?.push({
57
+ code: 'PSL_EXTENSION_INVALID_VALUE',
58
+ message: `enum "${block.name}" @@type references unknown codec "${codecId}"`,
59
+ sourceId,
60
+ span: typeArgSpan,
61
+ });
62
+ return undefined;
63
+ }
64
+
65
+ const codec = ctx.codecLookup?.get(codecId);
66
+ if (codec === undefined) {
67
+ const typeArgSpan = typeAttr.args[0]?.span ?? typeAttr.span;
68
+ diagnostics?.push({
69
+ code: 'PSL_EXTENSION_INVALID_VALUE',
70
+ message: `enum "${block.name}" @@type codec "${codecId}" resolves in targetTypesFor but is absent from codecLookup.get`,
71
+ sourceId,
72
+ span: typeArgSpan,
73
+ });
74
+ return undefined;
75
+ }
76
+
77
+ const seenValues = new Set<string>();
78
+ const members: { name: string; value: unknown }[] = [];
79
+ let memberError = false;
80
+
81
+ for (const [memberName, paramValue] of Object.entries(block.parameters)) {
82
+ let value: unknown;
83
+ if (paramValue.kind === 'bare') {
84
+ try {
85
+ value = codec.decodeJson(memberName);
86
+ } catch {
87
+ diagnostics?.push({
88
+ code: 'PSL_ENUM_BARE_MEMBER_NON_STRING_CODEC',
89
+ message: `enum "${block.name}" member "${memberName}" has no value and codec "${codecId}" does not accept a bare name as input`,
90
+ sourceId,
91
+ span: paramValue.span,
92
+ });
93
+ memberError = true;
94
+ continue;
95
+ }
96
+ } else if (paramValue.kind === 'value') {
97
+ let jsonValue: unknown;
98
+ try {
99
+ jsonValue = JSON.parse(paramValue.raw);
100
+ } catch {
101
+ diagnostics?.push({
102
+ code: 'PSL_EXTENSION_INVALID_VALUE',
103
+ message: `enum "${block.name}" member "${memberName}" value "${paramValue.raw}" is not valid JSON`,
104
+ sourceId,
105
+ span: paramValue.span,
106
+ });
107
+ memberError = true;
108
+ continue;
109
+ }
110
+ try {
111
+ value = codec.decodeJson(
112
+ blindCast<JsonValue, 'JSON.parse returns a JsonValue-compatible value'>(jsonValue),
113
+ );
114
+ } catch (err) {
115
+ const reason = err instanceof Error ? err.message : String(err);
116
+ diagnostics?.push({
117
+ code: 'PSL_EXTENSION_INVALID_VALUE',
118
+ message: `enum "${block.name}" member "${memberName}" was rejected by codec "${codecId}": ${reason}`,
119
+ sourceId,
120
+ span: paramValue.span,
121
+ });
122
+ memberError = true;
123
+ continue;
124
+ }
125
+ } else {
126
+ continue;
127
+ }
128
+
129
+ const valueKey = String(value);
130
+ if (seenValues.has(valueKey)) {
131
+ diagnostics?.push({
132
+ code: 'PSL_ENUM_DUPLICATE_MEMBER_VALUE',
133
+ message: `enum "${block.name}": duplicate member value "${valueKey}"`,
134
+ sourceId,
135
+ span: paramValue.span,
136
+ });
137
+ memberError = true;
138
+ continue;
139
+ }
140
+ seenValues.add(valueKey);
141
+ members.push({ name: memberName, value });
142
+ }
143
+
144
+ if (memberError) return undefined;
145
+
146
+ if (members.length === 0) {
147
+ diagnostics?.push({
148
+ code: 'PSL_ENUM_MISSING_TYPE',
149
+ message: `enum "${block.name}" must have at least one member`,
150
+ sourceId,
151
+ span: block.span,
152
+ });
153
+ return undefined;
154
+ }
155
+
156
+ return enumType(
157
+ block.name,
158
+ { codecId, nativeType },
159
+ ...members.map((m) => ({ name: m.name, value: m.value })),
160
+ );
161
+ },
162
+ },
163
+ } satisfies AuthoringEntityTypeDescriptor;
164
+
165
+ export const sqlFamilyEntityTypes: AuthoringEntityTypeNamespace = {
166
+ enum: sqlFamilyEnumEntityDescriptor,
167
+ };
168
+
169
+ export const sqlFamilyPslBlockDescriptors = {
170
+ enum: {
171
+ kind: 'pslBlock',
172
+ keyword: 'enum',
173
+ discriminator: 'enum',
174
+ name: { required: true },
175
+ parameters: {},
176
+ variadicParameters: true,
177
+ },
178
+ } as const satisfies AuthoringPslBlockDescriptorNamespace;
@@ -9,6 +9,11 @@ import type { AuthoringFieldNamespace } from '@prisma-next/framework-components/
9
9
  * (`character`) regardless of target, and the PSL interpreter lets the
10
10
  * generator override the scalar descriptor.
11
11
  *
12
+ * The `uuidString` / `id.uuidv4String` / `id.uuidv7String` presets store UUID
13
+ * values as `character(36)` — portable across all SQL targets. For a native
14
+ * Postgres `uuid` column use `uuidNative` / `id.uuidv4Native` /
15
+ * `id.uuidv7Native` from `@prisma-next/target-postgres`.
16
+ *
12
17
  * Scalar presets that map to target-specific codecs (e.g. `text`, `int`,
13
18
  * `boolean`, `dateTime`) are contributed by the target pack (see
14
19
  * `postgresAuthoringFieldPresets` in `@prisma-next/target-postgres`) so the
@@ -34,7 +39,7 @@ const nanoidOptionsArgument = {
34
39
  } as const;
35
40
 
36
41
  export const sqlFamilyAuthoringFieldPresets = {
37
- uuid: {
42
+ uuidString: {
38
43
  kind: 'fieldPreset',
39
44
  output: {
40
45
  codecId: CHARACTER_CODEC_ID,
@@ -91,7 +96,7 @@ export const sqlFamilyAuthoringFieldPresets = {
91
96
  },
92
97
  },
93
98
  id: {
94
- uuidv4: {
99
+ uuidv4String: {
95
100
  kind: 'fieldPreset',
96
101
  output: {
97
102
  codecId: CHARACTER_CODEC_ID,
@@ -108,7 +113,7 @@ export const sqlFamilyAuthoringFieldPresets = {
108
113
  id: true,
109
114
  },
110
115
  },
111
- uuidv7: {
116
+ uuidv7String: {
112
117
  kind: 'fieldPreset',
113
118
  output: {
114
119
  codecId: CHARACTER_CODEC_ID,
@@ -1,22 +1,15 @@
1
- import type {
2
- Contract,
3
- ContractMarkerRecord,
4
- LedgerEntryRecord,
5
- } from '@prisma-next/contract/types';
1
+ import type { ContractMarkerRecord, LedgerEntryRecord } from '@prisma-next/contract/types';
6
2
  import type {
7
3
  ControlAdapterInstance,
8
4
  ControlStack,
9
5
  } from '@prisma-next/framework-components/control';
10
- import type {
11
- PostgresEnumStorageEntry,
12
- SqlControlDriverInstance,
13
- SqlStorage,
14
- } from '@prisma-next/sql-contract/types';
6
+ import type { SqlControlDriverInstance } from '@prisma-next/sql-contract/types';
15
7
  import type {
16
8
  AnyQueryAst,
17
9
  DdlNode,
18
10
  LoweredStatement,
19
11
  LowererContext,
12
+ SqlExecuteRequest,
20
13
  } from '@prisma-next/sql-relational-core/ast';
21
14
  import type { SqlSchemaIR } from '@prisma-next/sql-schema-ir/types';
22
15
  import type { DefaultNormalizer, NativeTypeNormalizer } from './schema-verify/verify-sql-schema';
@@ -32,6 +25,19 @@ export interface Lowerer {
32
25
  lower(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement;
33
26
  }
34
27
 
28
+ /**
29
+ * Extends {@link Lowerer} with async codec-routed DDL lowering. Control
30
+ * adapters implement this; the planner's `CreateTableCall.toOp` and
31
+ * `CreateSchemaCall.toOp` accept it to produce executable DDL statements
32
+ * with literal defaults encoded through their codec.
33
+ */
34
+ export interface ExecuteRequestLowerer extends Lowerer {
35
+ lowerToExecuteRequest(
36
+ ast: AnyQueryAst | DdlNode,
37
+ context?: LowererContext<unknown>,
38
+ ): Promise<SqlExecuteRequest>;
39
+ }
40
+
35
41
  /**
36
42
  * SQL control adapter interface for control-plane operations.
37
43
  * Implemented by target-specific adapters (e.g., Postgres, MySQL).
@@ -39,7 +45,8 @@ export interface Lowerer {
39
45
  * @template TTarget - The target ID (e.g., 'postgres', 'mysql')
40
46
  */
41
47
  export interface SqlControlAdapter<TTarget extends string = string>
42
- extends ControlAdapterInstance<'sql', TTarget> {
48
+ extends ControlAdapterInstance<'sql', TTarget>,
49
+ ExecuteRequestLowerer {
43
50
  /**
44
51
  * Reads the contract marker for `space` from the database, returning
45
52
  * `null` if no marker row exists for that space (or if the marker
@@ -181,33 +188,6 @@ export interface SqlControlAdapter<TTarget extends string = string>
181
188
  */
182
189
  readonly normalizeNativeType?: NativeTypeNormalizer;
183
190
 
184
- /**
185
- * Optional bridging adapter for resolving the existing values of a
186
- * native enum type from the introspected schema IR. Targets supply
187
- * this so the family-level schema verifier can walk
188
- * `PostgresEnumStorageEntry` entries natively without needing to
189
- * know the target-specific `schema.annotations` shape
190
- * (e.g. `schema.annotations.pg.storageTypes`).
191
- */
192
- readonly resolveExistingEnumValues?: (
193
- schema: SqlSchemaIR,
194
- enumType: PostgresEnumStorageEntry,
195
- namespaceId: string,
196
- ) => readonly string[] | null;
197
- /**
198
- * Optional contract-scoped factory for {@link resolveExistingEnumValues}.
199
- * Targets that need the contract storage to resolve namespace → DDL schema
200
- * supply this; the family control instance prefers it over the bare adapter
201
- * hook when present.
202
- */
203
- readonly resolveExistingEnumValuesForContract?: (
204
- contract: Contract<SqlStorage>,
205
- ) => (
206
- schema: SqlSchemaIR,
207
- enumType: PostgresEnumStorageEntry,
208
- namespaceId: string,
209
- ) => readonly string[] | null;
210
-
211
191
  /**
212
192
  * Ordered DDL queries that bootstrap marker/ledger control tables for migration
213
193
  * runners. Postgres includes `CREATE SCHEMA`; SQLite does not.
@@ -219,17 +199,6 @@ export interface SqlControlAdapter<TTarget extends string = string>
219
199
  * `sign` — excludes the ledger table.
220
200
  */
221
201
  bootstrapSignMarkerQueries(): readonly DdlNode[];
222
-
223
- /**
224
- * Lower a SQL query AST into a target-flavored `{ sql, params }` payload.
225
- *
226
- * Migration tooling (e.g. the `dataTransform` operation) needs to materialize
227
- * SQL at emit/plan time without instantiating the runtime adapter. The control
228
- * adapter's `lower` is byte-equivalent to the runtime adapter's `lower` for the
229
- * same AST and contract, ensuring planned SQL matches what the runtime would
230
- * emit.
231
- */
232
- lower(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement;
233
202
  }
234
203
 
235
204
  /**
@@ -4,6 +4,7 @@ import type {
4
4
  } from '@prisma-next/framework-components/control';
5
5
  import type { EmissionSpi } from '@prisma-next/framework-components/emission';
6
6
  import { sqlEmission } from '@prisma-next/sql-contract-emitter';
7
+ import { sqlFamilyEntityTypes, sqlFamilyPslBlockDescriptors } from './authoring-entity-types';
7
8
  import { sqlFamilyAuthoringFieldPresets } from './authoring-field-presets';
8
9
  import { sqlFamilyAuthoringTypes } from './authoring-type-constructors';
9
10
  import { createSqlFamilyInstance, type SqlControlFamilyInstance } from './control-instance';
@@ -19,6 +20,8 @@ export class SqlFamilyDescriptor
19
20
  readonly authoring = {
20
21
  field: sqlFamilyAuthoringFieldPresets,
21
22
  type: sqlFamilyAuthoringTypes,
23
+ entityTypes: sqlFamilyEntityTypes,
24
+ pslBlockDescriptors: sqlFamilyPslBlockDescriptors,
22
25
  } as const;
23
26
 
24
27
  create<TTargetId extends string>(
@@ -35,8 +35,8 @@ import type { SqlControlDriverInstance, SqlStorage } from '@prisma-next/sql-cont
35
35
  import type {
36
36
  AnyQueryAst,
37
37
  DdlNode,
38
- LoweredStatement,
39
38
  LowererContext,
39
+ SqlExecuteRequest,
40
40
  } from '@prisma-next/sql-relational-core/ast';
41
41
  import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
42
42
  import type { SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
@@ -68,10 +68,10 @@ function extractCodecTypeIdsFromContract(contract: unknown): readonly string[] {
68
68
  ) {
69
69
  const namespaces = contract.storage.namespaces as Record<
70
70
  string,
71
- { readonly entries: { readonly table: Readonly<Record<string, unknown>> } }
71
+ { readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>> }
72
72
  >;
73
73
  for (const ns of Object.values(namespaces)) {
74
- const tbls = ns.entries.table;
74
+ const tbls = ns.entries['table'];
75
75
  if (typeof tbls !== 'object' || tbls === null) continue;
76
76
  for (const table of Object.values(tbls)) {
77
77
  if (
@@ -240,7 +240,10 @@ export interface SqlControlFamilyInstance
240
240
 
241
241
  inferPslContract(schemaIR: SqlSchemaIR): PslDocumentAst;
242
242
 
243
- lowerAst(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement;
243
+ lowerAst(
244
+ ast: AnyQueryAst | DdlNode,
245
+ context: LowererContext<unknown>,
246
+ ): Promise<SqlExecuteRequest>;
244
247
 
245
248
  /**
246
249
  * Inserts the initial marker row for `space` (upsert on `space`).
@@ -672,9 +675,6 @@ export function createSqlFamilyInstance<TTargetId extends string>(
672
675
  }): VerifyDatabaseSchemaResult {
673
676
  const contract = deserializeWithTargetSerializer(options.contract) as Contract<SqlStorage>;
674
677
  const controlAdapter = getControlAdapter();
675
- const resolveExistingEnumValues =
676
- controlAdapter.resolveExistingEnumValuesForContract?.(contract) ??
677
- controlAdapter.resolveExistingEnumValues;
678
678
  return verifySqlSchema({
679
679
  contract,
680
680
  schema: options.schema,
@@ -683,7 +683,6 @@ export function createSqlFamilyInstance<TTargetId extends string>(
683
683
  frameworkComponents: options.frameworkComponents,
684
684
  ...ifDefined('normalizeDefault', controlAdapter.normalizeDefault),
685
685
  ...ifDefined('normalizeNativeType', controlAdapter.normalizeNativeType),
686
- ...ifDefined('resolveExistingEnumValues', resolveExistingEnumValues),
687
686
  });
688
687
  },
689
688
  async sign(options: {
@@ -707,7 +706,7 @@ export function createSqlFamilyInstance<TTargetId extends string>(
707
706
  const controlAdapter = getControlAdapter();
708
707
  const lowererContext = { contract };
709
708
  for (const query of controlAdapter.bootstrapSignMarkerQueries()) {
710
- const lowered = controlAdapter.lower(query, lowererContext);
709
+ const lowered = await controlAdapter.lowerToExecuteRequest(query, lowererContext);
711
710
  await driver.query(lowered.sql, lowered.params);
712
711
  }
713
712
 
@@ -857,8 +856,11 @@ export function createSqlFamilyInstance<TTargetId extends string>(
857
856
  return sqlSchemaIrToPslAst(schemaIR);
858
857
  },
859
858
 
860
- lowerAst(ast: AnyQueryAst | DdlNode, context: LowererContext<unknown>): LoweredStatement {
861
- return getControlAdapter().lower(ast, context);
859
+ lowerAst(
860
+ ast: AnyQueryAst | DdlNode,
861
+ context: LowererContext<unknown>,
862
+ ): Promise<SqlExecuteRequest> {
863
+ return getControlAdapter().lowerToExecuteRequest(ast, context);
862
864
  },
863
865
 
864
866
  bootstrapControlTableQueries(): readonly DdlNode[] {
@@ -1,12 +1,16 @@
1
1
  import { ContractValidationError } from '@prisma-next/contract/contract-validation-error';
2
+ import { isPlainRecord } from '@prisma-next/contract/is-plain-record';
2
3
  import type { Contract } from '@prisma-next/contract/types';
3
4
  import type { ContractSerializer } from '@prisma-next/framework-components/control';
4
5
  import {
6
+ type AnyEntityKindDescriptor,
7
+ hydrateNamespaceEntities,
5
8
  type Namespace,
6
9
  NamespaceBase,
7
10
  UNBOUND_NAMESPACE_ID,
8
11
  } from '@prisma-next/framework-components/ir';
9
12
  import { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';
13
+ import { composeSqlEntityKinds } from '@prisma-next/sql-contract/entity-kinds';
10
14
  import {
11
15
  buildSqlNamespace,
12
16
  type SqlNamespaceTablesInput,
@@ -14,10 +18,6 @@ import {
14
18
  type SqlStorageInput,
15
19
  type SqlStorageTypeEntry,
16
20
  SqlUnboundNamespace,
17
- StorageTable,
18
- type StorageTableInput,
19
- StorageValueSet,
20
- type StorageValueSetInput,
21
21
  } from '@prisma-next/sql-contract/types';
22
22
  import {
23
23
  createSqlContractSchema,
@@ -36,10 +36,6 @@ const NamespaceRawSchema = type({
36
36
  }),
37
37
  });
38
38
 
39
- function isPlainRecord(value: unknown): value is Record<string, unknown> {
40
- return typeof value === 'object' && value !== null && !Array.isArray(value);
41
- }
42
-
43
39
  export type SqlEntityHydrationFactory = (entry: unknown) => unknown;
44
40
 
45
41
  /**
@@ -70,21 +66,18 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
70
66
  implements ContractSerializer<TContract>
71
67
  {
72
68
  private readonly contractSchema: Type<unknown> | undefined;
69
+ private readonly entryKinds: ReadonlyMap<string, AnyEntityKindDescriptor>;
73
70
 
74
71
  constructor(
75
- protected readonly entityTypeRegistry: ReadonlyMap<
72
+ protected readonly entityHydrationRegistry: ReadonlyMap<
76
73
  string,
77
74
  SqlEntityHydrationFactory
78
75
  > = new Map(),
79
- validatorFragments?: ReadonlyMap<string, Type<unknown>>,
76
+ packEntityKinds: readonly AnyEntityKindDescriptor[] = [],
80
77
  ) {
81
- // Only build a fragments-aware contract schema when pack contributions
82
- // exist. The cached module-level default in `validators.ts` covers the
83
- // no-contributions case and avoids per-instance schema compilation.
78
+ this.entryKinds = composeSqlEntityKinds(packEntityKinds);
84
79
  this.contractSchema =
85
- validatorFragments !== undefined && validatorFragments.size > 0
86
- ? createSqlContractSchema(validatorFragments)
87
- : undefined;
80
+ packEntityKinds.length > 0 ? createSqlContractSchema(this.entryKinds) : undefined;
88
81
  }
89
82
 
90
83
  deserializeContract<T extends TContract = TContract>(json: unknown): T {
@@ -136,7 +129,19 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
136
129
  // deserialized JSON (e.g. buildMixedPolyContract) working by providing a slot to
137
130
  // write into. Once runtime-qualification routes table lookups by namespace, this
138
131
  // shim should be removed.
139
- const unbound = hydratedNamespaces[UNBOUND_NAMESPACE_ID] ?? SqlUnboundNamespace.instance;
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;
140
145
 
141
146
  return {
142
147
  ...validated,
@@ -147,12 +152,14 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
147
152
  // framework `Namespace` to the SQL-family `SqlNamespace`.
148
153
  namespaces: blindCast<
149
154
  SqlStorageInput['namespaces'],
150
- 'hydrated SQL namespaces are SqlNamespace instances (family hydration guarantees this)'
151
- >({ ...hydratedNamespaces, [UNBOUND_NAMESPACE_ID]: unbound }),
155
+ 'hydrateSqlNamespaceMap builds each namespace through the SQL family concretions (SqlBoundNamespace / target schema), so every value is a SqlNamespace; the framework return type only promises the base Namespace.'
156
+ >(withInjectedUnbound),
152
157
  }),
153
158
  };
154
159
  }
155
160
 
161
+ protected abstract get defaultNamespaceId(): string;
162
+
156
163
  protected hydrateSqlNamespaceMap(
157
164
  namespaces: Readonly<Record<string, Namespace | Record<string, unknown>>>,
158
165
  ): Readonly<Record<string, Namespace>> {
@@ -182,71 +189,33 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
182
189
  return raw;
183
190
  }
184
191
  const rawRecord = isPlainRecord(raw) ? raw : {};
185
- if (
186
- Object.hasOwn(rawRecord, 'tables') ||
187
- Object.hasOwn(rawRecord, 'enum') ||
188
- Object.hasOwn(rawRecord, 'collections')
189
- ) {
190
- throw new ContractValidationError(
191
- 'Namespace envelope uses deprecated flat slot keys; expected `entries: { table? }`',
192
- 'structural',
193
- );
194
- }
195
192
  const id = typeof rawRecord['id'] === 'string' ? rawRecord['id'] : nsId;
196
193
  const parsed = NamespaceRawSchema({ ...rawRecord, id });
197
194
  if (parsed instanceof type.errors) {
198
195
  const messages = parsed.map((p: { message: string }) => p.message).join('; ');
199
196
  throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');
200
197
  }
201
- // Default to empty table; overwritten below if raw entries carry a table slot.
202
- const entriesInput: {
203
- table: Record<string, StorageTable>;
204
- valueSet?: Record<string, StorageValueSet>;
205
- } = { table: {} };
206
198
  const entriesRaw = parsed.entries;
207
- if (entriesRaw !== undefined && typeof entriesRaw === 'object' && entriesRaw !== null) {
208
- const rawEntries = entriesRaw as Record<string, unknown>;
209
- const tableSlot = rawEntries['table'];
210
- if (tableSlot !== null && typeof tableSlot === 'object' && !Array.isArray(tableSlot)) {
211
- entriesInput.table = Object.fromEntries(
212
- Object.entries(tableSlot as Record<string, unknown>).map(([tableName, table]) => [
213
- tableName,
214
- table instanceof StorageTable ? table : new StorageTable(table as StorageTableInput),
215
- ]),
216
- );
217
- }
218
- const valueSetSlot = rawEntries['valueSet'];
219
- if (
220
- valueSetSlot !== null &&
221
- typeof valueSetSlot === 'object' &&
222
- !Array.isArray(valueSetSlot)
223
- ) {
224
- entriesInput.valueSet = Object.fromEntries(
225
- Object.entries(
226
- blindCast<
227
- Record<string, unknown>,
228
- 'valueSet slot is a plain record after object check'
229
- >(valueSetSlot),
230
- ).map(([vsName, vs]) => [
231
- vsName,
232
- vs instanceof StorageValueSet
233
- ? vs
234
- : new StorageValueSet(
235
- blindCast<
236
- StorageValueSetInput,
237
- 'non-instance valueSet entry is StorageValueSetInput'
238
- >(vs),
239
- ),
240
- ]),
241
- );
242
- }
243
- // Target-specific slots (e.g. postgres `type`) are left for target
244
- // overrides to extract from the original `raw` parameter.
199
+ const rawEntriesMap = isPlainRecord(entriesRaw) ? entriesRaw : {};
200
+
201
+ const entriesInput: Record<string, Readonly<Record<string, unknown>>> = {};
202
+ for (const [key, innerMap] of Object.entries(rawEntriesMap)) {
203
+ entriesInput[key] = isPlainRecord(innerMap) ? innerMap : Object.freeze({});
204
+ }
205
+
206
+ const entriesOutput = hydrateNamespaceEntities(entriesInput, this.entryKinds, 'fail', id);
207
+
208
+ // Always ensure a 'table' key is present (may be empty).
209
+ if (!Object.hasOwn(entriesOutput, 'table')) {
210
+ entriesOutput['table'] = {};
245
211
  }
246
212
 
247
- return blindCast<SqlNamespaceTablesInput, 'hydrated namespace tables input'>({
213
+ return blindCast<
214
+ SqlNamespaceTablesInput,
215
+ 'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the SqlNamespaceTablesInput the family createNamespace consumes.'
216
+ >({
248
217
  id,
249
- entries: entriesInput,
218
+ entries: entriesOutput,
250
219
  });
251
220
  }
252
221
 
@@ -258,7 +227,7 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
258
227
  if (typeof kind !== 'string') {
259
228
  return entry;
260
229
  }
261
- const factory = this.entityTypeRegistry.get(kind);
230
+ const factory = this.entityHydrationRegistry.get(kind);
262
231
  if (factory === undefined) {
263
232
  return entry;
264
233
  }
@@ -1,4 +1,5 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
+ import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
2
3
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
3
4
  import { SqlContractSerializerBase } from './sql-contract-serializer-base';
4
5
 
@@ -15,4 +16,10 @@ export class SqlContractSerializer extends SqlContractSerializerBase<Contract<Sq
15
16
  constructor() {
16
17
  super(new Map());
17
18
  }
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
+ }
18
25
  }