@prisma-next/family-sql 0.14.0-dev.5 → 0.14.0-dev.51

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 (79) hide show
  1. package/dist/{authoring-type-constructors-CjFfO6LM.mjs → authoring-type-constructors-CXd-8ydc.mjs} +7 -30
  2. package/dist/authoring-type-constructors-CXd-8ydc.mjs.map +1 -0
  3. package/dist/{control-adapter-Cmw9LvEP.d.mts → control-adapter-BVOj4gXI.d.mts} +42 -8
  4. package/dist/control-adapter-BVOj4gXI.d.mts.map +1 -0
  5. package/dist/control-adapter.d.mts +1 -1
  6. package/dist/control.d.mts +238 -6
  7. package/dist/control.d.mts.map +1 -1
  8. package/dist/control.mjs +336 -942
  9. package/dist/control.mjs.map +1 -1
  10. package/dist/diff.d.mts +150 -0
  11. package/dist/diff.d.mts.map +1 -0
  12. package/dist/diff.mjs +188 -0
  13. package/dist/diff.mjs.map +1 -0
  14. package/dist/ir.d.mts +26 -9
  15. package/dist/ir.d.mts.map +1 -1
  16. package/dist/ir.mjs +2 -2
  17. package/dist/ir.mjs.map +1 -1
  18. package/dist/migration.d.mts +11 -2
  19. package/dist/migration.d.mts.map +1 -1
  20. package/dist/migration.mjs +7 -0
  21. package/dist/migration.mjs.map +1 -1
  22. package/dist/pack.mjs +1 -1
  23. package/dist/psl-infer.d.mts +101 -0
  24. package/dist/psl-infer.d.mts.map +1 -0
  25. package/dist/psl-infer.mjs +342 -0
  26. package/dist/psl-infer.mjs.map +1 -0
  27. package/dist/schema-differ-DnoopSXm.d.mts +45 -0
  28. package/dist/schema-differ-DnoopSXm.d.mts.map +1 -0
  29. package/dist/schema-verify-W3r631Jh.mjs +226 -0
  30. package/dist/schema-verify-W3r631Jh.mjs.map +1 -0
  31. package/dist/{sql-contract-serializer-BR2vC7Z-.mjs → sql-contract-serializer-C75cfMSS.mjs} +46 -20
  32. package/dist/sql-contract-serializer-C75cfMSS.mjs.map +1 -0
  33. package/dist/{types-kgstZ_Zd.d.mts → types-BW7pzb2j.d.mts} +34 -139
  34. package/dist/types-BW7pzb2j.d.mts.map +1 -0
  35. package/package.json +23 -23
  36. package/src/core/authoring-entity-types.ts +12 -37
  37. package/src/core/control-adapter.ts +10 -5
  38. package/src/core/control-instance.ts +210 -42
  39. package/src/core/control-target-descriptor.ts +98 -0
  40. package/src/core/diff/diff-tree-normalization.ts +211 -0
  41. package/src/core/diff/schema-verify.ts +324 -0
  42. package/src/core/diff/sql-schema-diff.ts +77 -0
  43. package/src/core/diff/verifier-disposition.ts +30 -0
  44. package/src/core/ir/sql-contract-serializer-base.ts +70 -56
  45. package/src/core/ir/sql-contract-serializer.ts +5 -7
  46. package/src/core/ir/sql-schema-verifier-base.ts +5 -5
  47. package/src/core/migrations/contract-to-schema-ir.ts +110 -30
  48. package/src/core/migrations/field-event-planner.ts +2 -2
  49. package/src/core/migrations/native-type-expander.ts +28 -0
  50. package/src/core/migrations/schema-differ.ts +41 -0
  51. package/src/core/migrations/types.ts +31 -34
  52. package/src/core/psl-contract-infer/printer-config.ts +12 -6
  53. package/src/core/psl-contract-infer/relation-inference.ts +9 -1
  54. package/src/core/sql-migration.ts +12 -1
  55. package/src/exports/control.ts +11 -1
  56. package/src/exports/diff.ts +38 -0
  57. package/src/exports/psl-infer.ts +39 -0
  58. package/dist/authoring-type-constructors-CjFfO6LM.mjs.map +0 -1
  59. package/dist/control-adapter-Cmw9LvEP.d.mts.map +0 -1
  60. package/dist/schema-verify.d.mts +0 -39
  61. package/dist/schema-verify.d.mts.map +0 -1
  62. package/dist/schema-verify.mjs +0 -2
  63. package/dist/sql-contract-serializer-BR2vC7Z-.mjs.map +0 -1
  64. package/dist/test-utils.d.mts +0 -2
  65. package/dist/test-utils.mjs +0 -2
  66. package/dist/types-kgstZ_Zd.d.mts.map +0 -1
  67. package/dist/verify-sql-schema-thU-jKpf.d.mts +0 -66
  68. package/dist/verify-sql-schema-thU-jKpf.d.mts.map +0 -1
  69. package/dist/verify-sql-schema-xT4udQLQ.mjs +0 -1501
  70. package/dist/verify-sql-schema-xT4udQLQ.mjs.map +0 -1
  71. package/src/core/psl-contract-infer/postgres-default-mapping.ts +0 -16
  72. package/src/core/psl-contract-infer/postgres-type-map.ts +0 -157
  73. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +0 -795
  74. package/src/core/schema-verify/control-verify-emit.ts +0 -46
  75. package/src/core/schema-verify/verifier-disposition.ts +0 -58
  76. package/src/core/schema-verify/verify-helpers.ts +0 -820
  77. package/src/core/schema-verify/verify-sql-schema.ts +0 -1311
  78. package/src/exports/schema-verify.ts +0 -18
  79. package/src/exports/test-utils.ts +0 -9
