@prisma/orm-family-sql 8.0.0-rc.17-dev.8 → 8.0.0-rc.17-dev.10

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 (47) hide show
  1. package/dist/{attribute-specs-15pm1Qvk.d.mts → attribute-specs-DfOulPzE.d.mts} +3 -3
  2. package/dist/{attribute-specs-15pm1Qvk.d.mts.map → attribute-specs-DfOulPzE.d.mts.map} +1 -1
  3. package/dist/{authoring-entity-types-CZ-vdwYS-BQPeBSla.d.mts → authoring-entity-types-CZ-vdwYS-BtvE94H_.d.mts} +2 -2
  4. package/dist/{authoring-entity-types-CZ-vdwYS-BQPeBSla.d.mts.map → authoring-entity-types-CZ-vdwYS-BtvE94H_.d.mts.map} +1 -1
  5. package/dist/{authoring-type-constructors-0Iv2zdwS-hVVlzpLW.mjs → authoring-type-constructors-0Iv2zdwS-DX59Qw3z.mjs} +2 -2
  6. package/dist/{authoring-type-constructors-0Iv2zdwS-hVVlzpLW.mjs.map → authoring-type-constructors-0Iv2zdwS-DX59Qw3z.mjs.map} +1 -1
  7. package/dist/{config-types-DsRO_uxd.mjs → config-types-HwU-FfRj.mjs} +2 -2
  8. package/dist/{config-types-DsRO_uxd.mjs.map → config-types-HwU-FfRj.mjs.map} +1 -1
  9. package/dist/{contract-builder-DWQhH8FQ.d.mts → contract-builder-DCTszECg.d.mts} +71 -35
  10. package/dist/contract-builder-DCTszECg.d.mts.map +1 -0
  11. package/dist/{contract-builder-BSipKPrc.mjs → contract-builder-Tmmlb0mv.mjs} +2 -2
  12. package/dist/{contract-builder-BSipKPrc.mjs.map → contract-builder-Tmmlb0mv.mjs.map} +1 -1
  13. package/dist/contract-prisma7__provider.mjs +3 -3
  14. package/dist/contract-psl.d.mts +1 -1
  15. package/dist/contract-psl.mjs +1 -1
  16. package/dist/contract-psl__attribute-specs.d.mts +1 -1
  17. package/dist/contract-psl__attribute-specs.mjs +1 -1
  18. package/dist/contract-psl__provider.mjs +2 -2
  19. package/dist/contract-ts.d.mts +2 -2
  20. package/dist/contract-ts.mjs +3 -3
  21. package/dist/contract-ts__config-types.mjs +1 -1
  22. package/dist/contract-ts__contract-builder.d.mts +2 -2
  23. package/dist/contract-ts__contract-builder.mjs +2 -2
  24. package/dist/contract.d.mts +1 -1
  25. package/dist/{control-3kVclzEh.d.mts → control-BQpDsD3I.d.mts} +3 -3
  26. package/dist/{control-3kVclzEh.d.mts.map → control-BQpDsD3I.d.mts.map} +1 -1
  27. package/dist/{control-CXHnIBSV.mjs → control-COI98aRs.mjs} +3 -3
  28. package/dist/{control-CXHnIBSV.mjs.map → control-COI98aRs.mjs.map} +1 -1
  29. package/dist/{derived-checks-CfXjGvNH-CrXoMrPU.mjs → derived-checks-RB_lNgFJ-CHamqgFW.mjs} +924 -593
  30. package/dist/derived-checks-RB_lNgFJ-CHamqgFW.mjs.map +1 -0
  31. package/dist/family.d.mts +2 -2
  32. package/dist/family.mjs +1 -1
  33. package/dist/family__control.d.mts +1 -1
  34. package/dist/family__control.mjs +1 -1
  35. package/dist/family__pack.d.mts +1 -1
  36. package/dist/family__pack.mjs +2 -2
  37. package/dist/{interpreter-BTM9k0wn-DPrWFd6h.mjs → interpreter-BTM9k0wn-Bu5eCC-Z.mjs} +3 -3
  38. package/dist/{interpreter-BTM9k0wn-DPrWFd6h.mjs.map → interpreter-BTM9k0wn-Bu5eCC-Z.mjs.map} +1 -1
  39. package/dist/{pack-g2rKoDhU.d.mts → pack-BIYpJtZF.d.mts} +3 -3
  40. package/dist/{pack-g2rKoDhU.d.mts.map → pack-BIYpJtZF.d.mts.map} +1 -1
  41. package/dist/{psl-column-resolution-Dm5LhN9b-Bz-Ax2iC.d.mts → psl-column-resolution-Dm5LhN9b-Br_RiSPI.d.mts} +2 -2
  42. package/dist/{psl-column-resolution-Dm5LhN9b-Bz-Ax2iC.d.mts.map → psl-column-resolution-Dm5LhN9b-Br_RiSPI.d.mts.map} +1 -1
  43. package/dist/{psl-field-resolution-CAJsUfST-BYuz5Nqh.mjs → psl-field-resolution-CAJsUfST-DWY_oqor.mjs} +2 -2
  44. package/dist/{psl-field-resolution-CAJsUfST-BYuz5Nqh.mjs.map → psl-field-resolution-CAJsUfST-DWY_oqor.mjs.map} +1 -1
  45. package/package.json +18 -18
  46. package/dist/contract-builder-DWQhH8FQ.d.mts.map +0 -1
  47. package/dist/derived-checks-CfXjGvNH-CrXoMrPU.mjs.map +0 -1
@@ -15,6 +15,7 @@ import { t as deriveValueSetFromEntity } from "./value-set-derivation-hook-Di9JZ
15
15
  import { isStructuredError, structuredError } from "@prisma/orm-framework/utils/structured-error";
16
16
  import { ifDefined } from "@prisma/orm-framework/utils/defined";
17
17
  import { flushAuthoringWarnings, isAuthoringEntityTypeDescriptor } from "@prisma/orm-framework/components/authoring";
18
+ import { compareCodeUnits } from "@prisma/orm-framework/contract/hashing-utils";
18
19
  import { UNBOUND_NAMESPACE_ID } from "@prisma/orm-framework/components/ir";
19
20
  import { codecForRef, enumRefusalOf } from "@prisma/orm-framework/components/codec";
20
21
  import { blindCast } from "@prisma/orm-framework/utils/casts";
@@ -26,7 +27,7 @@ import { resolveToOneRelationNullable } from "@prisma/orm-framework/contract-aut
26
27
  import { buildExecutionSection, computeProfileHash, computeStorageHash } from "@prisma/orm-framework/contract/hashing";
27
28
  import { invariant } from "@prisma/orm-framework/utils/assertions";
28
29
  import { applySpecifierDefaultControlPolicy } from "@prisma/orm-framework/contract/apply-specifier-default-control-policy";
29
- //#region ../../../2-sql/2-authoring/contract-ts/dist/derived-checks-CfXjGvNH.mjs
30
+ //#region ../../../2-sql/2-authoring/contract-ts/dist/derived-checks-RB_lNgFJ.mjs
30
31
  function contractError(code, message, options) {
31
32
  return structuredError(code, message, options);
32
33
  }