@@ -6,18 +6,15 @@ import {
6
6
  type AnyEntityKindDescriptor,
7
7
  hydrateNamespaceEntities,
8
8
  type Namespace,
9
- NamespaceBase,
10
- UNBOUND_NAMESPACE_ID,
11
9
  } from '@prisma-next/framework-components/ir';
12
10
  import { sqlContractCanonicalizationHooks } from '@prisma-next/sql-contract/canonicalization-hooks';
13
11
  import { composeSqlEntityKinds } from '@prisma-next/sql-contract/entity-kinds';
14
12
  import {
15
- buildSqlNamespace,
16
- type SqlNamespaceTablesInput,
13
+ isMaterializedSqlNamespace,
14
+ type SqlNamespaceInput,
17
15
  SqlStorage,
18
16
  type SqlStorageInput,
19
17
  type SqlStorageTypeEntry,
20
- SqlUnboundNamespace,
21
18
  } from '@prisma-next/sql-contract/types';
22
19
  import {
23
20
  createSqlContractSchema,
@@ -25,7 +22,7 @@ import {
25
22
  } from '@prisma-next/sql-contract/validators';
26
23
  import { blindCast } from '@prisma-next/utils/casts';
27
24
  import { ifDefined } from '@prisma-next/utils/defined';
28
- import type { JsonObject } from '@prisma-next/utils/json';
25
+ import type { JsonObject, JsonValue } from '@prisma-next/utils/json';
29
26
  import { type Type, type } from 'arktype';
30
27
 
31
28
  const NamespaceRawSchema = type({
@@ -120,77 +117,48 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
120
117
  'structural',
121
118
  );
122
119
  }
123
- const hydratedNamespaces = this.hydrateSqlNamespaceMap(rawNamespaces);
124
- // Compatibility shim: production code that addresses `__unbound__` for table
125
- // metadata lookups (collection-contract, query-plan-mutations, model-accessor,
126
- // query-plan-meta, where-binding) uses optional chaining and tolerates absence,
127
- // but runtime-qualification (TML-2605) has not yet landed cross-namespace table
128
- // routing. Injecting the empty singleton here keeps helpers that augment the
129
- // deserialized JSON (e.g. buildMixedPolyContract) working by providing a slot to
130
- // write into. Once runtime-qualification routes table lookups by namespace, this
131
- // shim should be removed.
132
- //
133
- // TML-2916: the shim only fires when the target's default namespace IS unbound
134
- // (SQLite, Mongo). On Postgres (`defaultNamespaceId === 'public'`) injecting an
135
- // empty `__unbound__` slot violates ADR 223 — un-namespaced PG models belong in
136
- // `public`, not `__unbound__`.
137
- const withInjectedUnbound =
138
- this.defaultNamespaceId === UNBOUND_NAMESPACE_ID
139
- ? {
140
- ...hydratedNamespaces,
141
- [UNBOUND_NAMESPACE_ID]:
142
- hydratedNamespaces[UNBOUND_NAMESPACE_ID] ?? SqlUnboundNamespace.instance,
143
- }
144
- : hydratedNamespaces;
120
+ const hydratedNamespaces = this.hydrateSqlNamespaceMap(
121
+ blindCast<
122
+ Readonly<Record<string, Record<string, unknown>>>,
123
+ 'parseSqlContractStructure validated raw JSON; namespace entries are plain objects, not SqlNamespace instances.'
124
+ >(rawNamespaces),
125
+ );
145
126
 
146
127
  return {
147
128
  ...validated,
148
129
  storage: new SqlStorage({
149
130
  storageHash: validated.storage.storageHash,
150
131
  ...ifDefined('types', hydratedTypes),
151
- // Cast narrows the result of hydrateSqlNamespaceMap from the wider
152
- // framework `Namespace` to the SQL-family `SqlNamespace`.
153
132
  namespaces: blindCast<
154
133
  SqlStorageInput['namespaces'],
155
- 'hydrateSqlNamespaceMap builds each namespace through the SQL family concretions (SqlBoundNamespace / target schema), so every value is a SqlNamespace; the framework return type only promises the base Namespace.'
156
- >(withInjectedUnbound),
134
+ 'hydrateSqlNamespaceMap builds each namespace through the target serializer override, so every value is a SqlNamespace; the framework return type only promises the base Namespace.'
135
+ >(hydratedNamespaces),
157
136
  }),
158
137
  };
159
138
  }
160
139
 
161
- protected abstract get defaultNamespaceId(): string;
162
-
163
140
  protected hydrateSqlNamespaceMap(
164
- namespaces: Readonly<Record<string, Namespace | Record<string, unknown>>>,
141
+ namespaces: Readonly<Record<string, Record<string, unknown>>>,
165
142
  ): Readonly<Record<string, Namespace>> {
166
143
  return Object.fromEntries(
167
144
  Object.entries(namespaces).map(([nsId, namespaceEntryRaw]) => {
168
- // Raw entries passed structural validation; hydrate materialises family IR class instances.
169
145
  const namespaceHydrated = this.hydrateSqlNamespaceEntry(nsId, namespaceEntryRaw);
170
- const namespaceMaterialised =
171
- namespaceHydrated instanceof NamespaceBase
172
- ? namespaceHydrated
173
- : buildSqlNamespace(
174
- blindCast<
175
- SqlNamespaceTablesInput,
176
- 'hydrateSqlNamespaceEntry returns SqlNamespaceTablesInput when raw is not a NamespaceBase'
177
- >(namespaceHydrated),
178
- );
179
- return [nsId, namespaceMaterialised];
146
+ if (!isMaterializedSqlNamespace(namespaceHydrated)) {
147
+ throw new Error(
148
+ `Target serializer bug: hydrateSqlNamespaceEntry for namespace "${nsId}" returned a non-NamespaceBase value. Override hydrateSqlNamespaceEntry to produce a target namespace concretion.`,
149
+ );
150
+ }
151
+ return [nsId, namespaceHydrated];
180
152
  }),
181
153
  );
182
154
  }
183
155
 