@@ -40,8 +41,44 @@ function storedAsListColumn(field) {
40
41
  function isValueObjectMember(field) {
41
42
  return "valueObjectName" in field;
42
43
  }
43
- function toOneNullabilityContradictionMessage(location, contradiction) {
44
- return contradiction === "declared-optional" ? `${location} is optional but every local field it joins on is required` : `${location} is required but a local field it joins on is nullable`;
44
+ /** Describes one column, declared by a model's field or by a table node. */
45
+ function describeColumn(column, site) {
46
+ return {
47
+ columnName: column.columnName,
48
+ descriptor: column.descriptor,
49
+ nullable: column.nullable,
50
+ many: column.many === true ? { elementNullable: column.elementNullable === true } : false,
51
+ default: column.default,
52
+ noCheck: column.noCheck,
53
+ enumTypeHandle: column.enumTypeHandle,
54
+ site
55
+ };
56
+ }
57
+ /** How an error message names the column: `Field "Model.field"` or `Column "table.column"`. */
58
+ function columnSiteSubject(site) {
59
+ return site.kind === "field" ? `Field "${site.modelName}.${site.fieldName}"` : `Column "${site.tableName}.${site.columnName}"`;
60
+ }
61
+ /** The error metadata that locates the column. */
62
+ function columnSiteMeta(site) {
63
+ return site.kind === "field" ? {
64
+ modelName: site.modelName,
65
+ fieldName: site.fieldName
66
+ } : {
67
+ namespaceId: site.namespaceId,
68
+ tableName: site.tableName,
69
+ columnName: site.columnName
70
+ };
71
+ }
72
+ /** How an error message names the owner: `model "User"` or `table "audit_rows" in namespace "public"`. */
73
+ function referenceOwnerSubject(owner) {
74
+ return owner.kind === "model" ? `model "${owner.modelName}"` : `table "${owner.tableName}" in namespace "${owner.namespaceId}"`;
75
+ }
76
+ /** The error metadata that locates the owner. */
77
+ function referenceOwnerMeta(owner) {
78
+ return owner.kind === "model" ? { sourceModel: owner.modelName } : {
79
+ sourceTable: owner.tableName,
80
+ sourceNamespaceId: owner.namespaceId
81
+ };
45
82
  }
46
83
  /**
47
84
  * The codec that encodes one column's default, built with the column's own `typeParams`, because a parameterized codec checks its params when it encodes and reads a default. Only a column has params; every other encode site takes the representative instance.
@@ -64,11 +101,10 @@ function encodeViaCodec(value, codec) {
64
101
  function defaultRefusal(site, cause, elementPosition) {
65
102
  const subject = elementPosition === void 0 ? "default" : `default (element ${elementPosition})`;
66
103
  const reason = cause instanceof Error ? cause.message : String(cause);
67
- return contractError("CONTRACT.DEFAULT_INVALID", `Field "${site.modelName}.${site.fieldName}" has a ${subject} that its codec refuses: ${reason}`, {
104
+ return contractError("CONTRACT.DEFAULT_INVALID", `${columnSiteSubject(site)} has a ${subject} that its codec refuses: ${reason}`, {
68
105
  cause,
69
106
  meta: {
70
- modelName: site.modelName,
71
- fieldName: site.fieldName,
107
+ ...columnSiteMeta(site),
72
108
  codecId: site.codecId,
73
109
  reason: "codec-refused-default",
74
110
  ...ifDefined("elementPosition", elementPosition)
@@ -85,9 +121,8 @@ function encodeDefaultValue(value, codec, site, elementPosition) {
85
121
  }
86
122
  function codecForDefault(codecLookup, resolveCodec, site) {
87
123
  const codec = buildCodecForDefault(codecLookup, resolveCodec, site);
88
- if (codec === void 0) throw contractError("CONTRACT.DEFAULT_INVALID", `Field "${site.modelName}.${site.fieldName}" has a default, but no pack in the contract declares its codec "${site.codecId}", so the default cannot be checked. List the pack that owns the codec in \`extensions\`.`, { meta: {
89
- modelName: site.modelName,
90
- fieldName: site.fieldName,
124
+ if (codec === void 0) throw contractError("CONTRACT.DEFAULT_INVALID", `${columnSiteSubject(site)} has a default, but no pack in the contract declares its codec "${site.codecId}", so the default cannot be checked. List the pack that owns the codec in \`extensions\`.`, { meta: {
125
+ ...columnSiteMeta(site),
91
126
  codecId: site.codecId,
92
127
  reason: "codec-not-found"
93
128
  } });
@@ -98,11 +133,10 @@ function buildCodecForDefault(codecLookup, resolveCodec, site) {
98
133
  return resolveCodec(codecLookup);
99
134
  } catch (cause) {
100
135
  if (!isStructuredError(cause) || cause.code !== "RUNTIME.TYPE_PARAMS_INVALID") throw cause;
101
- throw contractError("CONTRACT.ARGUMENT_INVALID", `Field "${site.modelName}.${site.fieldName}" has type parameters that its codec does not accept: ${cause.message}`, {
136
+ throw contractError("CONTRACT.ARGUMENT_INVALID", `${columnSiteSubject(site)} has type parameters that its codec does not accept: ${cause.message}`, {
102
137
  cause,
103
138
  meta: {
104
- modelName: site.modelName,
105
- fieldName: site.fieldName,
139
+ ...columnSiteMeta(site),
106
140
  codecId: site.codecId,
107
141
  reason: "type-params-invalid"
108
142
  }
@@ -119,9 +153,8 @@ function encodeColumnDefault(defaultInput, codecLookup, resolveCodec, site, many
119
153
  value: blindCast(defaultInput.value)
120
154
  };
121
155
  if (many) {
122
- if (!Array.isArray(defaultInput.value)) throw contractError("CONTRACT.DEFAULT_INVALID", `Field "${site.modelName}.${site.fieldName}" is a list field, so its default is an array; received ${typeof defaultInput.value}. Call .many() before .default().`, { meta: {
123
- modelName: site.modelName,
124
- fieldName: site.fieldName,
156
+ if (!Array.isArray(defaultInput.value)) throw contractError("CONTRACT.DEFAULT_INVALID", `${columnSiteSubject(site)} is a list field, so its default is an array; received ${typeof defaultInput.value}. Call .many() before .default().`, { meta: {
157
+ ...columnSiteMeta(site),
125
158
  codecId: site.codecId,
126
159
  reason: "list-default-not-array"
127
160
  } });
@@ -140,76 +173,6 @@ function encodeColumnDefault(defaultInput, codecLookup, resolveCodec, site, many
140
173
  value: encodeDefaultValue(defaultInput.value, codecForDefault(codecLookup, resolveCodec, site), site)
141
174
  };
142
175
  }
143
- function assertStorageSemantics(contract, indexTypeRegistry, codecLookup) {
144
- const semanticErrors = validateStorageSemantics(contract.storage);
145
- if (semanticErrors.length > 0) throw contractError("CONTRACT.VALIDATION_FAILED", `Contract semantic validation failed: ${semanticErrors.join("; ")}`, { meta: { errors: semanticErrors } });
146
- validateIndexTypes(contract, indexTypeRegistry, (codecId) => codecLookup.descriptorFor(codecId)?.traits);
147
- }
148
- function assertKnownTargetModel(modelsByName, modelsByCoordinate, sourceModelName, targetModelName, targetNamespaceId, context) {
149
- const targetModel = targetNamespaceId !== void 0 && targetNamespaceId.length > 0 ? modelsByCoordinate.get(`${targetNamespaceId}:${targetModelName}`) : modelsByName.get(targetModelName);
150
- if (!targetModel) {
151
- const qualified = targetNamespaceId !== void 0 && targetNamespaceId.length > 0 ? `${targetNamespaceId}.${targetModelName}` : targetModelName;
152
- throw contractError("CONTRACT.MODEL_UNKNOWN", `${context} on model "${sourceModelName}" references unknown model "${qualified}"`, { meta: {
153
- sourceModel: sourceModelName,
154
- targetModel: qualified,
155
- context
156
- } });
157
- }
158
- return targetModel;
159
- }
160
- function assertTargetTableMatches(sourceModelName, targetModel, referencedTableName, context) {
161
- if (targetModel.tableName !== referencedTableName) throw contractError("CONTRACT.TABLE_MISMATCH", `${context} on model "${sourceModelName}" references table "${referencedTableName}" but model "${targetModel.modelName}" maps to "${targetModel.tableName}"`, { meta: {
162
- sourceModel: sourceModelName,
163
- referencedTable: referencedTableName,
164
- mappedTable: targetModel.tableName,
165
- context
166
- } });
167
- }
168
- /**
169
- * Resolves a deferred entity-ref column descriptor (e.g. a `pg.enum(handle)`
170
- * column) against the field's now-known owning namespace: attaches the
171
- * storage `valueSet` ref the collected entity's derived value-set is stored
172
- * under. `typeParams.typeName` stays bare here — schema
173
- * qualification (e.g. `auth.aal_level`) is a target concern applied in the
174
- * next step, `qualifyColumnDescriptor`. A descriptor with no `entityRef` (the
175
- * ordinary case) passes through unchanged.
176
- */
177
- function resolveEntityRefDescriptor(descriptor, namespaceId) {
178
- const entityRef = descriptor.entityRef;
179
- if (entityRef === void 0) return descriptor;
180
- return {
181
- ...descriptor,
182
- valueSet: {
183
- plane: "storage",
184
- entityKind: "valueSet",
185
- namespaceId,
186
- entityName: entityRef.entityName
187
- }
188
- };
189
- }
190
- /**
191
- * Structural check for a target that contributes a `qualifyColumnType` hook
192
- * on its authoring contributions. Duck-typed (mirroring
193
- * `contract-psl`'s `hasColumnFromEntityHook`) so the SQL family stays blind
194
- * to the target's qualification logic and no framework/family interface has
195
- * to name the hook.
196
- */
197
- function hasColumnTypeQualifier(authoring) {
198
- return "qualifyColumnType" in authoring && typeof authoring.qualifyColumnType === "function";
199
- }
200
- /**
201
- * Structural check for a target that contributes a `renderCheckExpressions`
202
- * hook, duck-typed for the same reason {@link hasColumnTypeQualifier} is: the
203
- * SQL family stays blind to the target's predicate syntax.
204
- */
205
- function hasCheckExpressionRenderer(authoring) {
206
- return "renderCheckExpressions" in authoring && typeof authoring.renderCheckExpressions === "function";
207
- }
208
- function resolveCheckExpressionRenderer(target) {
209
- const authoring = target.authoring;
210
- if (authoring === void 0) return void 0;
211
- return hasCheckExpressionRenderer(authoring) ? authoring.renderCheckExpressions : void 0;
212
- }
213
176
  /** Whether TypeScript gives the value a literal type: a primitive, or an array or plain object of them. */
214
177
  function hasLiteralType(value) {
215
178
  if ([
@@ -311,149 +274,86 @@ function checkMemberValues(handle, codecLookup) {
311
274
  return values;
312
275
  }
313
276
  /**
314
- * Resolves a field's authored `noCheck` kinds against its column shape:
315
- * the bare form (`[]`) becomes every kind the shape derives, and a named
316
- * kind that can never apply to the shape is an authoring error
317
- * (`CONTRACT.CHECK_OPTOUT_INVALID`). Returns the concrete kinds in
318
- * canonical ascending order — the only form the contract persists.
277
+ * The refs of a field typed by an authored enum: the domain enum and its storage value set. Authored enums are registered in the default namespace, whatever namespace the field's model is in.
319
278
  */
320
- function resolveNoCheckKinds(input) {
321
- const derivable = [];
322
- if (input.many && !input.elementNullable) derivable.push("elementNotNull");
323
- if (input.isDomainEnum) derivable.push("membership");
324
- const subject = `Field "${input.modelName}.${input.fieldName}"`;
325
- const meta = {
326
- modelName: input.modelName,
327
- fieldName: input.fieldName
279
+ function enumValueSetRefs(enumHandle, defaultNamespaceId) {
280
+ if (enumHandle === void 0) return void 0;
281
+ const common = {
282
+ namespaceId: defaultNamespaceId,
283
+ entityName: enumHandle.enumName
328
284
  };
329
- if (input.kinds.length === 0) {
330
- if (derivable.length === 0) throw contractError("CONTRACT.CHECK_OPTOUT_INVALID", `${subject}: noCheck() waives nothing — this column's shape derives no generated checks.`, { meta: {
331
- ...meta,
332
- reason: "no-derivable-checks"
333
- } });
334
- return derivable;
335
- }
336
- const seen = /* @__PURE__ */ new Set();
337
- for (const kind of input.kinds) {
338
- if (seen.has(kind)) throw contractError("CONTRACT.CHECK_OPTOUT_INVALID", `${subject}: noCheck("${kind}") names the same kind twice.`, { meta: {
339
- ...meta,
340
- kind,
341
- reason: "duplicate-kind"
342
- } });
343
- seen.add(kind);
344
- if (!derivable.includes(kind)) throw contractError("CONTRACT.CHECK_OPTOUT_INVALID", `${subject}: noCheck("${kind}") does not apply — ${kind === "membership" ? "membership checks are derived only from enumType() value sets" : "element-non-null checks are derived only for lists whose elements are semantically non-null"}.`, { meta: {
345
- ...meta,
346
- kind,
347
- reason: "inapplicable-kind"
348
- } });
349
- }
350
- return [...input.kinds].sort();
351
- }
352
- /**
353
- * Names the target's rendered checks and lowers them into contract entities.
354
- *
355
- * Naming is composed family-side ({@link composeCheckWirePrefix}) rather than
356
- * by the target, and suffixed with the predicate's content hash. Composing
357
- * family-side is what makes the truncation safe — two prefixes that truncate
358
- * alike still differ in their hashes, and the family can see that the
359
- * (table, column, kind) triple they were built from is unique per table,
360
- * rather than having to assume something about SQL text it declares itself
361
- * unable to read.
362
- */
363
- function lowerRenderedChecks(tableName, candidates) {
364
- return candidates.map((candidate) => new CheckConstraint({
365
- naming: {
366
- kind: "wire",
367
- prefix: composeCheckWirePrefix(tableName, candidate.columnName, candidate.kind),
368
- hash: computeCheckContentHash(candidate.expression)
285
+ return {
286
+ domain: {
287
+ plane: "domain",
288
+ entityKind: "enum",
289
+ ...common
369
290
  },
370
- expression: candidate.expression
371
- }));
372
- }
373
- function resolveColumnTypeQualifier(target) {
374
- const authoring = target.authoring;
375
- if (authoring === void 0) return void 0;
376
- return hasColumnTypeQualifier(authoring) ? authoring.qualifyColumnType : void 0;
291
+ storage: {
292
+ plane: "storage",
293
+ entityKind: "valueSet",
294
+ ...common
295
+ }
296
+ };
377
297
  }
378
- /**
379
- * Applies the target's `qualifyColumnType` hook to a scalar column descriptor.
380
- * A descriptor whose codec the target leaves unchanged passes through untouched.
381
- */
382
- function qualifyColumnDescriptor(descriptor, namespaceId, qualify) {
383
- if (qualify === void 0) return descriptor;
384
- const qualified = qualify({
385
- codecId: descriptor.codecId,
386
- ...ifDefined("typeParams", descriptor.typeParams)
387
- }, namespaceId);
388
- if (qualified.typeParams === descriptor.typeParams) return descriptor;
298
+ function buildDomainField(field, defaultNamespaceId, storageTypes) {
299
+ if (isValueObjectMember(field)) return {
300
+ type: {
301
+ kind: "valueObject",
302
+ name: field.valueObjectName
303
+ },
304
+ nullable: field.nullable,
305
+ many: field.many ? { elementNullable: field.elementNullable === true } : false
306
+ };
389
307
  return {
390
- ...descriptor,
391
- ...ifDefined("typeParams", qualified.typeParams)
308
+ type: {
309
+ kind: "scalar",
310
+ codecId: field.descriptor.codecId,
311
+ ...ifDefined("typeParams", resolvedTypeParams(field.descriptor, storageTypes))
312
+ },
313
+ nullable: field.nullable,
314
+ many: field.many ? { elementNullable: field.elementNullable === true } : false,
315
+ ...ifDefined("valueSet", enumValueSetRefs(field.enumTypeHandle, defaultNamespaceId)?.domain)
392
316
  };
393
317
  }
394
- /**
395
- * Records a deferred column's entity-ref into the namespace-scoped collection
396
- * accumulator (`namespaceId → entityKind → entityName`) — folded into the same
397
- * namespace assembly `deriveEntityValueSets`/`entries.<kind>` step as the
398
- * entities-channel attachments, so a column-collected entity gets its
399
- * value-set the same way an entities-channel one does.
400
- *
401
- * The same handle reused by many columns in one namespace is normal (a native
402
- * enum type backs any number of columns) and records the identical entity once.
403
- * Two *different* entity instances sharing a name+kind in one namespace is a
404
- * name collision — the emitted `entries.valueSet.<name>` could only reflect one
405
- * of them, silently mismatching the other column's type/cast. PSL hard-errors
406
- * on the equivalent (`PSL_DUPLICATE_DECLARATION`); the TS path rejects it too.
407
- */
408
- function collectEntityFromColumn(collected, namespaceId, entityRef) {
409
- const forNs = collected[namespaceId] ?? {};
410
- const forKind = forNs[entityRef.entityKind] ?? {};
411
- const existing = forKind[entityRef.entityName];
412
- if (existing !== void 0 && existing !== entityRef.entity) throw contractError("CONTRACT.NAME_DUPLICATE", `buildSqlContractFromDefinition: two different "${entityRef.entityKind}" entities named "${entityRef.entityName}" in namespace "${namespaceId}" — pack-entity names must be unique per namespace.`, { meta: {
413
- kind: entityRef.entityKind,
414
- name: entityRef.entityName,
415
- namespaceId
416
- } });
417
- forKind[entityRef.entityName] = entityRef.entity;
418
- forNs[entityRef.entityKind] = forKind;
419
- collected[namespaceId] = forNs;
318
+ /** A declared namespace coordinate, where an omitted or empty one means the target's default namespace. */
319
+ function namespaceIdOrDefault(namespaceId, defaultNamespaceId) {
320
+ return namespaceId !== void 0 && namespaceId.length > 0 ? namespaceId : defaultNamespaceId;
420
321
  }
421
- /**
422
- * Merges a namespace's entities-channel attachments (lowered from the
423
- * `entities` handle list, carried on `ContractDefinition.attachedEntities`)
424
- * with the entities collected from that namespace's deferred entity-ref
425
- * columns. A column-collected entity that shadows a *different* attached
426
- * entity of the same kind+name (or vice-versa) is the same name-collision bug
427
- * `collectEntityFromColumn` guards against across columns, so it is rejected
428
- * the same way — by entity identity, so the same handle attached and used by
429
- * a column does not throw.
430
- */
431
- function mergeColumnAndAttachedEntities(namespaceId, attached, columnCollected) {
432
- if (attached === void 0) return columnCollected;
433
- if (columnCollected === void 0) return attached;
434
- const kinds = /* @__PURE__ */ new Set([...Object.keys(attached), ...Object.keys(columnCollected)]);
435
- const result = {};
436
- for (const kind of kinds) {
437
- const attachedForKind = attached[kind];
438
- const columnForKind = columnCollected[kind];
439
- for (const [name, entity] of Object.entries(columnForKind ?? {})) {
440
- const existing = attachedForKind?.[name];
441
- if (existing !== void 0 && existing !== entity) throw contractError("CONTRACT.NAME_DUPLICATE", `buildSqlContractFromDefinition: two different "${kind}" entities named "${name}" in namespace "${namespaceId}" — a column-referenced entity conflicts with an attached one; pack-entity names must be unique per namespace.`, { meta: {
442
- kind,
443
- name,
444
- namespaceId
445
- } });
446
- }
447
- result[kind] = {
448
- ...attachedForKind,
449
- ...columnForKind
450
- };
322
+ function assertKnownTargetModel(modelsByName, modelsByCoordinate, owner, targetModelName, targetNamespaceId, context) {
323
+ const targetModel = targetNamespaceId !== void 0 && targetNamespaceId.length > 0 ? modelsByCoordinate.get(`${targetNamespaceId}:${targetModelName}`) : modelsByName.get(targetModelName);
324
+ if (!targetModel) {
325
+ const qualified = targetNamespaceId !== void 0 && targetNamespaceId.length > 0 ? `${targetNamespaceId}.${targetModelName}` : targetModelName;
326
+ throw contractError("CONTRACT.MODEL_UNKNOWN", `${context} on ${referenceOwnerSubject(owner)} references unknown model "${qualified}"`, { meta: {
327
+ ...referenceOwnerMeta(owner),
328
+ targetModel: qualified,
329
+ context
330
+ } });
451
331
  }
452
- return result;
332
+ return targetModel;
333
+ }
334
+ function assertTargetTableMatches(owner, targetModel, referencedTableName, context) {
335
+ if (targetModel.tableName !== referencedTableName) throw contractError("CONTRACT.TABLE_MISMATCH", `${context} on ${referenceOwnerSubject(owner)} references table "${referencedTableName}" but model "${targetModel.modelName}" maps to "${targetModel.tableName}"`, { meta: {
336
+ ...referenceOwnerMeta(owner),
337
+ referencedTable: referencedTableName,
338
+ mappedTable: targetModel.tableName,
339
+ context
340
+ } });
453
341
  }
454
- function resolveModelNamespaceId(model, modelNameToNamespaceId, defaultNamespaceId) {
455
- if (model.namespaceId !== void 0 && model.namespaceId.length > 0) return model.namespaceId;
456
- return modelNameToNamespaceId.get(model.modelName) ?? defaultNamespaceId;
342
+ function modelNamespaceId(model, defaultNamespaceId) {
343
+ return namespaceIdOrDefault(model.namespaceId, defaultNamespaceId);
344
+ }
345
+ function modelLookupsOf(definition) {
346
+ const defaultNamespaceId = definition.target.defaultNamespaceId;
347
+ const { models } = definition;
348
+ return {
349
+ defaultNamespaceId,
350
+ modelsByName: new Map(models.map((m) => [m.modelName, m])),
351
+ modelsByCoordinate: new Map(models.map((m) => [`${modelNamespaceId(m, defaultNamespaceId)}:${m.modelName}`, m])),
352
+ tableNamespaceByName: new Map(models.map((m) => [m.tableName, modelNamespaceId(m, defaultNamespaceId)]))
353
+ };
354
+ }
355
+ function toOneNullabilityContradictionMessage(location, contradiction) {
356
+ return contradiction === "declared-optional" ? `${location} is optional but every local field it joins on is required` : `${location} is required but a local field it joins on is nullable`;
457
357
  }
458
358
  function toOneRelationNullable(semanticModel, relation) {
459
359
  const location = `Relation "${semanticModel.modelName}.${relation.fieldName}"`;
@@ -500,104 +400,404 @@ function targetColumnsForJunction(targetModel, fieldName) {
500
400
  reason: "no-key-for-junction-target-columns"
501
401
  } });
502
402
  }
503
- function validateColumnTypeParams(dataType, typeParams, site) {
504
- try {
505
- validateSqlTypeParams(dataType, dataTypeParams(dataType, typeParams));
506
- } catch (cause) {
507
- if (!isStructuredError(cause) || cause.code !== "CONTRACT.TYPE_PARAMS_INVALID") throw cause;
508
- throw contractError("CONTRACT.TYPE_PARAMS_INVALID", `Field "${site.modelName}.${site.fieldName}" has type parameters that its data type does not accept: ${cause.message}`, {
509
- cause,
510
- meta: {
511
- ...cause.meta,
512
- modelName: site.modelName,
513
- fieldName: site.fieldName
514
- }
515
- });
403
+ /** Lowers a model's relations. `columnToField` maps the model's columns to the fields that hold them. */
404
+ function lowerRelations(semanticModel, columnToField, lookups) {
405
+ const { defaultNamespaceId } = lookups;
406
+ const modelRelations = {};
407
+ for (const relation of semanticModel.relations ?? []) {
408
+ if (relation.spaceId !== void 0) {
409
+ const targetNamespaceId = relation.namespaceId ?? defaultNamespaceId;
410
+ modelRelations[relation.fieldName] = {
411
+ to: crossRef(relation.toModel, targetNamespaceId, relation.spaceId),
412
+ cardinality: "N:1",
413
+ nullable: toOneRelationNullable(semanticModel, relation),
414
+ on: {
415
+ localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
416
+ targetFields: relation.on.childColumns
417
+ }
418
+ };
419
+ continue;
420
+ }
421
+ const owner = {
422
+ kind: "model",
423
+ modelName: semanticModel.modelName
424
+ };
425
+ const targetModel = assertKnownTargetModel(lookups.modelsByName, lookups.modelsByCoordinate, owner, relation.toModel, relation.toNamespaceId, "Relation");
426
+ invariant(relation.toTable !== void 0, `Relation "${semanticModel.modelName}.${relation.fieldName}" is local but carries no target table; only cross-space relations may leave it unset.`);
427
+ assertTargetTableMatches(owner, targetModel, relation.toTable, "Relation");
428
+ const targetColumnToField = new Map(targetModel.fields.map((f) => [f.columnName, f.fieldName]));
429
+ const to = crossRef(relation.toModel, relation.toNamespaceId !== void 0 && relation.toNamespaceId.length > 0 ? relation.toNamespaceId : modelNamespaceId(targetModel, defaultNamespaceId));
430
+ const on = {
431
+ localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
432
+ targetFields: relation.on.childColumns.map((col) => targetColumnToField.get(col) ?? col)
433
+ };
434
+ if (relation.cardinality === "N:M") {
435
+ if (!relation.through) throw contractError("CONTRACT.RELATION_INVALID", `Relation "${semanticModel.modelName}.${relation.fieldName}" with cardinality "N:M" requires through metadata`, { meta: {
436
+ modelName: semanticModel.modelName,
437
+ relationName: relation.fieldName,
438
+ reason: "many-to-many-missing-through"
439
+ } });
440
+ modelRelations[relation.fieldName] = {
441
+ to,
442
+ cardinality: "N:M",
443
+ on,
444
+ through: buildThroughDescriptor(relation.through, lookups.tableNamespaceByName, targetModel, semanticModel.modelName, relation.fieldName, defaultNamespaceId)
445
+ };
446
+ } else if (relation.cardinality === "1:N") modelRelations[relation.fieldName] = {
447
+ to,
448
+ cardinality: "1:N",
449
+ on
450
+ };
451
+ else modelRelations[relation.fieldName] = {
452
+ to,
453
+ cardinality: relation.cardinality,
454
+ nullable: toOneRelationNullable(semanticModel, relation),
455
+ on
456
+ };
516
457
  }
458
+ return modelRelations;
517
459
  }
518
- function buildStorageColumn(field, enumRefs, modelName, storageTypes, lookups) {
519
- const { codecLookup } = lookups;
520
- const { descriptor } = field;
521
- const codecId = descriptor.codecId;
522
- const isListColumn = storedAsListColumn({
523
- list: field.many === true,
524
- typedByValueObject: isValueObjectMember(field)
525
- });
526
- const noCheck = isValueObjectMember(field) ? void 0 : field.noCheck;
527
- const typeParams = resolvedTypeParams(descriptor, storageTypes);
528
- const dataType = sqlDataTypeOfCodec(codecId, lookups);
529
- validateColumnTypeParams(dataType, typeParams, {
530
- modelName,
531
- fieldName: field.fieldName
532
- });
533
- const encodedDefault = field.default !== void 0 ? encodeColumnDefault(field.default, codecLookup, (lookup) => columnCodec(codecId, typeParams, lookup), {
534
- modelName,
535
- fieldName: field.fieldName,
536
- codecId
537
- }, isListColumn, field.elementNullable === true) : void 0;
538
- invariant(enumRefs === void 0 || descriptor.valueSet === void 0, `Field "${modelName}.${field.fieldName}" is typed by a domain enum and also carries a storage value set from its type constructor.`);
539
- const valueSet = enumRefs?.storage ?? descriptor.valueSet;
460
+ /** The key of a table among all tables of a contract: its namespace and its name. */
461
+ function tableKey(namespaceId, tableName) {
462
+ return JSON.stringify([namespaceId, tableName]);
463
+ }
464
+ /** Resolves a foreign key's target. A cross-space key keeps its foreign space; a local key names either a model of this definition and that model's table, or a table a model or table node declares. */
465
+ function resolveForeignKey(fk, sourceTable, context) {
466
+ const source = {
467
+ namespaceId: asNamespaceId(sourceTable.namespaceId),
468
+ tableName: sourceTable.tableName,
469
+ columns: fk.columns
470
+ };
471
+ const options = {
472
+ ...applyFkDefaults({
473
+ ...ifDefined("constraint", fk.constraint),
474
+ ...ifDefined("index", fk.index)
475
+ }, context.foreignKeyDefaults),
476
+ ...ifDefined("name", fk.name),
477
+ ...ifDefined("onDelete", fk.onDelete),
478
+ ...ifDefined("onUpdate", fk.onUpdate)
479
+ };
480
+ if (fk.references.spaceId !== void 0) return {
481
+ source,
482
+ target: {
483
+ namespaceId: asNamespaceId(fk.references.namespaceId ?? context.defaultNamespaceId),
484
+ tableName: fk.references.table,
485
+ columns: fk.references.columns,
486
+ spaceId: fk.references.spaceId
487
+ },
488
+ ...options
489
+ };
490
+ const { references } = fk;
491
+ if (references.model === void 0) {
492
+ const namespaceId = namespaceIdOrDefault(references.namespaceId, context.defaultNamespaceId);
493
+ if (!context.declaredTables.has(tableKey(namespaceId, references.table))) throw contractError("CONTRACT.TABLE_UNKNOWN", `Foreign key on ${referenceOwnerSubject(sourceTable.owner)} references table "${references.table}" in namespace "${namespaceId}", which no model or table node declares. Declare the table with a model or a table node, or correct the name.`, { meta: {
494
+ ...referenceOwnerMeta(sourceTable.owner),
495
+ referencedTable: references.table,
496
+ namespaceId
497
+ } });
498
+ return {
499
+ source,
500
+ target: {
501
+ namespaceId: asNamespaceId(namespaceId),
502
+ tableName: references.table,
503
+ columns: references.columns
504
+ },
505
+ ...options
506
+ };
507
+ }
508
+ const targetModel = assertKnownTargetModel(context.modelsByName, context.modelsByCoordinate, sourceTable.owner, references.model, fk.references.namespaceId, "Foreign key");
509
+ assertTargetTableMatches(sourceTable.owner, targetModel, fk.references.table, "Foreign key");
540
510
  return {
541
- dataType: dataType.id,
542
- codecId,
543
- nullable: field.nullable,
544
- many: isListColumn ? { elementNullable: field.elementNullable === true } : false,
545
- ...ifDefined("noCheck", noCheck && [...noCheck].sort()),
546
- ...ifDefined("typeParams", descriptor.typeParams),
547
- ...ifDefined("default", encodedDefault),
548
- ...ifDefined("typeRef", descriptor.typeRef),
549
- ...ifDefined("valueSet", valueSet)
511
+ source,
512
+ target: {
513
+ namespaceId: asNamespaceId(namespaceIdOrDefault(fk.references.namespaceId, modelNamespaceId(targetModel, context.defaultNamespaceId))),
514
+ tableName: fk.references.table,
515
+ columns: fk.references.columns
516
+ },
517
+ ...options
550
518
  };
551
519
  }
552
520
  /**
553
- * The refs of a field typed by an authored enum: the domain enum and its storage value set. Authored enums are registered in the default namespace, whatever namespace the field's model is in.
521
+ * Resolves a deferred entity-ref column descriptor (e.g. a `pg.enum(handle)`
522
+ * column) against the field's now-known owning namespace: attaches the
523
+ * storage `valueSet` ref the collected entity's derived value-set is stored
524
+ * under. `typeParams.typeName` stays bare here — schema
525
+ * qualification (e.g. `auth.aal_level`) is a target concern applied in the
526
+ * next step, `qualifyColumnDescriptor`. A descriptor with no `entityRef` (the
527
+ * ordinary case) passes through unchanged.
554
528
  */
555
- function enumValueSetRefs(enumHandle, defaultNamespaceId) {
556
- if (enumHandle === void 0) return void 0;
557
- const common = {
558
- namespaceId: defaultNamespaceId,
559
- entityName: enumHandle.enumName
560
- };
529
+ function resolveEntityRefDescriptor(descriptor, namespaceId) {
530
+ const entityRef = descriptor.entityRef;
531
+ if (entityRef === void 0) return descriptor;
561
532
  return {
562
- domain: {
563
- plane: "domain",
564
- entityKind: "enum",
565
- ...common
566
- },
567
- storage: {
533
+ ...descriptor,
534
+ valueSet: {
568
535
  plane: "storage",
569
536
  entityKind: "valueSet",
570
- ...common
537
+ namespaceId,
538
+ entityName: entityRef.entityName
571
539
  }
572
540
  };
573
541
  }
574
- function buildDomainField(field, defaultNamespaceId, storageTypes) {
575
- if (isValueObjectMember(field)) return {
576
- type: {
577
- kind: "valueObject",
578
- name: field.valueObjectName
579
- },
580
- nullable: field.nullable,
581
- many: field.many ? { elementNullable: field.elementNullable === true } : false
542
+ /**
543
+ * Structural check for a target that contributes a `qualifyColumnType` hook
544
+ * on its authoring contributions. Duck-typed (mirroring
545
+ * `contract-psl`'s `hasColumnFromEntityHook`) so the SQL family stays blind
546
+ * to the target's qualification logic and no framework/family interface has
547
+ * to name the hook.
548
+ */
549
+ function hasColumnTypeQualifier(authoring) {
550
+ return "qualifyColumnType" in authoring && typeof authoring.qualifyColumnType === "function";
551
+ }
552
+ /**
553
+ * Structural check for a target that contributes a `renderCheckExpressions`
554
+ * hook, duck-typed for the same reason {@link hasColumnTypeQualifier} is: the
555
+ * SQL family stays blind to the target's predicate syntax.
556
+ */
557
+ function hasCheckExpressionRenderer(authoring) {
558
+ return "renderCheckExpressions" in authoring && typeof authoring.renderCheckExpressions === "function";
559
+ }
560
+ function resolveCheckExpressionRenderer(target) {
561
+ const authoring = target.authoring;
562
+ if (authoring === void 0) return void 0;
563
+ return hasCheckExpressionRenderer(authoring) ? authoring.renderCheckExpressions : void 0;
564
+ }
565
+ function resolveColumnTypeQualifier(target) {
566
+ const authoring = target.authoring;
567
+ if (authoring === void 0) return void 0;
568
+ return hasColumnTypeQualifier(authoring) ? authoring.qualifyColumnType : void 0;
569
+ }
570
+ /**
571
+ * Applies the target's `qualifyColumnType` hook to a scalar column descriptor.
572
+ * A descriptor whose codec the target leaves unchanged passes through untouched.
573
+ */
574
+ function qualifyColumnDescriptor(descriptor, namespaceId, qualify) {
575
+ if (qualify === void 0) return descriptor;
576
+ const qualified = qualify({
577
+ codecId: descriptor.codecId,
578
+ ...ifDefined("typeParams", descriptor.typeParams)
579
+ }, namespaceId);
580
+ if (qualified.typeParams === descriptor.typeParams) return descriptor;
581
+ return {
582
+ ...descriptor,
583
+ ...ifDefined("typeParams", qualified.typeParams)
582
584
  };
585
+ }
586
+ /**
587
+ * A column's descriptor as its namespace makes it: a deferred entity ref gets its storage value set, and the target's `qualifyColumnType` hook qualifies the type for the namespace.
588
+ */
589
+ function resolveColumnDescriptor(descriptor, namespaceId, qualify) {
590
+ return qualifyColumnDescriptor(resolveEntityRefDescriptor(descriptor, namespaceId), namespaceId, qualify);
591
+ }
592
+ /**
593
+ * Converts one model into the storage it implies and its domain model. Other models are read only to resolve the references this one makes to them.
594
+ */
595
+ function describeModel(model, context) {
596
+ const namespaceId = modelNamespaceId(model, context.defaultNamespaceId);
597
+ const { tableName } = model;
598
+ const fieldToColumn = {};
599
+ const domainFields = {};
600
+ const executionDefaults = [];
601
+ for (const field of model.fields) {
602
+ const executionDefault = executionDefaultOf(model.modelName, field);
603
+ fieldToColumn[field.fieldName] = field.columnName;
604
+ domainFields[field.fieldName] = buildDomainField(isValueObjectMember(field) ? field : {
605
+ ...field,
606
+ descriptor: resolveColumnDescriptor(field.descriptor, namespaceId, context.qualifyColumnType)
607
+ }, context.defaultNamespaceId, context.storageTypes);
608
+ if (executionDefault) executionDefaults.push({
609
+ ref: {
610
+ namespace: namespaceId,
611
+ entry: tableName,
612
+ field: field.columnName
613
+ },
614
+ ...ifDefined("onCreate", executionDefault.onCreate),
615
+ ...ifDefined("onUpdate", executionDefault.onUpdate)
616
+ });
617
+ }
618
+ const storage = describeModelStorage(model, namespaceId, (model.foreignKeys ?? []).map((fk) => resolveForeignKey(fk, {
619
+ namespaceId,
620
+ tableName: model.tableName,
621
+ owner: {
622
+ kind: "model",
623
+ modelName: model.modelName
624
+ }
625
+ }, context)));
626
+ const storageFields = {};
627
+ for (const [fieldName, columnName] of Object.entries(fieldToColumn)) storageFields[fieldName] = { column: columnName };
628
+ const columnToField = new Map(Object.entries(fieldToColumn).map(([field, col]) => [col, field]));
583
629
  return {
584
- type: {
585
- kind: "scalar",
586
- codecId: field.descriptor.codecId,
587
- ...ifDefined("typeParams", resolvedTypeParams(field.descriptor, storageTypes))
588
- },
630
+ storage,
631
+ domain: {
632
+ namespaceId,
633
+ modelName: model.modelName,
634
+ model: {
635
+ storage: {
636
+ table: tableName,
637
+ namespaceId,
638
+ fields: storageFields
639
+ },
640
+ fields: domainFields,
641
+ relations: lowerRelations(model, columnToField, context)
642
+ },
643
+ executionDefaults
644
+ }
645
+ };
646
+ }
647
+ /**
648
+ * Single-table-inheritance variants share their base model's table: the variant's columns must already be on that table, and it has no table of its own. That leaves an authored check nowhere to attach, so it is refused here as a backstop; the PSL surface refuses it earlier, at interpretation, with a diagnostic that names the base model.
649
+ */
650
+ function describeModelStorage(model, namespaceId, foreignKeys) {
651
+ const { tableName } = model;
652
+ const columns = model.fields.map((field) => describeFieldColumn(model.modelName, field));
653
+ if (model.sharesBaseTable) {
654
+ if (model.checks && model.checks.length > 0) throw contractError("CONTRACT.CHECK_ON_STI_VARIANT", `Model "${model.modelName}" declares a check constraint but shares its base model's storage table (single-table inheritance) and has no table of its own to declare it on. Declare the check on the base model instead.`, { meta: {
655
+ tableName,
656
+ modelName: model.modelName
657
+ } });
658
+ return {
659
+ kind: "baseTable",
660
+ modelName: model.modelName,
661
+ namespaceId,
662
+ tableName,
663
+ columns
664
+ };
665
+ }
666
+ return {
667
+ kind: "ownTable",
668
+ modelName: model.modelName,
669
+ table: {
670
+ namespaceId,
671
+ tableName,
672
+ columns,
673
+ control: model.control,
674
+ id: model.id,
675
+ uniques: model.uniques ?? [],
676
+ indexes: model.indexes ?? [],
677
+ checks: model.checks ?? [],
678
+ foreignKeys
679
+ }
680
+ };
681
+ }
682
+ function describeFieldColumn(modelName, field) {
683
+ const site = {
684
+ kind: "field",
685
+ modelName,
686
+ fieldName: field.fieldName
687
+ };
688
+ if (isValueObjectMember(field)) return describeColumn({
689
+ columnName: field.columnName,
690
+ descriptor: field.descriptor,
589
691
  nullable: field.nullable,
590
- many: field.many ? { elementNullable: field.elementNullable === true } : false,
591
- ...ifDefined("valueSet", enumValueSetRefs(field.enumTypeHandle, defaultNamespaceId)?.domain)
692
+ ...ifDefined("default", field.default)
693
+ }, site);
694
+ return describeColumn(field, site);
695
+ }
696
+ /** The phases a generated default fills the field on, refused alongside a default or on an optional field. */
697
+ function executionDefaultOf(modelName, field) {
698
+ const phases = field.executionDefaults?.onCreate || field.executionDefaults?.onUpdate ? field.executionDefaults : void 0;
699
+ if (phases === void 0) return void 0;
700
+ if (field.default !== void 0) throw contractError("CONTRACT.DEFAULT_INVALID", `Field "${modelName}.${field.fieldName}" cannot define both default and executionDefaults.`, { meta: {
701
+ modelName,
702
+ fieldName: field.fieldName,
703
+ reason: "default-and-executionDefaults"
704
+ } });
705
+ if (field.nullable) throw contractError("CONTRACT.DEFAULT_INVALID", `Field "${modelName}.${field.fieldName}" is filled on write by a generated default (a preset such as temporal.createdAt() or an id generator), so it cannot be optional; remove .optional().`, { meta: {
706
+ modelName,
707
+ fieldName: field.fieldName,
708
+ reason: "nullable-with-executionDefaults"
709
+ } });
710
+ return phases;
711
+ }
712
+ /** Describes a table node: its columns, and its foreign keys resolved against the definition's models and tables. */
713
+ function describeTableNode(table, context) {
714
+ const namespaceId = namespaceIdOrDefault(table.namespaceId, context.defaultNamespaceId);
715
+ const { tableName } = table;
716
+ const owner = {
717
+ kind: "tableNode",
718
+ namespaceId,
719
+ tableName
720
+ };
721
+ return {
722
+ namespaceId,
723
+ tableName,
724
+ columns: table.columns.map((column) => describeColumn(column, {
725
+ kind: "tableNode",
726
+ namespaceId,
727
+ tableName,
728
+ columnName: column.columnName
729
+ })),
730
+ id: table.id,
731
+ uniques: table.uniques ?? [],
732
+ indexes: table.indexes ?? [],
733
+ checks: table.checks ?? [],
734
+ foreignKeys: (table.foreignKeys ?? []).map((fk) => resolveForeignKey(fk, {
735
+ namespaceId,
736
+ tableName,
737
+ owner
738
+ }, context)),
739
+ control: table.control
592
740
  };
593
741
  }
594
- function collectStorageNamespaceCoordinateIds(definition) {
595
- const ids = /* @__PURE__ */ new Set();
596
- ids.add(definition.target.defaultNamespaceId);
597
- for (const id of definition.namespaces ?? []) if (id.length > 0) ids.add(id);
598
- for (const model of definition.models) if (model.namespaceId !== void 0 && model.namespaceId.length > 0) ids.add(model.namespaceId);
599
- for (const id of Object.keys(definition.attachedEntities ?? {})) if (id.length > 0) ids.add(id);
600
- return ids;
742
+ /**
743
+ * Records a deferred column's entity-ref into the namespace-scoped collection
744
+ * accumulator (`namespaceId → entityKind → entityName`) — folded into the same
745
+ * namespace assembly `deriveEntityValueSets`/`entries.<kind>` step as the
746
+ * entities-channel attachments, so a column-collected entity gets its
747
+ * value-set the same way an entities-channel one does.
748
+ *
749
+ * The same handle reused by many columns in one namespace is normal (a native
750
+ * enum type backs any number of columns) and records the identical entity once.
751
+ * Two *different* entity instances sharing a name+kind in one namespace is a
752
+ * name collision — the emitted `entries.valueSet.<name>` could only reflect one
753
+ * of them, silently mismatching the other column's type/cast. PSL hard-errors
754
+ * on the equivalent (`PSL_DUPLICATE_DECLARATION`); the TS path rejects it too.
755
+ */
756
+ function collectEntityFromColumn(collected, namespaceId, entityRef) {
757
+ const forNs = collected[namespaceId] ?? {};
758
+ const forKind = forNs[entityRef.entityKind] ?? {};
759
+ const existing = forKind[entityRef.entityName];
760
+ if (existing !== void 0 && existing !== entityRef.entity) throw contractError("CONTRACT.NAME_DUPLICATE", `buildSqlContractFromDefinition: two different "${entityRef.entityKind}" entities named "${entityRef.entityName}" in namespace "${namespaceId}" — pack-entity names must be unique per namespace.`, { meta: {
761
+ kind: entityRef.entityKind,
762
+ name: entityRef.entityName,
763
+ namespaceId
764
+ } });
765
+ forKind[entityRef.entityName] = entityRef.entity;
766
+ forNs[entityRef.entityKind] = forKind;
767
+ collected[namespaceId] = forNs;
768
+ }
769
+ /**
770
+ * Merges a namespace's entities-channel attachments (lowered from the
771
+ * `entities` handle list, carried on `ContractDefinition.attachedEntities`)
772
+ * with the entities collected from that namespace's deferred entity-ref
773
+ * columns. A column-collected entity that shadows a *different* attached
774
+ * entity of the same kind+name (or vice-versa) is the same name-collision bug
775
+ * `collectEntityFromColumn` guards against across columns, so it is rejected
776
+ * the same way — by entity identity, so the same handle attached and used by
777
+ * a column does not throw.
778
+ */
779
+ function mergeColumnAndAttachedEntities(namespaceId, attached, columnCollected) {
780
+ if (attached === void 0) return columnCollected;
781
+ if (columnCollected === void 0) return attached;
782
+ const kinds = /* @__PURE__ */ new Set([...Object.keys(attached), ...Object.keys(columnCollected)]);
783
+ const result = {};
784
+ for (const kind of kinds) {
785
+ const attachedForKind = attached[kind];
786
+ const columnForKind = columnCollected[kind];
787
+ for (const [name, entity] of Object.entries(columnForKind ?? {})) {
788
+ const existing = attachedForKind?.[name];
789
+ if (existing !== void 0 && existing !== entity) throw contractError("CONTRACT.NAME_DUPLICATE", `buildSqlContractFromDefinition: two different "${kind}" entities named "${name}" in namespace "${namespaceId}" — a column-referenced entity conflicts with an attached one; pack-entity names must be unique per namespace.`, { meta: {
790
+ kind,
791
+ name,
792
+ namespaceId
793
+ } });
794
+ }
795
+ result[kind] = {
796
+ ...attachedForKind,
797
+ ...columnForKind
798
+ };
799
+ }
800
+ return result;
601
801
  }
602
802
  /**
603
803
  * Entry kinds the framework assembler itself manages (`table` from models,
@@ -681,15 +881,136 @@ function mergeNamespaceValueSets(namespaceId, enumValueSets, packValueSets) {
681
881
  ...packValueSets
682
882
  };
683
883
  }
684
- function ensureUnboundNamespaceSlot(namespaces, createNamespace) {
685
- if (Object.hasOwn(namespaces, UNBOUND_NAMESPACE_ID)) return namespaces;
686
- const unbound = createNamespace({
687
- id: UNBOUND_NAMESPACE_ID,
688
- entries: { table: {} }
689
- });
884
+ /**
885
+ * Resolves a field's authored `noCheck` kinds against its column shape:
886
+ * the bare form (`[]`) becomes every kind the shape derives, and a named
887
+ * kind that can never apply to the shape is an authoring error
888
+ * (`CONTRACT.CHECK_OPTOUT_INVALID`). Returns the concrete kinds in
889
+ * canonical ascending order — the only form the contract persists.
890
+ */
891
+ function resolveNoCheckKinds(input) {
892
+ const derivable = [];
893
+ if (input.many && !input.elementNullable) derivable.push("elementNotNull");
894
+ if (input.isDomainEnum) derivable.push("membership");
895
+ const subject = columnSiteSubject(input.site);
896
+ const meta = columnSiteMeta(input.site);
897
+ if (input.kinds.length === 0) {
898
+ if (derivable.length === 0) throw contractError("CONTRACT.CHECK_OPTOUT_INVALID", `${subject}: noCheck() waives nothing — this column's shape derives no generated checks.`, { meta: {
899
+ ...meta,
900
+ reason: "no-derivable-checks"
901
+ } });
902
+ return derivable;
903
+ }
904
+ const seen = /* @__PURE__ */ new Set();
905
+ for (const kind of input.kinds) {
906
+ if (seen.has(kind)) throw contractError("CONTRACT.CHECK_OPTOUT_INVALID", `${subject}: noCheck("${kind}") names the same kind twice.`, { meta: {
907
+ ...meta,
908
+ kind,
909
+ reason: "duplicate-kind"
910
+ } });
911
+ seen.add(kind);
912
+ if (!derivable.includes(kind)) throw contractError("CONTRACT.CHECK_OPTOUT_INVALID", `${subject}: noCheck("${kind}") does not apply — ${kind === "membership" ? "membership checks are derived only from enumType() value sets" : "element-non-null checks are derived only for lists whose elements are semantically non-null"}.`, { meta: {
913
+ ...meta,
914
+ kind,
915
+ reason: "inapplicable-kind"
916
+ } });
917
+ }
918
+ return [...input.kinds].sort();
919
+ }
920
+ /**
921
+ * Names the target's rendered checks and lowers them into contract entities.
922
+ *
923
+ * Naming is composed family-side ({@link composeCheckWirePrefix}) rather than
924
+ * by the target, and suffixed with the predicate's content hash. Composing
925
+ * family-side is what makes the truncation safe — two prefixes that truncate
926
+ * alike still differ in their hashes, and the family can see that the
927
+ * (table, column, kind) triple they were built from is unique per table,
928
+ * rather than having to assume something about SQL text it declares itself
929
+ * unable to read.
930
+ */
931
+ function lowerRenderedChecks(tableName, candidates) {
932
+ return candidates.map((candidate) => new CheckConstraint({
933
+ naming: {
934
+ kind: "wire",
935
+ prefix: composeCheckWirePrefix(tableName, candidate.columnName, candidate.kind),
936
+ hash: computeCheckContentHash(candidate.expression)
937
+ },
938
+ expression: candidate.expression
939
+ }));
940
+ }
941
+ function validateColumnTypeParams(dataType, typeParams, site) {
942
+ try {
943
+ validateSqlTypeParams(dataType, dataTypeParams(dataType, typeParams));
944
+ } catch (cause) {
945
+ if (!isStructuredError(cause) || cause.code !== "CONTRACT.TYPE_PARAMS_INVALID") throw cause;
946
+ throw contractError("CONTRACT.TYPE_PARAMS_INVALID", `${columnSiteSubject(site)} has type parameters that its data type does not accept: ${cause.message}`, {
947
+ cause,
948
+ meta: {
949
+ ...cause.meta,
950
+ ...columnSiteMeta(site)
951
+ }
952
+ });
953
+ }
954
+ }
955
+ /**
956
+ * Lowers one column into its storage column and the enforcement checks its shape derives.
957
+ *
958
+ * A column whose type constructor refers to a pack entity (e.g. `pg.enum(handle)`) records that entity, and its descriptor is resolved against the column's namespace and handed to the target's `qualifyColumnType` hook. An opt-out from derived checks on a table that derives none is dropped, never persisted: the policy may also be stamped after the build by a specifier.
959
+ *
960
+ * Member values reach the check renderer only for a column typed by an `enumType()` handle: that column is a plain scalar with no native type of its own to enforce membership. A value set resolved by a type constructor binds the column to a native type that already enforces it.
961
+ */
962
+ function lowerColumn(description, placement, context) {
963
+ const { site, many } = description;
964
+ const { codecLookup } = context.lookups;
965
+ const entityRef = description.descriptor.entityRef;
966
+ if (entityRef !== void 0) collectEntityFromColumn(context.collectedColumnEntities, placement.namespaceId, entityRef);
967
+ const descriptor = resolveColumnDescriptor(description.descriptor, placement.namespaceId, context.qualifyColumnType);
968
+ const elementNullable = many !== false && many.elementNullable;
969
+ const noCheck = description.noCheck !== void 0 && placement.derivesChecks ? resolveNoCheckKinds({
970
+ site,
971
+ kinds: description.noCheck,
972
+ many: many !== false,
973
+ elementNullable,
974
+ isDomainEnum: description.enumTypeHandle !== void 0
975
+ }) : void 0;
976
+ const codecId = descriptor.codecId;
977
+ const typeParams = resolvedTypeParams(descriptor, context.storageTypes);
978
+ const dataType = sqlDataTypeOfCodec(codecId, context.lookups);
979
+ validateColumnTypeParams(dataType, typeParams, site);
980
+ const encodedDefault = description.default !== void 0 ? encodeColumnDefault(description.default, codecLookup, (lookup) => columnCodec(codecId, typeParams, lookup), {
981
+ ...site,
982
+ codecId
983
+ }, many !== false, elementNullable) : void 0;
984
+ const enumRefs = enumValueSetRefs(description.enumTypeHandle, context.defaultNamespaceId);
985
+ invariant(enumRefs === void 0 || descriptor.valueSet === void 0, `${columnSiteSubject(site)} is typed by a domain enum and also carries a storage value set from its type constructor.`);
986
+ const valueSet = enumRefs?.storage ?? descriptor.valueSet;
987
+ const column = {
988
+ dataType: dataType.id,
989
+ codecId,
990
+ nullable: description.nullable,
991
+ many,
992
+ ...ifDefined("noCheck", noCheck && [...noCheck].sort()),
993
+ ...ifDefined("typeParams", descriptor.typeParams),
994
+ ...ifDefined("default", encodedDefault),
995
+ ...ifDefined("typeRef", descriptor.typeRef),
996
+ ...ifDefined("valueSet", valueSet)
997
+ };
998
+ const render = context.renderCheckExpressions;
999
+ if (render === void 0 || !placement.derivesChecks) return {
1000
+ column,
1001
+ derivedChecks: []
1002
+ };
1003
+ const { enumTypeHandle } = description;
1004
+ const candidates = render({
1005
+ tableName: placement.tableName,
1006
+ columnName: description.columnName,
1007
+ many: many !== false,
1008
+ elementNullable,
1009
+ memberValues: enumTypeHandle !== void 0 ? checkMemberValues(enumTypeHandle, codecLookup) : void 0
1010
+ }).filter((candidate) => !(noCheck?.includes(candidate.kind) ?? false));
690
1011
  return {
691
- [UNBOUND_NAMESPACE_ID]: unbound,
692
- ...namespaces
1012
+ column,
1013
+ derivedChecks: lowerRenderedChecks(placement.tableName, candidates)
693
1014
  };
694
1015
  }
695
1016
  const DERIVABLE_CHECK_KINDS = ["membership", "elementNotNull"];
@@ -703,6 +1024,249 @@ const DERIVABLE_CHECK_KINDS = ["membership", "elementNotNull"];
703
1024
  function columnsProducingCheckPrefix(tableName, columnNames, prefix) {
704
1025
  return columnNames.filter((columnName) => DERIVABLE_CHECK_KINDS.some((kind) => composeCheckWirePrefix(tableName, columnName, kind) === prefix));
705
1026
  }
1027
+ /**
1028
+ * Lowers one table: each of its columns, then its indexes, checks, primary key, uniques and foreign keys, with the backing indexes its foreign keys need.
1029
+ *
1030
+ * Checks are derived only for tables Prisma 8 owns: the contract describes an external schema, it does not prescribe enforcement for it. This reads the policy the source declares; a policy applied by a contract specifier lands after the build and is handled by `stripDerivedChecksFromNonManagedTables`. Authored checks are kept whatever the policy: they are the author's statement about a constraint they know exists.
1031
+ */
1032
+ function lowerTable(table, context) {
1033
+ const { tableName } = table;
1034
+ const derivesChecks = effectiveControlPolicy(table.control, context.defaultControlPolicy) === "managed";
1035
+ const placement = {
1036
+ namespaceId: table.namespaceId,
1037
+ tableName,
1038
+ derivesChecks
1039
+ };
1040
+ const columns = {};
1041
+ const checks = [];
1042
+ for (const description of table.columns) {
1043
+ const lowered = lowerColumn(description, placement, context);
1044
+ columns[description.columnName] = lowered.column;
1045
+ checks.push(...lowered.derivedChecks);
1046
+ }
1047
+ const uniques = table.uniques.map((u) => ({
1048
+ columns: u.columns,
1049
+ ...ifDefined("name", u.name)
1050
+ }));
1051
+ const declaredIndexes = table.indexes.map((index) => ({
1052
+ namedByUser: index.name !== void 0 || index.map !== void 0,
1053
+ index: lowerAuthoredIndex(tableName, authoredIndexInput(index), context.warnings, context.indexTypeRegistry)
1054
+ }));
1055
+ checks.push(...lowerAuthoredChecks(tableName, Object.keys(columns), table.checks, context));
1056
+ const primaryKey = table.id ? {
1057
+ columns: table.id.columns,
1058
+ ...ifDefined("name", table.id.name)
1059
+ } : void 0;
1060
+ const { foreignKeys, indexes } = materializeForeignKeysAndIndexes({
1061
+ tableName,
1062
+ foreignKeys: table.foreignKeys,
1063
+ declaredIndexes,
1064
+ uniques,
1065
+ primaryKey,
1066
+ warnings: context.warnings,
1067
+ indexTypes: context.indexTypeRegistry
1068
+ });
1069
+ return {
1070
+ columns,
1071
+ ...ifDefined("control", table.control),
1072
+ uniques,
1073
+ indexes: indexes.map((i) => new Index(i)),
1074
+ foreignKeys,
1075
+ ...primaryKey ? { primaryKey } : {},
1076
+ ...checks.length > 0 ? { checks: inCanonicalOrder(checks) } : {}
1077
+ };
1078
+ }
1079
+ /**
1080
+ * The checks sorted by name in UTF-16 code-unit order, the order contract canonicalization gives them. The table's storage is then the same whichever declaration each column came from and in whatever order the columns were listed.
1081
+ */
1082
+ function inCanonicalOrder(checks) {
1083
+ return [...checks].sort((a, b) => compareCodeUnits(a.name, b.name));
1084
+ }
1085
+ function authoredIndexInput(index) {
1086
+ return blindCast({
1087
+ ...ifDefined("columns", index.columns),
1088
+ ...ifDefined("expression", index.expression),
1089
+ where: index.where,
1090
+ unique: index.unique,
1091
+ map: index.map,
1092
+ name: index.name,
1093
+ type: index.type,
1094
+ options: index.options
1095
+ });
1096
+ }
1097
+ /** Names the authored checks, and refuses one whose name could be taken for a derived check of a column of this table. */
1098
+ function lowerAuthoredChecks(tableName, tableColumnNames, authoredChecks, context) {
1099
+ if (authoredChecks.length === 0) return [];
1100
+ const reservedCheckPrefixes = derivedCheckPrefixes(tableName, tableColumnNames);
1101
+ return authoredChecks.map((authoredCheck) => {
1102
+ const lowered = lowerAuthoredCheck(tableName, authoredCheck, context.warnings);
1103
+ if (lowered.naming.kind === "wire" && reservedCheckPrefixes.has(lowered.naming.prefix)) {
1104
+ const collidingColumns = columnsProducingCheckPrefix(tableName, tableColumnNames, lowered.naming.prefix);
1105
+ const columnList = collidingColumns.map((name) => `"${name}"`).join(", ");
1106
+ throw contractError("CONTRACT.CHECK_NAME_RESERVED", `Check "${lowered.naming.prefix}" on table "${tableName}": this name's prefix matches the shape a derived enforcement check would use for column${collidingColumns.length === 1 ? "" : "s"} ${columnList} of this table, so it can't be told apart from one. Choose a different name.`, { meta: {
1107
+ tableName,
1108
+ prefix: lowered.naming.prefix,
1109
+ collidingColumns
1110
+ } });
1111
+ }
1112
+ return new CheckConstraint(lowered);
1113
+ });
1114
+ }
1115
+ /** How a refusal names each table-level property. Typed by every key of {@link TableProperties}, so a new property cannot be left out of the check. */
1116
+ const tablePropertyLabels = {
1117
+ id: "a primary key",
1118
+ uniques: "unique constraints",
1119
+ indexes: "indexes",
1120
+ checks: "check constraints",
1121
+ foreignKeys: "foreign keys",
1122
+ control: "a control policy"
1123
+ };
1124
+ function isTableProperty(key) {
1125
+ return Object.hasOwn(tablePropertyLabels, key);
1126
+ }
1127
+ /**
1128
+ * Collects the tables the models and table nodes describe, one per namespace and table name: the models' tables in model order, then the tables only table nodes declare.
1129
+ *
1130
+ * A table node for a table a model maps adds its columns to that table and nothing else, because the model owns the table's table-level properties. A table node for any other table is the whole table. A single-table variant adds nothing: each of its columns must already be on its base's table, which the base owns, and no table node may declare a column a variant's field maps. A variant whose table no model owns is not checked.
1131
+ *
1132
+ * Every table comes out with its foreign keys from exactly one declaration, a model or a table node, in the order that declaration lists them.
1133
+ */
1134
+ function mergeTables(storages, tableNodes) {
1135
+ const tables = /* @__PURE__ */ new Map();
1136
+ const modelOfTable = /* @__PURE__ */ new Map();
1137
+ for (const storage of storages) {
1138
+ if (storage.kind !== "ownTable") continue;
1139
+ const { table } = storage;
1140
+ const key = tableKey(table.namespaceId, table.tableName);
1141
+ if (tables.has(key)) throw contractError("CONTRACT.NAME_DUPLICATE", `buildSqlContractFromDefinition: duplicate table "${table.tableName}" in namespace "${table.namespaceId}".`, { meta: {
1142
+ kind: "table",
1143
+ name: table.tableName,
1144
+ namespaceId: table.namespaceId
1145
+ } });
1146
+ tables.set(key, table);
1147
+ modelOfTable.set(key, storage.modelName);
1148
+ }
1149
+ const variants = storages.filter((storage) => storage.kind === "baseTable");
1150
+ const variantColumnSites = variantColumnSitesByTable(variants);
1151
+ const tableNodeKeys = /* @__PURE__ */ new Set();
1152
+ for (const tableNode of tableNodes) {
1153
+ const key = tableKey(tableNode.namespaceId, tableNode.tableName);
1154
+ if (tableNodeKeys.has(key)) throw contractError("CONTRACT.NAME_DUPLICATE", `Table "${tableNode.tableName}" in namespace "${tableNode.namespaceId}" is declared by two table nodes.`, { meta: {
1155
+ kind: "table",
1156
+ name: tableNode.tableName,
1157
+ namespaceId: tableNode.namespaceId
1158
+ } });
1159
+ tableNodeKeys.add(key);
1160
+ const columnSites = variantColumnSites.get(key) ?? /* @__PURE__ */ new Map();
1161
+ const modelName = modelOfTable.get(key);
1162
+ const modelled = tables.get(key);
1163
+ if (modelName === void 0 || modelled === void 0) {
1164
+ tables.set(key, {
1165
+ ...tableNode,
1166
+ columns: withColumns([], tableNode, columnSites)
1167
+ });
1168
+ continue;
1169
+ }
1170
+ assertOnlyColumns(tableNode, modelName);
1171
+ tables.set(key, {
1172
+ ...modelled,
1173
+ columns: withColumns(modelled.columns, tableNode, columnSites)
1174
+ });
1175
+ }
1176
+ for (const variant of variants) {
1177
+ const table = tables.get(tableKey(variant.namespaceId, variant.tableName));
1178
+ if (table !== void 0) assertVariantColumnsOnBaseTable(variant, table);
1179
+ }
1180
+ return [...tables.values()];
1181
+ }
1182
+ /** For each table, the variant field that maps each variant column; the first variant to map a column names it. */
1183
+ function variantColumnSitesByTable(variants) {
1184
+ const sites = /* @__PURE__ */ new Map();
1185
+ for (const variant of variants) {
1186
+ const key = tableKey(variant.namespaceId, variant.tableName);
1187
+ const byColumn = sites.get(key) ?? /* @__PURE__ */ new Map();
1188
+ for (const column of variant.columns) if (!byColumn.has(column.columnName)) byColumn.set(column.columnName, column.site);
1189
+ sites.set(key, byColumn);
1190
+ }
1191
+ return sites;
1192
+ }
1193
+ /**
1194
+ * The table's columns with the table node's columns added. A column some field already declares is refused, and the error names the field the author wrote: a single-table variant's field when a variant maps the column, even though the base table carries a copy of it.
1195
+ */
1196
+ function withColumns(columns, tableNode, variantColumnSites) {
1197
+ const sites = new Map(columns.map((column) => [column.columnName, column.site]));
1198
+ for (const column of tableNode.columns) {
1199
+ const earlier = variantColumnSites.get(column.columnName) ?? sites.get(column.columnName);
1200
+ if (earlier !== void 0) {
1201
+ const problem = earlier.kind === "field" ? `is declared by field "${earlier.modelName}.${earlier.fieldName}" and again by a table node` : "is listed twice by its table node";
1202
+ throw contractError("CONTRACT.NAME_DUPLICATE", `Column "${column.columnName}" of table "${tableNode.tableName}" ${problem}.`, { meta: {
1203
+ kind: "column",
1204
+ name: column.columnName,
1205
+ tableName: tableNode.tableName,
1206
+ namespaceId: tableNode.namespaceId
1207
+ } });
1208
+ }
1209
+ sites.set(column.columnName, column.site);
1210
+ }
1211
+ return [...columns, ...tableNode.columns];
1212
+ }
1213
+ function isStated(value) {
1214
+ return Array.isArray(value) ? value.length > 0 : value !== void 0;
1215
+ }
1216
+ function assertOnlyColumns(tableNode, modelName) {
1217
+ const property = Object.keys(tablePropertyLabels).filter(isTableProperty).find((name) => isStated(tableNode[name]));
1218
+ if (property === void 0) return;
1219
+ throw contractError("CONTRACT.TABLE_OWNED_BY_MODEL", `A table node for table "${tableNode.tableName}" states ${tablePropertyLabels[property]}, but model "${modelName}" maps that table and owns its table-level properties. A table node for a modelled table may only add columns.`, { meta: {
1220
+ namespaceId: tableNode.namespaceId,
1221
+ tableName: tableNode.tableName,
1222
+ modelName,
1223
+ property
1224
+ } });
1225
+ }
1226
+ function assertVariantColumnsOnBaseTable(variant, table) {
1227
+ const present = new Set(table.columns.map((column) => column.columnName));
1228
+ const missing = variant.columns.find((column) => !present.has(column.columnName));
1229
+ if (missing === void 0) return;
1230
+ throw contractError("CONTRACT.VARIANT_COLUMN_NOT_ON_BASE_TABLE", `Model "${variant.modelName}" shares table "${variant.tableName}" with its base model (single-table inheritance), but its column "${missing.columnName}" is not on that table. The base model owns the table; declare the column there.`, { meta: {
1231
+ modelName: variant.modelName,
1232
+ namespaceId: variant.namespaceId,
1233
+ tableName: variant.tableName,
1234
+ columnName: missing.columnName
1235
+ } });
1236
+ }
1237
+ function assertStorageSemantics(contract, indexTypeRegistry, codecLookup) {
1238
+ const semanticErrors = validateStorageSemantics(contract.storage);
1239
+ if (semanticErrors.length > 0) throw contractError("CONTRACT.VALIDATION_FAILED", `Contract semantic validation failed: ${semanticErrors.join("; ")}`, { meta: { errors: semanticErrors } });
1240
+ validateIndexTypes(contract, indexTypeRegistry, (codecId) => codecLookup.descriptorFor(codecId)?.traits);
1241
+ }
1242
+ function collectStorageNamespaceCoordinateIds(definition) {
1243
+ const defaultNamespaceId = definition.target.defaultNamespaceId;
1244
+ const declared = [
1245
+ ...definition.namespaces ?? [],
1246
+ ...definition.models.map((model) => model.namespaceId),
1247
+ ...(definition.tables ?? []).map((table) => table.namespaceId),
1248
+ ...Object.keys(definition.attachedEntities ?? {})
1249
+ ];
1250
+ return /* @__PURE__ */ new Set([defaultNamespaceId, ...declared.map((id) => namespaceIdOrDefault(id, defaultNamespaceId))]);
1251
+ }
1252
+ function ensureUnboundNamespaceSlot(namespaces, createNamespace) {
1253
+ if (Object.hasOwn(namespaces, UNBOUND_NAMESPACE_ID)) return namespaces;
1254
+ const unbound = createNamespace({
1255
+ id: UNBOUND_NAMESPACE_ID,
1256
+ entries: { table: {} }
1257
+ });
1258
+ return {
1259
+ [UNBOUND_NAMESPACE_ID]: unbound,
1260
+ ...namespaces
1261
+ };
1262
+ }
1263
+ function declaredTableKeys(definition) {
1264
+ const defaultNamespaceId = definition.target.defaultNamespaceId;
1265
+ return /* @__PURE__ */ new Set([...definition.models.map((model) => tableKey(modelNamespaceId(model, defaultNamespaceId), model.tableName)), ...(definition.tables ?? []).map((table) => tableKey(namespaceIdOrDefault(table.namespaceId, defaultNamespaceId), table.tableName))]);
1266
+ }
1267
+ /**
1268
+ * Builds the contract in two stages. First each model is converted on its own into the table it describes and its domain model, and each table node into the table it describes. Then the tables are merged, each is lowered, and the contract is assembled.
1269
+ */
706
1270
  function buildSqlContractFromDefinition(definition, codecLookup, dataTypeLookup) {
707
1271
  const lookups = {
708
1272
  codecLookup,
@@ -712,296 +1276,45 @@ function buildSqlContractFromDefinition(definition, codecLookup, dataTypeLookup)
712
1276
  const defaultNamespaceId = definition.target.defaultNamespaceId;
713
1277
  const indexTypeRegistry = indexTypeRegistryOf(definition.target, Object.values(definition.extensions ?? {}));
714
1278
  const qualifyColumnType = resolveColumnTypeQualifier(definition.target);
715
- const renderCheckExpressions = resolveCheckExpressionRenderer(definition.target);
1279
+ const storageTypes = definition.storageTypes ?? {};
716
1280
  const targetFamily = "sql";
717
- const resolveNamespaceId = (m) => m.namespaceId !== void 0 && m.namespaceId.length > 0 ? m.namespaceId : defaultNamespaceId;
718
- const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));
719
- const tableNamespaceByName = new Map(definition.models.map((m) => [m.tableName, m.namespaceId !== void 0 && m.namespaceId.length > 0 ? m.namespaceId : defaultNamespaceId]));
720
- const modelsByCoordinate = new Map(definition.models.map((m) => [`${resolveNamespaceId(m)}:${m.modelName}`, m]));
721
- const tablesByNamespace = {};
1281
+ const modelDescriptionContext = {
1282
+ ...modelLookupsOf(definition),
1283
+ declaredTables: declaredTableKeys(definition),
1284
+ foreignKeyDefaults: definition.foreignKeyDefaults,
1285
+ storageTypes,
1286
+ qualifyColumnType
1287
+ };
1288
+ const components = definition.models.map((model) => describeModel(model, modelDescriptionContext));
1289
+ const tableNodes = (definition.tables ?? []).map((table) => describeTableNode(table, modelDescriptionContext));
722
1290
  const authoringWarnings = [...definition.warnings ?? []];
723
- const modelNameToNamespaceId = /* @__PURE__ */ new Map();
724
- const executionDefaults = [];
725
- const modelsByNamespace = {};
726
1291
  const collectedColumnEntities = {};
727
- const rootEntries = [];
728
- for (const semanticModel of definition.models) {
729
- const tableName = semanticModel.tableName;
730
- const namespaceId = semanticModel.namespaceId !== void 0 && semanticModel.namespaceId.length > 0 ? semanticModel.namespaceId : defaultNamespaceId;
731
- modelNameToNamespaceId.set(semanticModel.modelName, namespaceId);
732
- if (!semanticModel.sharesBaseTable) rootEntries.push({
733
- tableName,
734
- namespaceId,
735
- ref: crossRef(semanticModel.modelName, namespaceId)
736
- });
737
- const columns = {};
738
- const fieldToColumn = {};
739
- const domainFields = {};
740
- const checksForTable = [];
741
- const derivesChecks = effectiveControlPolicy(semanticModel.control, definition.defaultControlPolicy) === "managed";
742
- for (const field of semanticModel.fields) {
743
- const executionDefaultPhases = field.executionDefaults?.onCreate || field.executionDefaults?.onUpdate ? field.executionDefaults : void 0;
744
- if (executionDefaultPhases) {
745
- if (field.default !== void 0) throw contractError("CONTRACT.DEFAULT_INVALID", `Field "${semanticModel.modelName}.${field.fieldName}" cannot define both default and executionDefaults.`, { meta: {
746
- modelName: semanticModel.modelName,
747
- fieldName: field.fieldName,
748
- reason: "default-and-executionDefaults"
749
- } });
750
- if (field.nullable) throw contractError("CONTRACT.DEFAULT_INVALID", `Field "${semanticModel.modelName}.${field.fieldName}" is filled on write by a generated default (a preset such as temporal.createdAt() or an id generator), so it cannot be optional; remove .optional().`, { meta: {
751
- modelName: semanticModel.modelName,
752
- fieldName: field.fieldName,
753
- reason: "nullable-with-executionDefaults"
754
- } });
755
- }
756
- const enumHandle = !isValueObjectMember(field) ? field.enumTypeHandle : void 0;
757
- let resolvedField = field;
758
- if (!isValueObjectMember(field)) {
759
- let descriptor = field.descriptor;
760
- const entityRef = descriptor.entityRef;
761
- if (entityRef !== void 0) {
762
- collectEntityFromColumn(collectedColumnEntities, namespaceId, entityRef);
763
- descriptor = resolveEntityRefDescriptor(descriptor, namespaceId);
764
- }
765
- descriptor = qualifyColumnDescriptor(descriptor, namespaceId, qualifyColumnType);
766
- if (descriptor !== field.descriptor) resolvedField = {
767
- ...field,
768
- descriptor
769
- };
770
- }
771
- if (!isValueObjectMember(resolvedField) && resolvedField.noCheck !== void 0) {
772
- const { noCheck: authoredNoCheck, ...withoutNoCheck } = resolvedField;
773
- resolvedField = derivesChecks ? {
774
- ...withoutNoCheck,
775
- noCheck: resolveNoCheckKinds({
776
- modelName: semanticModel.modelName,
777
- fieldName: field.fieldName,
778
- kinds: authoredNoCheck,
779
- many: resolvedField.many === true,
780
- elementNullable: resolvedField.elementNullable === true,
781
- isDomainEnum: enumHandle !== void 0
782
- })
783
- } : withoutNoCheck;
784
- }
785
- const column = buildStorageColumn(resolvedField, enumValueSetRefs(enumHandle, defaultNamespaceId), semanticModel.modelName, definition.storageTypes ?? {}, lookups);
786
- const columnMany = column.many ?? false;
787
- columns[field.columnName] = column;
788
- fieldToColumn[field.fieldName] = field.columnName;
789
- if (renderCheckExpressions !== void 0 && derivesChecks) {
790
- const waivedKinds = !isValueObjectMember(resolvedField) ? resolvedField.noCheck : void 0;
791
- checksForTable.push(...lowerRenderedChecks(tableName, renderCheckExpressions({
792
- tableName,
793
- columnName: field.columnName,
794
- many: columnMany !== false,
795
- elementNullable: columnMany !== false && columnMany.elementNullable,
796
- memberValues: enumHandle !== void 0 ? checkMemberValues(enumHandle, codecLookup) : void 0
797
- }).filter((candidate) => !(waivedKinds?.includes(candidate.kind) ?? false))));
798
- }
799
- domainFields[field.fieldName] = buildDomainField(resolvedField, defaultNamespaceId, definition.storageTypes ?? {});
800
- if (executionDefaultPhases) executionDefaults.push({
801
- ref: {
802
- namespace: namespaceId,
803
- entry: tableName,
804
- field: field.columnName
805
- },
806
- ...ifDefined("onCreate", executionDefaultPhases.onCreate),
807
- ...ifDefined("onUpdate", executionDefaultPhases.onUpdate)
808
- });
809
- }
810
- const authoringForeignKeys = (semanticModel.foreignKeys ?? []).map((fk) => {
811
- if (fk.references.spaceId !== void 0) {
812
- const targetNamespaceId = fk.references.namespaceId ?? defaultNamespaceId;
813
- return {
814
- source: {
815
- namespaceId: asNamespaceId(namespaceId),
816
- tableName,
817
- columns: fk.columns
818
- },
819
- target: {
820
- namespaceId: asNamespaceId(targetNamespaceId),
821
- tableName: fk.references.table,
822
- columns: fk.references.columns,
823
- spaceId: fk.references.spaceId
824
- },
825
- ...applyFkDefaults({
826
- ...ifDefined("constraint", fk.constraint),
827
- ...ifDefined("index", fk.index)
828
- }, definition.foreignKeyDefaults),
829
- ...ifDefined("name", fk.name),
830
- ...ifDefined("onDelete", fk.onDelete),
831
- ...ifDefined("onUpdate", fk.onUpdate)
832
- };
833
- }
834
- const targetModel = assertKnownTargetModel(modelsByName, modelsByCoordinate, semanticModel.modelName, fk.references.model, fk.references.namespaceId, "Foreign key");
835
- assertTargetTableMatches(semanticModel.modelName, targetModel, fk.references.table, "Foreign key");
836
- const targetNamespaceId = fk.references.namespaceId ?? (targetModel.namespaceId !== void 0 && targetModel.namespaceId.length > 0 ? targetModel.namespaceId : defaultNamespaceId);
837
- return {
838
- source: {
839
- namespaceId: asNamespaceId(namespaceId),
840
- tableName,
841
- columns: fk.columns
842
- },
843
- target: {
844
- namespaceId: asNamespaceId(targetNamespaceId),
845
- tableName: fk.references.table,
846
- columns: fk.references.columns
847
- },
848
- ...applyFkDefaults({
849
- ...ifDefined("constraint", fk.constraint),
850
- ...ifDefined("index", fk.index)
851
- }, definition.foreignKeyDefaults),
852
- ...ifDefined("name", fk.name),
853
- ...ifDefined("onDelete", fk.onDelete),
854
- ...ifDefined("onUpdate", fk.onUpdate)
855
- };
856
- });
857
- if (semanticModel.sharesBaseTable && semanticModel.checks && semanticModel.checks.length > 0) throw contractError("CONTRACT.CHECK_ON_STI_VARIANT", `Model "${semanticModel.modelName}" declares a check constraint but shares its base model's storage table (single-table inheritance) and has no table of its own to declare it on. Declare the check on the base model instead.`, { meta: {
858
- tableName,
859
- modelName: semanticModel.modelName
860
- } });
861
- if (!semanticModel.sharesBaseTable) {
862
- const uniques = (semanticModel.uniques ?? []).map((u) => ({
863
- columns: u.columns,
864
- ...ifDefined("name", u.name)
865
- }));
866
- const declaredIndexes = (semanticModel.indexes ?? []).map((i) => ({
867
- namedByUser: i.name !== void 0 || i.map !== void 0,
868
- index: lowerAuthoredIndex(tableName, blindCast({
869
- ...ifDefined("columns", i.columns),
870
- ...ifDefined("expression", i.expression),
871
- where: i.where,
872
- unique: i.unique,
873
- map: i.map,
874
- name: i.name,
875
- type: i.type,
876
- options: i.options
877
- }), authoringWarnings, indexTypeRegistry)
878
- }));
879
- if (semanticModel.checks !== void 0 && semanticModel.checks.length > 0) {
880
- const tableColumnNames = Object.keys(columns);
881
- const reservedCheckPrefixes = derivedCheckPrefixes(tableName, tableColumnNames);
882
- for (const authoredCheck of semanticModel.checks) {
883
- const lowered = lowerAuthoredCheck(tableName, authoredCheck, authoringWarnings);
884
- if (lowered.naming.kind === "wire" && reservedCheckPrefixes.has(lowered.naming.prefix)) {
885
- const collidingColumns = columnsProducingCheckPrefix(tableName, tableColumnNames, lowered.naming.prefix);
886
- const columnList = collidingColumns.map((name) => `"${name}"`).join(", ");
887
- throw contractError("CONTRACT.CHECK_NAME_RESERVED", `Check "${lowered.naming.prefix}" on table "${tableName}": this name's prefix matches the shape a derived enforcement check would use for column${collidingColumns.length === 1 ? "" : "s"} ${columnList} of this table, so it can't be told apart from one. Choose a different name.`, { meta: {
888
- tableName,
889
- prefix: lowered.naming.prefix,
890
- collidingColumns
891
- } });
892
- }
893
- checksForTable.push(new CheckConstraint(lowered));
894
- }
895
- }
896
- const primaryKey = semanticModel.id ? {
897
- columns: semanticModel.id.columns,
898
- ...ifDefined("name", semanticModel.id.name)
899
- } : void 0;
900
- const { foreignKeys, indexes } = materializeForeignKeysAndIndexes({
901
- tableName,
902
- foreignKeys: authoringForeignKeys,
903
- declaredIndexes,
904
- uniques,
905
- primaryKey,
906
- warnings: authoringWarnings,
907
- indexTypes: indexTypeRegistry
908
- });
909
- const tableInput = {
910
- columns,
911
- ...ifDefined("control", semanticModel.control),
912
- uniques,
913
- indexes: indexes.map((i) => new Index(i)),
914
- foreignKeys,
915
- ...primaryKey ? { primaryKey } : {},
916
- ...checksForTable.length > 0 ? { checks: checksForTable } : {}
917
- };
918
- let nsTables = tablesByNamespace[namespaceId];
919
- if (nsTables === void 0) {
920
- nsTables = {};
921
- tablesByNamespace[namespaceId] = nsTables;
922
- }
923
- if (nsTables[tableName] !== void 0) throw contractError("CONTRACT.NAME_DUPLICATE", `buildSqlContractFromDefinition: duplicate table "${tableName}" in namespace "${namespaceId}".`, { meta: {
924
- kind: "table",
925
- name: tableName,
926
- namespaceId
927
- } });
928
- nsTables[tableName] = tableInput;
929
- }
930
- const storageFields = {};
931
- for (const [fieldName, columnName] of Object.entries(fieldToColumn)) storageFields[fieldName] = { column: columnName };
932
- const columnToField = new Map(Object.entries(fieldToColumn).map(([field, col]) => [col, field]));
933
- const modelRelations = {};
934
- for (const relation of semanticModel.relations ?? []) {
935
- if (relation.spaceId !== void 0) {
936
- const targetNamespaceId = relation.namespaceId ?? defaultNamespaceId;
937
- modelRelations[relation.fieldName] = {
938
- to: crossRef(relation.toModel, targetNamespaceId, relation.spaceId),
939
- cardinality: "N:1",
940
- nullable: toOneRelationNullable(semanticModel, relation),
941
- on: {
942
- localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
943
- targetFields: relation.on.childColumns
944
- }
945
- };
946
- continue;
947
- }
948
- const targetModel = assertKnownTargetModel(modelsByName, modelsByCoordinate, semanticModel.modelName, relation.toModel, relation.toNamespaceId, "Relation");
949
- invariant(relation.toTable !== void 0, `Relation "${semanticModel.modelName}.${relation.fieldName}" is local but carries no target table; only cross-space relations may leave it unset.`);
950
- assertTargetTableMatches(semanticModel.modelName, targetModel, relation.toTable, "Relation");
951
- const targetColumnToField = new Map(targetModel.fields.map((f) => [f.columnName, f.fieldName]));
952
- const to = crossRef(relation.toModel, relation.toNamespaceId !== void 0 && relation.toNamespaceId.length > 0 ? relation.toNamespaceId : resolveModelNamespaceId(targetModel, modelNameToNamespaceId, defaultNamespaceId));
953
- const on = {
954
- localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
955
- targetFields: relation.on.childColumns.map((col) => targetColumnToField.get(col) ?? col)
956
- };
957
- if (relation.cardinality === "N:M") {
958
- if (!relation.through) throw contractError("CONTRACT.RELATION_INVALID", `Relation "${semanticModel.modelName}.${relation.fieldName}" with cardinality "N:M" requires through metadata`, { meta: {
959
- modelName: semanticModel.modelName,
960
- relationName: relation.fieldName,
961
- reason: "many-to-many-missing-through"
962
- } });
963
- modelRelations[relation.fieldName] = {
964
- to,
965
- cardinality: "N:M",
966
- on,
967
- through: buildThroughDescriptor(relation.through, tableNamespaceByName, targetModel, semanticModel.modelName, relation.fieldName, defaultNamespaceId)
968
- };
969
- } else if (relation.cardinality === "1:N") modelRelations[relation.fieldName] = {
970
- to,
971
- cardinality: "1:N",
972
- on
973
- };
974
- else modelRelations[relation.fieldName] = {
975
- to,
976
- cardinality: relation.cardinality,
977
- nullable: toOneRelationNullable(semanticModel, relation),
978
- on
979
- };
980
- }
981
- let namespaceModels = modelsByNamespace[namespaceId];
982
- if (namespaceModels === void 0) {
983
- namespaceModels = {};
984
- modelsByNamespace[namespaceId] = namespaceModels;
985
- }
986
- namespaceModels[semanticModel.modelName] = {
987
- storage: {
988
- table: tableName,
989
- namespaceId,
990
- fields: storageFields
991
- },
992
- fields: domainFields,
993
- relations: modelRelations
994
- };
1292
+ const tableLoweringContext = {
1293
+ lookups,
1294
+ storageTypes,
1295
+ defaultNamespaceId,
1296
+ qualifyColumnType,
1297
+ renderCheckExpressions: resolveCheckExpressionRenderer(definition.target),
1298
+ collectedColumnEntities,
1299
+ defaultControlPolicy: definition.defaultControlPolicy,
1300
+ indexTypeRegistry,
1301
+ warnings: authoringWarnings
1302
+ };
1303
+ const tablesByNamespace = {};
1304
+ for (const table of mergeTables(components.map((c) => c.storage), tableNodes)) {
1305
+ const namespaceTables = tablesByNamespace[table.namespaceId] ?? {};
1306
+ namespaceTables[table.tableName] = lowerTable(table, tableLoweringContext);
1307
+ tablesByNamespace[table.namespaceId] = namespaceTables;
995
1308
  }
996
- const rootTableNameCounts = /* @__PURE__ */ new Map();
997
- for (const entry of rootEntries) rootTableNameCounts.set(entry.tableName, (rootTableNameCounts.get(entry.tableName) ?? 0) + 1);
998
- const roots = {};
999
- for (const entry of rootEntries) {
1000
- const key = (rootTableNameCounts.get(entry.tableName) ?? 0) > 1 ? `${entry.namespaceId}.${entry.tableName}` : entry.tableName;
1001
- roots[key] = entry.ref;
1309
+ const executionDefaults = components.flatMap((c) => c.domain.executionDefaults);
1310
+ const modelsByNamespace = {};
1311
+ for (const { domain } of components) {
1312
+ const namespaceModels = modelsByNamespace[domain.namespaceId] ?? {};
1313
+ namespaceModels[domain.modelName] = domain.model;
1314
+ modelsByNamespace[domain.namespaceId] = namespaceModels;
1002
1315
  }
1003
- const rawStorageTypes = definition.storageTypes ?? {};
1004
- const documentTypes = Object.fromEntries(Object.entries(rawStorageTypes).map(([name, entry]) => [name, toStorageTypeInstance({
1316
+ const roots = buildRoots(components);
1317
+ const documentTypes = Object.fromEntries(Object.entries(storageTypes).map(([name, entry]) => [name, toStorageTypeInstance({
1005
1318
  codecId: entry.codecId,
1006
1319
  dataType: sqlDataTypeOfCodec(entry.codecId, lookups).id,
1007
1320
  typeParams: entry.typeParams
@@ -1084,7 +1397,7 @@ function buildSqlContractFromDefinition(definition, codecLookup, dataTypeLookup)
1084
1397
  targetFamily,
1085
1398
  defaults: executionDefaults
1086
1399
  });
1087
- const valueObjects = definition.valueObjects && definition.valueObjects.length > 0 ? Object.fromEntries(definition.valueObjects.map((vo) => [vo.name, { fields: Object.fromEntries(vo.fields.map((f) => [f.fieldName, buildDomainField(f, defaultNamespaceId, definition.storageTypes ?? {})])) }])) : void 0;
1400
+ const valueObjects = definition.valueObjects && definition.valueObjects.length > 0 ? Object.fromEntries(definition.valueObjects.map((vo) => [vo.name, { fields: Object.fromEntries(vo.fields.map((f) => [f.fieldName, buildDomainField(f, defaultNamespaceId, storageTypes)])) }])) : void 0;
1088
1401
  const domainNamespaceIds = new Set(Object.keys(modelsByNamespace));
1089
1402
  if (domainNamespaceIds.size === 0) domainNamespaceIds.add(defaultNamespaceId);
1090
1403
  if (valueObjects !== void 0) domainNamespaceIds.add(defaultNamespaceId);
@@ -1116,6 +1429,24 @@ function buildSqlContractFromDefinition(definition, codecLookup, dataTypeLookup)
1116
1429
  return contract;
1117
1430
  }
1118
1431
  /**
1432
+ * Aggregate roots, one per model that owns a table, keyed by bare storage table name. When two models in different namespaces map to the same bare table name, the bare key would collide, so those entries fall back to a namespace-qualified key. Single-namespace contracts never collide and keep their bare keys.
1433
+ */
1434
+ function buildRoots(components) {
1435
+ const rootEntries = components.flatMap(({ storage, domain }) => storage.kind === "ownTable" ? [{
1436
+ tableName: storage.table.tableName,
1437
+ namespaceId: domain.namespaceId,
1438
+ ref: crossRef(domain.modelName, domain.namespaceId)
1439
+ }] : []);
1440
+ const rootTableNameCounts = /* @__PURE__ */ new Map();
1441
+ for (const entry of rootEntries) rootTableNameCounts.set(entry.tableName, (rootTableNameCounts.get(entry.tableName) ?? 0) + 1);
1442
+ const roots = {};
1443
+ for (const entry of rootEntries) {
1444
+ const key = (rootTableNameCounts.get(entry.tableName) ?? 0) > 1 ? `${entry.namespaceId}.${entry.tableName}` : entry.tableName;
1445
+ roots[key] = entry.ref;
1446
+ }
1447
+ return roots;
1448
+ }
1449
+ /**
1119
1450
  * The one step every SQL contract specifier funnels its loaded contract
1120
1451
  * through: stamp the specifier's default control policy, then strip derived
1121
1452
  * checks from every table the stamped policy leaves non-managed. Composing the
@@ -1250,4 +1581,4 @@ function withoutDerivedChecks(tableName, table, contractDefault) {
1250
1581
  //#endregion
1251
1582
  export { storedAsListColumn as a, isValueObjectMember as i, buildSqlContractFromDefinition as n, toOneNullabilityContradictionMessage as o, contractError as r, applySqlSpecifierControlPolicy as t };
1252
1583
 
1253
- //# sourceMappingURL=derived-checks-CfXjGvNH-CrXoMrPU.mjs.map
1584
+ //# sourceMappingURL=derived-checks-RB_lNgFJ-CHamqgFW.mjs.map