184
156
  protected hydrateSqlNamespaceEntry(
185
157
  nsId: string,
186
- raw: Namespace | Record<string, unknown>,
187
- ): Namespace | SqlNamespaceTablesInput {
188
- if (raw instanceof NamespaceBase) {
189
- return raw;
190
- }
191
- const rawRecord = isPlainRecord(raw) ? raw : {};
192
- const id = typeof rawRecord['id'] === 'string' ? rawRecord['id'] : nsId;
193
- const parsed = NamespaceRawSchema({ ...rawRecord, id });
158
+ raw: Record<string, unknown>,
159
+ ): Namespace | SqlNamespaceInput {
160
+ const id = typeof raw['id'] === 'string' ? raw['id'] : nsId;
161
+ const parsed = NamespaceRawSchema({ ...raw, id });
194
162
  if (parsed instanceof type.errors) {
195
163
  const messages = parsed.map((p: { message: string }) => p.message).join('; ');
196
164
  throw new ContractValidationError(`Namespace hydration failed: ${messages}`, 'structural');
@@ -211,8 +179,8 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
211
179
  }
212
180
 
213
181
  return blindCast<
214
- SqlNamespaceTablesInput,
215
- 'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the SqlNamespaceTablesInput the family createNamespace consumes.'
182
+ SqlNamespaceInput,
183
+ 'entriesOutput holds the hydrated SQL entity-kind maps (table always present); this wraps them as the SqlNamespaceInput the target createNamespace consumes.'
216
184
  >({
217
185
  id,
218
186
  entries: entriesOutput,
@@ -240,4 +208,50 @@ export abstract class SqlContractSerializerBase<TContract extends Contract<SqlSt
240
208
  protected constructTargetContract(hydrated: Contract<SqlStorage>): TContract {
241
209
  return hydrated as TContract;
242
210
  }
211
+
212
+ /**
213
+ * Serializes a namespace's `entries` dict by walking every enumerable
214
+ * kind — no kind is named here, mirroring the generic hydrate walk in
215
+ * `hydrateSqlNamespaceEntry` above. `table` is the SQL family's one
216
+ * universal base kind (every namespace carries it), so it is always
217
+ * emitted, even when empty; every other kind — target- or
218
+ * pack-contributed — is emitted only when it holds at least one entry.
219
+ * A kind carried non-enumerable on `entries` is excluded here for free,
220
+ * since `Object.entries` honors enumerability.
221
+ */
222
+ protected serializeNamespaceEntries(
223
+ entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>,
224
+ ): Record<string, Record<string, JsonObject>> {
225
+ const out: Record<string, Record<string, JsonObject>> = {
226
+ table: this.serializeEntries(entries['table'] ?? {}),
227
+ };
228
+ for (const [kind, record] of Object.entries(entries)) {
229
+ if (kind === 'table' || record == null || Object.keys(record).length === 0) {
230
+ continue;
231
+ }
232
+ out[kind] = this.serializeEntries(record);
233
+ }
234
+ return out;
235
+ }
236
+
237
+ private serializeEntries(entries: Readonly<Record<string, unknown>>): Record<string, JsonObject> {
238
+ const out: Record<string, JsonObject> = {};
239
+ for (const [name, entry] of Object.entries(entries)) {
240
+ out[name] = this.serializeJsonObject(entry);
241
+ }
242
+ return out;
243
+ }
244
+
245
+ protected serializeJsonObject(value: unknown): JsonObject {
246
+ return blindCast<
247
+ JsonObject,
248
+ 'serializeJsonValue round-trips an IR node through JSON, yielding a JsonObject'
249
+ >(this.serializeJsonValue(value));
250
+ }
251
+
252
+ private serializeJsonValue(value: unknown): JsonValue {
253
+ return blindCast<JsonValue, 'JSON.parse(JSON.stringify(x)) yields a JsonValue'>(
254
+ JSON.parse(JSON.stringify(value)),
255
+ );
256
+ }
243
257
  }
@@ -1,5 +1,4 @@
1
1
  import type { Contract } from '@prisma-next/contract/types';
2
- import { UNBOUND_NAMESPACE_ID } from '@prisma-next/framework-components/ir';
3
2
  import type { SqlStorage } from '@prisma-next/sql-contract/types';
4
3
  import { SqlContractSerializerBase } from './sql-contract-serializer-base';
5
4
 
@@ -11,15 +10,14 @@ import { SqlContractSerializerBase } from './sql-contract-serializer-base';
11
10
  * codec-typed envelope wire a target-specific subclass with a populated
12
11
  * registry (see Postgres). Family-level call sites instantiate this
13
12
  * default directly when no target serializer is supplied.
13
+ *
14
+ * Because this serializer has no target concretion, deserialization of
15
+ * contracts that include namespace entries from JSON will throw unless
16
+ * the caller provides pre-hydrated `NamespaceBase` instances. Production
17
+ * paths always supply a target-specific serializer.
14
18
  */
15
19
  export class SqlContractSerializer extends SqlContractSerializerBase<Contract<SqlStorage>> {
16
20
  constructor() {
17
21
  super(new Map());
18
22
  }
19
-
20
- // Family-level fallback when no target descriptor is wired in. Preserves the
21
- // pre-TML-2916 compatibility-shim behaviour for the unbound slot.
22
- protected override get defaultNamespaceId(): string {
23
- return UNBOUND_NAMESPACE_ID;
24
- }
25
23
  }
@@ -1,5 +1,5 @@
1
1
  import type {
2
- SchemaIssue,
2
+ SchemaDiffIssue,
3
3
  SchemaVerifier,
4
4
  SchemaVerifyOptions,
5
5
  SchemaVerifyResult,
@@ -9,7 +9,7 @@ import type {
9
9
  * SQL family `SchemaVerifier` abstract base. Centralises the SQL-shared
10
10
  * walk (table-by-table + column-by-column matching keyed by
11
11
  * `(namespace.id, name)`, FK / unique / index comparisons via the
12
- * existing helpers in `verify-helpers.ts`) and exposes a protected hook
12
+ * shared satisfaction helpers in `sql-schema-diff.ts`) and exposes a protected hook
13
13
  * for target extensions (Postgres functions, RLS policies, future
14
14
  * target-only kinds).
15
15
  *
@@ -29,7 +29,7 @@ export abstract class SqlSchemaVerifierBase<TContract, TSchema>
29
29
  implements SchemaVerifier<TContract, TSchema>
30
30
  {
31
31
  verifySchema(options: SchemaVerifyOptions<TContract, TSchema>): SchemaVerifyResult {
32
- const issues: SchemaIssue[] = [];
32
+ const issues: SchemaDiffIssue[] = [];
33
33
  issues.push(...this.verifyCommonSqlSchema(options));
34
34
  issues.push(...this.verifyTargetExtensions(options));
35
35
  return { ok: issues.length === 0, issues };
@@ -43,7 +43,7 @@ export abstract class SqlSchemaVerifierBase<TContract, TSchema>
43
43
  */
44
44
  protected abstract verifyCommonSqlSchema(
45
45
  options: SchemaVerifyOptions<TContract, TSchema>,
46
- ): readonly SchemaIssue[];
46
+ ): readonly SchemaDiffIssue[];
47
47
 
48
48
  /**
49
49
  * Target-specific extensions — e.g. Postgres functions, future RLS
@@ -52,5 +52,5 @@ export abstract class SqlSchemaVerifierBase<TContract, TSchema>
52
52
  */
53
53
  protected abstract verifyTargetExtensions(
54
54
  options: SchemaVerifyOptions<TContract, TSchema>,
55
- ): readonly SchemaIssue[];
55
+ ): readonly SchemaDiffIssue[];
56
56
  }
@@ -1,4 +1,5 @@
1
1
  import type { ColumnDefault, Contract, JsonValue } from '@prisma-next/contract/types';
2
+ import type { CodecRef } from '@prisma-next/framework-components/codec';
2
3
  import type { MigrationPlannerConflict } from '@prisma-next/framework-components/control';
3
4
  import {
4
5
  type CheckConstraint,
@@ -12,16 +13,17 @@ import {
12
13
  type UniqueConstraint,
13
14
  } from '@prisma-next/sql-contract/types';
14
15
  import { defaultIndexName } from '@prisma-next/sql-schema-ir/naming';
15
- import type {
16
- SqlAnnotations,
17
- SqlCheckConstraintIRInput,
18
- SqlColumnIR,
19
- SqlForeignKeyIR,
20
- SqlIndexIR,
16
+ import {
17
+ type SqlAnnotations,
18
+ type SqlCheckConstraintIRInput,
19
+ type SqlColumnIRInput,
20
+ type SqlForeignKeyIRInput,
21
+ type SqlIndexIRInput,
21
22
  SqlSchemaIR,
22
23
  SqlTableIR,
23
- SqlUniqueIR,
24
+ type SqlUniqueIRInput,
24
25
  } from '@prisma-next/sql-schema-ir/types';
26
+ import { blindCast } from '@prisma-next/utils/casts';
25
27
  import { ifDefined } from '@prisma-next/utils/defined';
26
28
 
27
29
  /**
@@ -72,7 +74,7 @@ function convertColumn(
72
74
  storageTypes: ResolvedStorageTypes,
73
75
  expandNativeType: NativeTypeExpander | undefined,
74
76
  renderDefault: DefaultRenderer | undefined,
75
- ): SqlColumnIR {
77
+ ): SqlColumnIRInput {
76
78
  // Resolve `typeRef` so columns that delegate their `nativeType`/`codecId`/
77
79
  // `typeParams` to a named `storage.types` entry expand the same way as
78
80
  // columns that inline those fields. Without this resolution, a
@@ -82,13 +84,14 @@ function convertColumn(
82
84
  // typeRef and produces `"vector(1536)"` — making diffs on the same
83
85
  // contract falsely report a `type_mismatch`.
84
86
  const resolved = resolveColumnTypeMetadata(column, storageTypes);
85
- const nativeType = expandNativeType
87
+ const baseNativeType = expandNativeType
86
88
  ? expandNativeType({
87
89
  nativeType: resolved.nativeType,
88
90
  codecId: resolved.codecId,
89
91
  ...ifDefined('typeParams', resolved.typeParams),
90
92
  })
91
93
  : resolved.nativeType;
94
+ const nativeType = column.many ? `${baseNativeType}[]` : baseNativeType;
92
95
  return {
93
96
  name,
94
97
  nativeType,
@@ -97,6 +100,43 @@ function convertColumn(
97
100
  'default',
98
101
  column.default != null && renderDefault ? renderDefault(column.default, column) : undefined,
99
102
  ),
103
+ // Contract-derived columns are resolved by construction: the computed
104
+ // full native type doubles as the resolved value, and the contract's
105
+ // structured default is the resolved default (the introspected side
106
+ // stamps its normalizer's parse of the raw expression).
107
+ resolvedNativeType: nativeType,
108
+ ...ifDefined('resolvedDefault', column.default ?? undefined),
109
+ // The column's codec identity, carried the same way the query AST
110
+ // carries `CodecRef` (TML-2456) — the migration planner's op-builders
111
+ // resolve DDL rendering from this at plan time (Decision 5), instead of
112
+ // reading a derivation-precomputed render payload.
113
+ codecRef: buildColumnCodecRef(resolved, column.many),
114
+ codecBaseNativeType: resolved.nativeType,
115
+ ...(column.typeRef !== undefined ? { codecNamedType: true } : {}),
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Builds the column's `CodecRef` from its resolved (post-`typeRef`) codec
121
+ * identity — the same construction the query AST and the migration DDL
122
+ * renderer already use (TML-2456, TML-2918).
123
+ */
124
+ function buildColumnCodecRef(
125
+ resolved: Pick<StorageColumn, 'codecId' | 'nativeType' | 'typeParams'>,
126
+ many: boolean | undefined,
127
+ ): CodecRef {
128
+ return {
129
+ codecId: resolved.codecId,
130
+ ...ifDefined(
131
+ 'typeParams',
132
+ resolved.typeParams !== undefined
133
+ ? blindCast<
134
+ JsonValue,
135
+ 'resolved.typeParams is JsonValue-shaped storage metadata; the narrowed (non-undefined) value lands in CodecRef.typeParams which is JsonValue'
136
+ >(resolved.typeParams)
137
+ : undefined,
138
+ ),
139
+ ...ifDefined('many', many),
100
140
  };
101
141
  }
102
142
 
@@ -192,22 +232,26 @@ function convertCheck(check: CheckConstraint, storage: SqlStorage): SqlCheckCons
192
232
  };
193
233
  }
194
234
 
195
- function convertUnique(unique: UniqueConstraint): SqlUniqueIR {
235
+ function convertUnique(unique: UniqueConstraint): SqlUniqueIRInput {
196
236
  return {
197
237
  columns: unique.columns,
198
238
  ...ifDefined('name', unique.name),
199
239
  };
200
240
  }
201
241
 
202
- function convertIndex(index: Index): SqlIndexIR {
242
+ function convertIndex(index: Index): SqlIndexIRInput {
203
243
  return {
204
244
  columns: index.columns,
205
245
  unique: false,
206
246
  ...ifDefined('name', index.name),
247
+ // Carried so the derived index node compares type/options against the
248
+ // introspected side (the legacy walk read them from the contract).
249
+ ...ifDefined('type', index.type),
250
+ ...ifDefined('options', index.options),
207
251
  };
208
252
  }
209
253
 
210
- function convertForeignKey(fk: ForeignKey): SqlForeignKeyIR {
254
+ function convertForeignKey(fk: ForeignKey): SqlForeignKeyIRInput {
211
255
  return {
212
256
  columns: fk.source.columns,
213
257
  referencedTable: fk.target.tableName,
@@ -227,7 +271,7 @@ function convertTable(
227
271
  renderDefault: DefaultRenderer | undefined,
228
272
  storage: SqlStorage,
229
273
  ): SqlTableIR {
230
- const columns: Record<string, SqlColumnIR> = {};
274
+ const columns: Record<string, SqlColumnIRInput> = {};
231
275
  for (const [colName, colDef] of Object.entries(table.columns)) {
232
276
  columns[colName] = convertColumn(
233
277
  colName,
@@ -243,7 +287,7 @@ function convertTable(
243
287
  ...table.uniques.map((unique) => unique.columns.join(',')),
244
288
  ...(table.primaryKey ? [table.primaryKey.columns.join(',')] : []),
245
289
  ]);
246
- const fkBackingIndexes: SqlIndexIR[] = [];
290
+ const fkBackingIndexes: SqlIndexIRInput[] = [];
247
291
  for (const fk of table.foreignKeys) {
248
292
  if (fk.index === false) continue;
249
293
  const key = fk.source.columns.join(',');
@@ -261,7 +305,7 @@ function convertTable(
261
305
  ? table.checks.map((c) => convertCheck(c, storage))
262
306
  : undefined;
263
307
 
264
- return {
308
+ return new SqlTableIR({
265
309
  name,
266
310
  columns,
267
311
  ...ifDefined('primaryKey', table.primaryKey),
@@ -269,7 +313,7 @@ function convertTable(
269
313
  uniques: table.uniques.map(convertUnique),
270
314
  indexes: [...table.indexes.map(convertIndex), ...fkBackingIndexes],
271
315
  ...ifDefined('checks', checks),
272
- };
316
+ });
273
317
  }
274
318
 
275
319
  /**
@@ -303,7 +347,7 @@ export function detectDestructiveChanges(
303
347
 
304
348
  for (const tableName of Object.keys(fromTables)) {
305
349
  const toTableRaw = toNs?.entries.table?.[tableName];
306
- if (!(toTableRaw instanceof StorageTable)) {
350
+ if (!StorageTable.is(toTableRaw)) {
307
351
  conflicts.push({
308
352
  kind: 'tableRemoved',
309
353
  summary: `Table "${tableName}" was removed`,
@@ -313,7 +357,7 @@ export function detectDestructiveChanges(
313
357
  const toTable = toTableRaw;
314
358
 
315
359
  const fromTableRaw = fromTables[tableName];
316
- if (!(fromTableRaw instanceof StorageTable)) continue;
360
+ if (!StorageTable.is(fromTableRaw)) continue;
317
361
  const fromTable = fromTableRaw;
318
362
 
319
363
  for (const columnName of Object.keys(fromTable.columns)) {
@@ -358,6 +402,48 @@ export interface ContractToSchemaIROptions {
358
402
  *
359
403
  * Returns an empty schema IR when `contract` is `null` (new project).
360
404
  */
405
+ /**
406
+ * Converts the tables of a single namespace into a `SqlSchemaIR`, keyed by
407
+ * table name within that namespace. Unlike {@link contractToSchemaIR}, which
408
+ * flattens every namespace's tables into one bare-keyed record (and throws on a
409
+ * cross-namespace name collision), this scopes the table iteration to one
410
+ * namespace so the same table name can exist in two schemas.
411
+ *
412
+ * The full `storage` is still passed to `convertTable`, so value-set / enum /
413
+ * type resolution that legitimately spans namespaces is unaffected. Foreign
414
+ * keys are built purely from the FK descriptor (`fk.target`), so cross-namespace
415
+ * FKs survive per-namespace conversion. The `annotations` block (storage-type
416
+ * derived) is omitted here — the per-namespace tree consumer reads only the
417
+ * per-table fields.
418
+ */
419
+ export function contractNamespaceToSchemaIR(
420
+ storage: SqlStorage,
421
+ namespaceId: string,
422
+ options: ContractToSchemaIROptions,
423
+ ): SqlSchemaIR {
424
+ if (options.annotationNamespace.length === 0) {
425
+ throw new Error('annotationNamespace must be a non-empty string');
426
+ }
427
+ const namespace = storage.namespaces[namespaceId];
428
+ if (!namespace) {
429
+ return new SqlSchemaIR({ tables: {} });
430
+ }
431
+ const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
432
+ const tables: Record<string, SqlTableIR> = {};
433
+ for (const [tableName, tableDefRaw] of Object.entries(namespace.entries.table ?? {})) {
434
+ StorageTable.assert(tableDefRaw, `namespaces.${namespaceId}.entries.table.${tableName}`);
435
+ tables[tableName] = convertTable(
436
+ tableName,
437
+ tableDefRaw,
438
+ storageTypes,
439
+ options.expandNativeType,
440
+ options.renderDefault,
441
+ storage,
442
+ );
443
+ }
444
+ return new SqlSchemaIR({ tables });
445
+ }
446
+
361
447
  export function contractToSchemaIR(
362
448
  contract: Contract<SqlStorage> | null,
363
449
  options: ContractToSchemaIROptions,
@@ -367,21 +453,15 @@ export function contractToSchemaIR(
367
453
  }
368
454
 
369
455
  if (!contract) {
370
- return { tables: {} };
456
+ return new SqlSchemaIR({ tables: {} });
371
457
  }
372
458
 
373
459
  const storage = contract.storage;
374
- const storageTypes: ResolvedStorageTypes = {
375
- ...((storage.types ?? {}) as ResolvedStorageTypes),
376
- };
460
+ const storageTypes: ResolvedStorageTypes = { ...(storage.types ?? {}) };
377
461
  const tables: Record<string, SqlTableIR> = {};
378
462
  for (const ns of Object.values(storage.namespaces)) {
379
463
  for (const [tableName, tableDefRaw] of Object.entries(ns.entries.table ?? {})) {
380
- if (!(tableDefRaw instanceof StorageTable)) {
381
- throw new Error(
382
- `contractToSchemaIR: expected StorageTable at namespaces.${ns.id}.entries.table.${tableName}`,
383
- );
384
- }
464
+ StorageTable.assert(tableDefRaw, `namespaces.${ns.id}.entries.table.${tableName}`);
385
465
  const tableDef = tableDefRaw;
386
466
  if (tables[tableName] !== undefined) {
387
467
  throw new Error(
@@ -405,10 +485,10 @@ export function contractToSchemaIR(
405
485
  options.resolveEnumNamespaceSchema,
406
486
  );
407
487
 
408
- return {
488
+ return new SqlSchemaIR({
409
489
  tables,
410
490
  ...ifDefined('annotations', annotations),
411
- };
491
+ });
412
492
  }
413
493
 
414
494
  function deriveAnnotations(
@@ -418,7 +498,7 @@ function deriveAnnotations(
418
498
  ): SqlAnnotations | undefined {
419
499
  const storageTypes: Record<string, StorageTypeInstance> = {};
420
500
 
421
- for (const typeInstance of Object.values((storage.types ?? {}) as ResolvedStorageTypes)) {
501
+ for (const typeInstance of Object.values(storage.types ?? {})) {
422
502
  if (isStorageTypeInstance(typeInstance)) {
423
503
  storageTypes[typeInstance.nativeType] = typeInstance;
424
504
  }
@@ -88,8 +88,8 @@ export function planFieldEventOperations(
88
88
  for (const tableName of tableNames) {
89
89
  const priorTableRaw = priorTables?.[tableName];
90
90
  const newTableRaw = newTables?.[tableName];
91
- const priorTable = priorTableRaw instanceof StorageTable ? priorTableRaw : undefined;
92
- const newTable = newTableRaw instanceof StorageTable ? newTableRaw : undefined;
91
+ const priorTable = StorageTable.is(priorTableRaw) ? priorTableRaw : undefined;
92
+ const newTable = StorageTable.is(newTableRaw) ? newTableRaw : undefined;
93
93
  const fieldNames = unionSorted(
94
94
  priorTable ? Object.keys(priorTable.columns) : [],
95
95
  newTable ? Object.keys(newTable.columns) : [],
@@ -0,0 +1,28 @@
1
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
2
+ import { extractCodecControlHooks } from '../assembly';
3
+
4
+ /**
5
+ * Builds the codec-hook-composed `expandNativeType` callback the contract→IR
6
+ * derivation uses to expand parameterized native types (e.g. `character` +
7
+ * `{ length: 36 }` → `character(36)`). Returns `undefined` when no framework
8
+ * components are supplied, so callers can omit the option entirely.
9
+ */
10
+ export function buildNativeTypeExpander(
11
+ frameworkComponents?: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>,
12
+ ) {
13
+ if (!frameworkComponents) {
14
+ return undefined;
15
+ }
16
+ const codecHooks = extractCodecControlHooks(frameworkComponents);
17
+ return (input: {
18
+ readonly nativeType: string;
19
+ readonly codecId?: string;
20
+ readonly typeParams?: Record<string, unknown>;
21
+ }) => {
22
+ if (!input.typeParams) return input.nativeType;
23
+ if (!input.codecId) return input.nativeType;
24
+ const hooks = codecHooks.get(input.codecId);
25
+ if (!hooks?.expandNativeType) return input.nativeType;
26
+ return hooks.expandNativeType(input);
27
+ };
28
+ }
@@ -0,0 +1,41 @@
1
+ import type { Contract, ControlPolicy } from '@prisma-next/contract/types';
2
+ import type { TargetBoundComponentDescriptor } from '@prisma-next/framework-components/components';
3
+ import type { SchemaDiffIssue } from '@prisma-next/framework-components/control';
4
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
5
+ import type { SqlSchemaIRNode } from '@prisma-next/sql-schema-ir/types';
6
+
7
+ /**
8
+ * The full-tree node diff a SQL target produces for the family verify
9
+ * verdict: the target derives the expected tree from the contract, applies
10
+ * the pre-diff normalizations (semantic satisfaction, FK schema-segment
11
+ * resolution), runs the generic differ, and ownership-scopes the result.
12
+ * Strict gating, control-policy disposition, and the verdict itself are the
13
+ * family's post-diff filters over this output.
14
+ */
15
+ export interface SqlSchemaDiffResult {
16
+ /** The full, ownership-scoped diff issue list. */
17
+ readonly issues: readonly SchemaDiffIssue[];
18
+ /**
19
+ * Resolves a diff issue's subject table's declared control policy directly
20
+ * from the contract (Decision 5's own-layer-per-concern discipline extends
21
+ * here too: control policy is a contract concern, resolved by the target
22
+ * at disposition time — never stamped on the diff node). `undefined`
23
+ * when the issue's path resolves to no contract table (a genuine orphan,
24
+ * or a non-table subject).
25
+ */
26
+ readonly resolveControlPolicy: (issue: SchemaDiffIssue) => ControlPolicy | undefined;
27
+ /**
28
+ * The expected/actual namespace-node pairs the codec `verifyType` hooks
29
+ * run over — one per contract namespace with tables, paired by DDL
30
+ * schema; a flat target repeats its sole actual root per such namespace.
31
+ */
32
+ readonly namespacePairs: ReadonlyArray<{ readonly actual: SqlSchemaIRNode | undefined }>;
33
+ }
34
+
35
+ export interface SqlSchemaDiffInput {
36
+ readonly contract: Contract<SqlStorage>;
37
+ readonly schema: SqlSchemaIRNode;
38
+ readonly frameworkComponents: ReadonlyArray<TargetBoundComponentDescriptor<'sql', string>>;
39
+ }
40
+
41
+ export type SqlSchemaDiffFn = (input: SqlSchemaDiffInput) => SqlSchemaDiffResult;