@prisma-next/sql-contract-psl 0.13.0 → 0.14.0-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.
@@ -1,13 +1,15 @@
1
1
  import { crossRef } from "@prisma-next/contract/types";
2
- import { hasRegisteredFieldNamespace, instantiateAuthoringEntityType, instantiateAuthoringFieldPreset, instantiateAuthoringTypeConstructor, isAuthoringEntityTypeDescriptor, isAuthoringFieldPresetDescriptor, isAuthoringTypeConstructorDescriptor, validateAuthoringHelperArguments } from "@prisma-next/framework-components/authoring";
3
- import { isPostgresEnumStorageEntry } from "@prisma-next/sql-contract/types";
2
+ import { hasRegisteredFieldNamespace, instantiateAuthoringEntityType, instantiateAuthoringFieldPreset, instantiateAuthoringTypeConstructor, isAuthoringEntityTypeDescriptor, isAuthoringFieldPresetDescriptor, isAuthoringPslBlockDescriptor, isAuthoringTypeConstructorDescriptor, validateAuthoringHelperArguments } from "@prisma-next/framework-components/authoring";
3
+ import { keywordPslSpan, nodePslSpan, parseQuotedStringLiteral } from "@prisma-next/psl-parser";
4
4
  import { buildSqlContractFromDefinition } from "@prisma-next/sql-contract-ts/contract-builder";
5
+ import { assertDefined, invariant } from "@prisma-next/utils/assertions";
5
6
  import { blindCast } from "@prisma-next/utils/casts";
6
7
  import { ifDefined } from "@prisma-next/utils/defined";
7
8
  import { notOk, ok } from "@prisma-next/utils/result";
8
- import { getPositionalArgument, parseQuotedStringLiteral } from "@prisma-next/psl-parser";
9
- import { assertDefined, invariant } from "@prisma-next/utils/assertions";
10
9
  //#region src/psl-attribute-parsing.ts
10
+ function getPositionalArgument(attribute, index = 0) {
11
+ return attribute.args.filter((arg) => arg.kind === "positional")[index]?.value;
12
+ }
11
13
  function lowerFirst(value) {
12
14
  if (value.length === 0) return value;
13
15
  return value[0]?.toLowerCase() + value.slice(1);
@@ -931,7 +933,7 @@ function resolvePslTypeConstructorDescriptor(input) {
931
933
  *
932
934
  * Symmetric with `instantiatePslTypeConstructor` but richer: a field preset can contribute `default`, `executionDefaults`, `id`, `unique`, and `nullable` in addition to the storage-type triple. PSL → typed-args coercion happens here (via `mapPslHelperArgs`) so that `instantiateAuthoringFieldPreset` itself stays typed-input-only and TS keeps its zero-runtime-validation cost.
933
935
  */
934
- function instantiatePslFieldPreset(input) {
936
+ function instantiateFieldPreset(input) {
935
937
  const helperPath = input.call.path.join(".");
936
938
  const args = mapPslHelperArgs({
937
939
  args: input.call.args,
@@ -970,10 +972,14 @@ function instantiatePslFieldPreset(input) {
970
972
  }
971
973
  }
972
974
  function resolveFieldTypeDescriptor(input) {
975
+ if (input.field.malformedType) return {
976
+ ok: false,
977
+ alreadyReported: true
978
+ };
973
979
  if (input.field.typeConstructor) {
974
980
  const presetDescriptor = getAuthoringFieldPreset(input.authoringContributions, input.field.typeConstructor.path);
975
981
  if (presetDescriptor) {
976
- const instantiated = instantiatePslFieldPreset({
982
+ const instantiated = instantiateFieldPreset({
977
983
  call: input.field.typeConstructor,
978
984
  descriptor: presetDescriptor,
979
985
  diagnostics: input.diagnostics,
@@ -1071,7 +1077,7 @@ const NATIVE_TYPE_SPECS = {
1071
1077
  "db.Uuid": {
1072
1078
  args: "noArgs",
1073
1079
  baseType: "String",
1074
- codecId: null,
1080
+ codecId: "pg/uuid@1",
1075
1081
  nativeType: "uuid"
1076
1082
  },
1077
1083
  "db.SmallInt": {
@@ -1301,13 +1307,53 @@ function lowerDefaultForField(input) {
1301
1307
  return { executionDefaults: { onCreate: lowered.value.generated } };
1302
1308
  }
1303
1309
  function resolveColumnDescriptor(field, enumTypeDescriptors, namedTypeDescriptors, scalarTypeDescriptors) {
1304
- if (field.typeRef && namedTypeDescriptors.has(field.typeRef)) return namedTypeDescriptors.get(field.typeRef);
1305
1310
  if (namedTypeDescriptors.has(field.typeName)) return namedTypeDescriptors.get(field.typeName);
1306
1311
  if (enumTypeDescriptors.has(field.typeName)) return enumTypeDescriptors.get(field.typeName);
1307
1312
  return scalarTypeDescriptors.get(field.typeName);
1308
1313
  }
1309
1314
  //#endregion
1310
1315
  //#region src/psl-field-resolution.ts
1316
+ function lowerEnumDefaultForField(input) {
1317
+ const positionalEntries = input.defaultAttribute.args.filter((arg) => arg.kind === "positional");
1318
+ const hasNamedEntries = input.defaultAttribute.args.some((arg) => arg.kind === "named");
1319
+ const expressionEntry = positionalEntries[0];
1320
+ if (hasNamedEntries || positionalEntries.length !== 1 || expressionEntry === void 0) {
1321
+ input.diagnostics.push({
1322
+ code: "PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT",
1323
+ message: `Field "${input.modelName}.${input.fieldName}" @default on an enum field expects exactly one positional enum member argument.`,
1324
+ sourceId: input.sourceId,
1325
+ span: input.defaultAttribute.span
1326
+ });
1327
+ return {};
1328
+ }
1329
+ const raw = expressionEntry.value.trim();
1330
+ const isQuotedString = /^(['"]).*\1$/.test(raw);
1331
+ const isFunctionCall = raw.includes("(") && raw.endsWith(")");
1332
+ if (isQuotedString || isFunctionCall) {
1333
+ input.diagnostics.push({
1334
+ code: "PSL_ENUM_DEFAULT_MUST_BE_MEMBER_NAME",
1335
+ message: `Field "${input.modelName}.${input.fieldName}" @default on an enum field must name a member (e.g. @default(Low)), not a raw value or function.`,
1336
+ sourceId: input.sourceId,
1337
+ span: input.defaultAttribute.span
1338
+ });
1339
+ return {};
1340
+ }
1341
+ const match = input.enumHandle.enumMembers.find((m) => m.name === raw);
1342
+ if (!match) {
1343
+ const validNames = input.enumHandle.enumMembers.map((m) => m.name).join(", ");
1344
+ input.diagnostics.push({
1345
+ code: "PSL_ENUM_UNKNOWN_DEFAULT_MEMBER",
1346
+ message: `Field "${input.modelName}.${input.fieldName}" @default(${raw}) does not name a member of ${input.enumHandle.enumName}. Valid members: ${validNames}.`,
1347
+ sourceId: input.sourceId,
1348
+ span: input.defaultAttribute.span
1349
+ });
1350
+ return {};
1351
+ }
1352
+ return { defaultValue: {
1353
+ kind: "literal",
1354
+ value: blindCast(match.value)
1355
+ } };
1356
+ }
1311
1357
  const MODEL_COORDINATE_SEPARATOR = "\0";
1312
1358
  function modelCoordinateKey(namespaceId, modelName) {
1313
1359
  return `${namespaceId}${MODEL_COORDINATE_SEPARATOR}${modelName}`;
@@ -1381,9 +1427,9 @@ function extractFieldConstraintNames(input) {
1381
1427
  };
1382
1428
  }
1383
1429
  function collectResolvedFields(input) {
1384
- const { model, mapping, enumTypeDescriptors, namedTypeDescriptors, modelNames, compositeTypeNames, composedExtensions, authoringContributions, familyId, targetId, defaultFunctionRegistry, generatorDescriptorById, diagnostics, sourceId, scalarTypeDescriptors } = input;
1430
+ const { model, mapping, enumTypeDescriptors, namedTypeDescriptors, modelNames, compositeTypeNames, composedExtensions, authoringContributions, familyId, targetId, defaultFunctionRegistry, generatorDescriptorById, diagnostics, sourceId, scalarTypeDescriptors, enumHandles } = input;
1385
1431
  const resolvedFields = [];
1386
- for (const field of model.fields) {
1432
+ for (const field of Object.values(model.fields)) {
1387
1433
  const isModelField = modelNames.has(field.typeName);
1388
1434
  if (field.list && isModelField) continue;
1389
1435
  validateFieldAttributes({
@@ -1474,7 +1520,15 @@ function collectResolvedFields(input) {
1474
1520
  });
1475
1521
  continue;
1476
1522
  }
1477
- const loweredDefault = defaultAttribute ? lowerDefaultForField({
1523
+ const enumHandle = enumHandles?.get(field.typeName);
1524
+ const loweredDefault = defaultAttribute ? enumHandle ? lowerEnumDefaultForField({
1525
+ modelName: model.name,
1526
+ fieldName: field.name,
1527
+ defaultAttribute,
1528
+ enumHandle,
1529
+ sourceId,
1530
+ diagnostics
1531
+ }) : lowerDefaultForField({
1478
1532
  modelName: model.name,
1479
1533
  fieldName: field.name,
1480
1534
  defaultAttribute,
@@ -1495,7 +1549,8 @@ function collectResolvedFields(input) {
1495
1549
  });
1496
1550
  continue;
1497
1551
  }
1498
- if (loweredOnCreate) {
1552
+ const fieldUsesNamedType = namedTypeDescriptors.has(field.typeName);
1553
+ if (loweredOnCreate && !fieldUsesNamedType) {
1499
1554
  const generatedDescriptor = generatorDescriptorById.get(loweredOnCreate.id)?.resolveGeneratedColumnDescriptor?.({ generated: loweredOnCreate });
1500
1555
  if (generatedDescriptor) descriptor = generatedDescriptor;
1501
1556
  }
@@ -1531,6 +1586,7 @@ function collectResolvedFields(input) {
1531
1586
  field,
1532
1587
  columnName: mappedColumnName,
1533
1588
  descriptor,
1589
+ nullable: presetContributions?.nullable ?? field.optional,
1534
1590
  ...ifDefined("defaultValue", fieldDefaultValue),
1535
1591
  ...ifDefined("executionDefaults", fieldExecutionDefaults),
1536
1592
  isId: isIdField || Boolean(presetContributions?.id),
@@ -1556,7 +1612,7 @@ function buildModelMappings(modelEntries, defaultNamespaceId, diagnostics, sourc
1556
1612
  span: model.span
1557
1613
  });
1558
1614
  const fieldColumns = /* @__PURE__ */ new Map();
1559
- for (const field of model.fields) {
1615
+ for (const field of Object.values(model.fields)) {
1560
1616
  const columnName = parseMapName({
1561
1617
  attribute: getAttribute(field.attributes, "map"),
1562
1618
  defaultValue: field.name,
@@ -1576,6 +1632,171 @@ function buildModelMappings(modelEntries, defaultNamespaceId, diagnostics, sourc
1576
1632
  return result;
1577
1633
  }
1578
1634
  //#endregion
1635
+ //#region src/psl-named-type-resolution.ts
1636
+ function validateNamedTypeAttributes(input) {
1637
+ const [dbNativeTypeAttribute, ...extraDbNativeTypeAttributes] = input.allowDbNativeType ? input.declaration.attributes.filter((attribute) => attribute.name.startsWith("db.")) : [];
1638
+ let hasUnsupportedNamedTypeAttribute = false;
1639
+ for (const extra of extraDbNativeTypeAttributes) {
1640
+ input.diagnostics.push({
1641
+ code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
1642
+ message: `Named type "${input.declaration.name}" can declare at most one @db.* attribute`,
1643
+ sourceId: input.sourceId,
1644
+ span: extra.span
1645
+ });
1646
+ hasUnsupportedNamedTypeAttribute = true;
1647
+ }
1648
+ for (const attribute of input.declaration.attributes) {
1649
+ if (input.allowDbNativeType && attribute.name.startsWith("db.")) continue;
1650
+ const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
1651
+ familyId: input.familyId,
1652
+ targetId: input.targetId,
1653
+ authoringContributions: input.authoringContributions
1654
+ });
1655
+ if (uncomposedNamespace) {
1656
+ reportUncomposedNamespace({
1657
+ subjectLabel: `Attribute "@${attribute.name}"`,
1658
+ namespace: uncomposedNamespace,
1659
+ sourceId: input.sourceId,
1660
+ span: attribute.span,
1661
+ diagnostics: input.diagnostics
1662
+ });
1663
+ hasUnsupportedNamedTypeAttribute = true;
1664
+ continue;
1665
+ }
1666
+ input.diagnostics.push({
1667
+ code: "PSL_UNSUPPORTED_NAMED_TYPE_ATTRIBUTE",
1668
+ message: `Named type "${input.declaration.name}" uses unsupported attribute "${attribute.name}"`,
1669
+ sourceId: input.sourceId,
1670
+ span: attribute.span
1671
+ });
1672
+ hasUnsupportedNamedTypeAttribute = true;
1673
+ }
1674
+ return {
1675
+ dbNativeTypeAttribute,
1676
+ hasUnsupportedNamedTypeAttribute
1677
+ };
1678
+ }
1679
+ function resolveNamedTypeDeclarations(input) {
1680
+ const storageTypeEntries = [];
1681
+ const namedTypeDescriptors = /* @__PURE__ */ new Map();
1682
+ for (const declaration of input.declarations) {
1683
+ if (declaration.isConstructor) {
1684
+ const typeConstructor = declaration.typeConstructor;
1685
+ if (typeConstructor === void 0) {
1686
+ input.diagnostics.push({
1687
+ code: "PSL_UNSUPPORTED_NAMED_TYPE_BASE",
1688
+ message: `Named type "${declaration.name}" must declare a base type or constructor`,
1689
+ sourceId: input.sourceId,
1690
+ span: declaration.span
1691
+ });
1692
+ continue;
1693
+ }
1694
+ const { hasUnsupportedNamedTypeAttribute } = validateNamedTypeAttributes({
1695
+ declaration,
1696
+ sourceId: input.sourceId,
1697
+ diagnostics: input.diagnostics,
1698
+ composedExtensions: input.composedExtensions,
1699
+ authoringContributions: input.authoringContributions,
1700
+ allowDbNativeType: false,
1701
+ familyId: input.familyId,
1702
+ targetId: input.targetId
1703
+ });
1704
+ if (hasUnsupportedNamedTypeAttribute) continue;
1705
+ const helperPath = typeConstructor.path.join(".");
1706
+ const descriptor = resolvePslTypeConstructorDescriptor({
1707
+ call: typeConstructor,
1708
+ authoringContributions: input.authoringContributions,
1709
+ composedExtensions: input.composedExtensions,
1710
+ familyId: input.familyId,
1711
+ targetId: input.targetId,
1712
+ diagnostics: input.diagnostics,
1713
+ sourceId: input.sourceId,
1714
+ unsupportedCode: "PSL_UNSUPPORTED_NAMED_TYPE_CONSTRUCTOR",
1715
+ unsupportedMessage: `Named type "${declaration.name}" references unsupported constructor "${helperPath}"`
1716
+ });
1717
+ if (!descriptor) continue;
1718
+ const storageType = instantiatePslTypeConstructor({
1719
+ call: typeConstructor,
1720
+ descriptor,
1721
+ diagnostics: input.diagnostics,
1722
+ sourceId: input.sourceId,
1723
+ entityLabel: `Named type "${declaration.name}"`
1724
+ });
1725
+ if (!storageType) continue;
1726
+ namedTypeDescriptors.set(declaration.name, toNamedTypeFieldDescriptor(declaration.name, storageType));
1727
+ storageTypeEntries.push([declaration.name, {
1728
+ kind: "codec-instance",
1729
+ codecId: storageType.codecId,
1730
+ nativeType: storageType.nativeType,
1731
+ typeParams: storageType.typeParams ?? {}
1732
+ }]);
1733
+ continue;
1734
+ }
1735
+ const baseType = declaration.baseType;
1736
+ if (baseType === void 0) {
1737
+ input.diagnostics.push({
1738
+ code: "PSL_UNSUPPORTED_NAMED_TYPE_BASE",
1739
+ message: `Named type "${declaration.name}" must declare a base type or constructor`,
1740
+ sourceId: input.sourceId,
1741
+ span: declaration.span
1742
+ });
1743
+ continue;
1744
+ }
1745
+ const baseDescriptor = input.enumTypeDescriptors.get(baseType) ?? input.scalarTypeDescriptors.get(baseType);
1746
+ if (!baseDescriptor) {
1747
+ input.diagnostics.push({
1748
+ code: "PSL_UNSUPPORTED_NAMED_TYPE_BASE",
1749
+ message: `Named type "${declaration.name}" references unsupported base type "${baseType}"`,
1750
+ sourceId: input.sourceId,
1751
+ span: declaration.span
1752
+ });
1753
+ continue;
1754
+ }
1755
+ const { dbNativeTypeAttribute, hasUnsupportedNamedTypeAttribute } = validateNamedTypeAttributes({
1756
+ declaration,
1757
+ sourceId: input.sourceId,
1758
+ diagnostics: input.diagnostics,
1759
+ composedExtensions: input.composedExtensions,
1760
+ authoringContributions: input.authoringContributions,
1761
+ allowDbNativeType: true,
1762
+ familyId: input.familyId,
1763
+ targetId: input.targetId
1764
+ });
1765
+ if (hasUnsupportedNamedTypeAttribute) continue;
1766
+ if (dbNativeTypeAttribute) {
1767
+ const descriptor = resolveDbNativeTypeAttribute({
1768
+ attribute: dbNativeTypeAttribute,
1769
+ baseType,
1770
+ baseDescriptor,
1771
+ diagnostics: input.diagnostics,
1772
+ sourceId: input.sourceId,
1773
+ entityLabel: `Named type "${declaration.name}"`
1774
+ });
1775
+ if (!descriptor) continue;
1776
+ namedTypeDescriptors.set(declaration.name, toNamedTypeFieldDescriptor(declaration.name, descriptor));
1777
+ storageTypeEntries.push([declaration.name, {
1778
+ kind: "codec-instance",
1779
+ codecId: descriptor.codecId,
1780
+ nativeType: descriptor.nativeType,
1781
+ typeParams: descriptor.typeParams ?? {}
1782
+ }]);
1783
+ continue;
1784
+ }
1785
+ const descriptor = toNamedTypeFieldDescriptor(declaration.name, baseDescriptor);
1786
+ namedTypeDescriptors.set(declaration.name, descriptor);
1787
+ storageTypeEntries.push([declaration.name, {
1788
+ kind: "codec-instance",
1789
+ codecId: baseDescriptor.codecId,
1790
+ nativeType: baseDescriptor.nativeType,
1791
+ typeParams: {}
1792
+ }]);
1793
+ }
1794
+ return {
1795
+ storageTypes: Object.fromEntries(storageTypeEntries),
1796
+ namedTypeDescriptors
1797
+ };
1798
+ }
1799
+ //#endregion
1579
1800
  //#region src/psl-relation-resolution.ts
1580
1801
  const REFERENTIAL_ACTION_MAP = {
1581
1802
  NoAction: "noAction",
@@ -1713,7 +1934,11 @@ function parseRelationAttribute(input) {
1713
1934
  function indexFkRelations(input) {
1714
1935
  const modelRelations = /* @__PURE__ */ new Map();
1715
1936
  const fkRelationsByPair = /* @__PURE__ */ new Map();
1937
+ const fkRelationsByDeclaringModel = /* @__PURE__ */ new Map();
1716
1938
  for (const relation of input.fkRelationMetadata) {
1939
+ const declaringFkRelations = fkRelationsByDeclaringModel.get(relation.declaringModelName);
1940
+ if (declaringFkRelations) declaringFkRelations.push(relation);
1941
+ else fkRelationsByDeclaringModel.set(relation.declaringModelName, [relation]);
1717
1942
  const existing = modelRelations.get(relation.declaringModelName);
1718
1943
  const current = existing ?? [];
1719
1944
  if (!existing) modelRelations.set(relation.declaringModelName, current);
@@ -1740,15 +1965,177 @@ function indexFkRelations(input) {
1740
1965
  }
1741
1966
  return {
1742
1967
  modelRelations,
1743
- fkRelationsByPair
1968
+ fkRelationsByPair,
1969
+ fkRelationsByDeclaringModel
1970
+ };
1971
+ }
1972
+ function idColumnsAreExactlyFkPair(idColumns, parentColumns, childColumns) {
1973
+ if (idColumns.length !== parentColumns.length + childColumns.length) return false;
1974
+ const fkColumns = new Set([...parentColumns, ...childColumns]);
1975
+ if (fkColumns.size !== parentColumns.length + childColumns.length) return false;
1976
+ return idColumns.every((column) => fkColumns.has(column));
1977
+ }
1978
+ /**
1979
+ * Reorders the child FK's junction columns into the target model's id-column
1980
+ * order. Returns undefined unless the FK references exactly the target's full
1981
+ * id, because downstream consumers pair `through.childColumns` positionally
1982
+ * against the target id columns — an FK referencing anything else (a non-id
1983
+ * unique, a partial id) would produce a silently wrong join.
1984
+ */
1985
+ function childColumnsInTargetIdOrder(childFk, targetIdColumns) {
1986
+ if (childFk.referencedColumns.length !== targetIdColumns.length) return;
1987
+ const localByReferenced = /* @__PURE__ */ new Map();
1988
+ for (const [index, referencedColumn] of childFk.referencedColumns.entries()) {
1989
+ const localColumn = childFk.localColumns[index];
1990
+ if (localColumn === void 0) return;
1991
+ localByReferenced.set(referencedColumn, localColumn);
1992
+ }
1993
+ if (localByReferenced.size !== targetIdColumns.length) return;
1994
+ const ordered = [];
1995
+ for (const idColumn of targetIdColumns) {
1996
+ const localColumn = localByReferenced.get(idColumn);
1997
+ if (localColumn === void 0) return;
1998
+ ordered.push(localColumn);
1999
+ }
2000
+ return ordered;
2001
+ }
2002
+ /**
2003
+ * Finds explicit junction models that connect a bare backrelation list field
2004
+ * to its target model: a model whose composite id columns are exactly the FK
2005
+ * columns of one relation back to the candidate's model (the parent side) and
2006
+ * one relation to the candidate's target model (the child side). The child
2007
+ * FK must reference exactly the target model's id columns; its junction
2008
+ * columns are carried in target-id order on the pair. A relation name on the
2009
+ * list field pins the parent-side FK relation, which is how self-referential
2010
+ * many-to-many sides are disambiguated.
2011
+ *
2012
+ * Alongside the recognised pairs, returns junction-shaped near-misses (models
2013
+ * that link both sides but were declined) so the caller can emit a
2014
+ * junction-specific diagnostic instead of the generic orphaned-list message.
2015
+ */
2016
+ function findJunctionFkPairs(input) {
2017
+ const targetIdColumns = input.modelIdColumns.get(input.candidate.targetModelName);
2018
+ if (!targetIdColumns || targetIdColumns.length === 0) return {
2019
+ pairs: [],
2020
+ nearMisses: []
2021
+ };
2022
+ const pairs = [];
2023
+ const nearMisses = [];
2024
+ for (const [junctionModelName, junctionFks] of input.fkRelationsByDeclaringModel) {
2025
+ const idColumns = input.modelIdColumns.get(junctionModelName);
2026
+ for (const parentFk of junctionFks) {
2027
+ if (parentFk.targetModelName !== input.candidate.modelName) continue;
2028
+ if (input.candidate.relationName !== void 0 && parentFk.relationName !== input.candidate.relationName) continue;
2029
+ for (const childFk of junctionFks) {
2030
+ if (childFk === parentFk || childFk.targetModelName !== input.candidate.targetModelName) continue;
2031
+ if (!idColumns || !idColumnsAreExactlyFkPair(idColumns, parentFk.localColumns, childFk.localColumns)) {
2032
+ nearMisses.push({
2033
+ junctionModelName,
2034
+ reason: "id-not-fk-covering"
2035
+ });
2036
+ continue;
2037
+ }
2038
+ const orderedChildColumns = childColumnsInTargetIdOrder(childFk, targetIdColumns);
2039
+ if (!orderedChildColumns) {
2040
+ nearMisses.push({
2041
+ junctionModelName,
2042
+ reason: "target-fk-not-id"
2043
+ });
2044
+ continue;
2045
+ }
2046
+ pairs.push({
2047
+ parentFk,
2048
+ childFk,
2049
+ childColumnsInTargetIdOrder: orderedChildColumns
2050
+ });
2051
+ }
2052
+ }
2053
+ }
2054
+ return {
2055
+ pairs,
2056
+ nearMisses
2057
+ };
2058
+ }
2059
+ function junctionNearMissDiagnostic(candidate, nearMiss, sourceId) {
2060
+ const listField = `${candidate.modelName}.${candidate.field.name}`;
2061
+ const data = {
2062
+ listField,
2063
+ junctionModel: nearMiss.junctionModelName,
2064
+ targetModel: candidate.targetModelName
2065
+ };
2066
+ if (nearMiss.reason === "target-fk-not-id") return {
2067
+ code: "PSL_JUNCTION_TARGET_FK_NOT_ID",
2068
+ message: `Backrelation list field "${listField}" found junction model "${nearMiss.junctionModelName}", but its foreign key to "${candidate.targetModelName}" does not reference "${candidate.targetModelName}"'s @id. The junction's target-side foreign key must reference "${candidate.targetModelName}"'s full @id columns for many-to-many recognition.`,
2069
+ sourceId,
2070
+ span: candidate.field.span,
2071
+ data
2072
+ };
2073
+ return {
2074
+ code: "PSL_JUNCTION_ID_NOT_FK_COVERING",
2075
+ message: `Backrelation list field "${listField}" found junction-shaped model "${nearMiss.junctionModelName}" linking "${candidate.modelName}" and "${candidate.targetModelName}", but its id does not cover exactly its foreign-key columns. Declare @@id([...]) on "${nearMiss.junctionModelName}" listing exactly the two foreign-key columns for many-to-many recognition.`,
2076
+ sourceId,
2077
+ span: candidate.field.span,
2078
+ data
1744
2079
  };
1745
2080
  }
2081
+ function manyToManyRelationNode(candidate, pair) {
2082
+ return {
2083
+ fieldName: candidate.field.name,
2084
+ toModel: pair.childFk.targetModelName,
2085
+ toTable: pair.childFk.targetTableName,
2086
+ ...ifDefined("toNamespaceId", pair.childFk.targetNamespaceId),
2087
+ cardinality: "N:M",
2088
+ on: {
2089
+ parentTable: candidate.tableName,
2090
+ parentColumns: pair.parentFk.referencedColumns,
2091
+ childTable: pair.parentFk.declaringTableName,
2092
+ childColumns: pair.parentFk.localColumns
2093
+ },
2094
+ through: {
2095
+ table: pair.parentFk.declaringTableName,
2096
+ ...ifDefined("namespaceId", pair.parentFk.declaringNamespaceId),
2097
+ parentColumns: pair.parentFk.localColumns,
2098
+ childColumns: pair.childColumnsInTargetIdOrder
2099
+ }
2100
+ };
2101
+ }
2102
+ function relationsForModel(modelRelations, modelName) {
2103
+ const existing = modelRelations.get(modelName);
2104
+ if (existing) return existing;
2105
+ const created = [];
2106
+ modelRelations.set(modelName, created);
2107
+ return created;
2108
+ }
1746
2109
  function applyBackrelationCandidates(input) {
1747
2110
  for (const candidate of input.backrelationCandidates) {
1748
2111
  const pairKey = fkRelationPairKey(candidate.targetModelName, candidate.modelName);
1749
2112
  const pairMatches = input.fkRelationsByPair.get(pairKey) ?? [];
1750
2113
  const matches = candidate.relationName ? pairMatches.filter((relation) => relation.relationName === candidate.relationName) : [...pairMatches];
1751
2114
  if (matches.length === 0) {
2115
+ const { pairs: junctionPairs, nearMisses } = findJunctionFkPairs({
2116
+ candidate,
2117
+ fkRelationsByDeclaringModel: input.fkRelationsByDeclaringModel,
2118
+ modelIdColumns: input.modelIdColumns
2119
+ });
2120
+ const junctionPair = junctionPairs[0];
2121
+ if (junctionPairs.length === 1 && junctionPair) {
2122
+ relationsForModel(input.modelRelations, candidate.modelName).push(manyToManyRelationNode(candidate, junctionPair));
2123
+ continue;
2124
+ }
2125
+ if (junctionPairs.length > 1) {
2126
+ input.diagnostics.push({
2127
+ code: "PSL_AMBIGUOUS_BACKRELATION_LIST",
2128
+ message: `Backrelation list field "${candidate.modelName}.${candidate.field.name}" matches multiple junction FK pairs for a many-to-many relation. Add @relation(name: "...") (or @relation("...")) to the list field and the junction FK-side relation pointing back at "${candidate.modelName}" to disambiguate.`,
2129
+ sourceId: input.sourceId,
2130
+ span: candidate.field.span
2131
+ });
2132
+ continue;
2133
+ }
2134
+ const nearMiss = nearMisses[0];
2135
+ if (nearMiss) {
2136
+ input.diagnostics.push(junctionNearMissDiagnostic(candidate, nearMiss, input.sourceId));
2137
+ continue;
2138
+ }
1752
2139
  input.diagnostics.push({
1753
2140
  code: "PSL_ORPHANED_BACKRELATION_LIST",
1754
2141
  message: `Backrelation list field "${candidate.modelName}.${candidate.field.name}" has no matching FK-side relation on model "${candidate.targetModelName}". Add @relation(fields: [...], references: [...]) on the FK-side relation or use an explicit join model for many-to-many.`,
@@ -1769,13 +2156,11 @@ function applyBackrelationCandidates(input) {
1769
2156
  invariant(matches.length === 1, "Backrelation matching requires exactly one match");
1770
2157
  const matched = matches[0];
1771
2158
  assertDefined(matched, "Backrelation matching requires a defined relation match");
1772
- const existing = input.modelRelations.get(candidate.modelName);
1773
- const current = existing ?? [];
1774
- if (!existing) input.modelRelations.set(candidate.modelName, current);
1775
- current.push({
2159
+ relationsForModel(input.modelRelations, candidate.modelName).push({
1776
2160
  fieldName: candidate.field.name,
1777
2161
  toModel: matched.declaringModelName,
1778
2162
  toTable: matched.declaringTableName,
2163
+ ...ifDefined("toNamespaceId", matched.declaringNamespaceId),
1779
2164
  cardinality: "1:N",
1780
2165
  on: {
1781
2166
  parentTable: candidate.tableName,
@@ -1829,32 +2214,11 @@ function buildComposedExtensionPackRefs(target, extensionIds, extensionPackRefs
1829
2214
  version: "0.0.1"
1830
2215
  }]));
1831
2216
  }
1832
- function diagnosticDedupKey(diagnostic) {
1833
- const span = diagnostic.span;
1834
- const spanKey = span ? `${span.start.offset}:${span.end.offset}:${span.start.line}:${span.end.line}` : "";
1835
- return `${diagnostic.code}\u0000${diagnostic.sourceId}\u0000${spanKey}\u0000${diagnostic.message}`;
1836
- }
1837
- function dedupeDiagnostics(diagnostics) {
1838
- const seen = /* @__PURE__ */ new Map();
1839
- for (const diagnostic of diagnostics) {
1840
- const key = diagnosticDedupKey(diagnostic);
1841
- if (!seen.has(key)) seen.set(key, diagnostic);
1842
- }
1843
- return [...seen.values()];
1844
- }
1845
2217
  function compareStrings(left, right) {
1846
2218
  if (left < right) return -1;
1847
2219
  if (left > right) return 1;
1848
2220
  return 0;
1849
2221
  }
1850
- function mapParserDiagnostics(document) {
1851
- return document.diagnostics.map((diagnostic) => ({
1852
- code: diagnostic.code,
1853
- message: diagnostic.message,
1854
- sourceId: diagnostic.sourceId,
1855
- span: diagnostic.span
1856
- }));
1857
- }
1858
2222
  /**
1859
2223
  * Name of the framework-parser synthesised bucket for top-level
1860
2224
  * declarations. Re-declared here so the per-target dispatch does not
@@ -1916,240 +2280,69 @@ function resolveNamespaceIdForSqlTarget(input) {
1916
2280
  }
1917
2281
  function validateNamespaceBlocksForSqlTarget(input) {
1918
2282
  if (input.targetId === "sqlite") {
1919
- for (const namespace of input.namespaces) {
1920
- if (namespace.name === UNSPECIFIED_PSL_NAMESPACE_NAME) continue;
1921
- input.diagnostics.push({
1922
- code: "PSL_UNSUPPORTED_NAMESPACE_BLOCK",
1923
- message: `SQLite does not support \`namespace ${namespace.name} { … }\` blocks (SQLite has no schema concept; declare models at the document top level instead).`,
1924
- sourceId: input.sourceId,
1925
- span: namespace.span
1926
- });
1927
- }
2283
+ for (const namespace of input.namespaces) input.diagnostics.push({
2284
+ code: "PSL_UNSUPPORTED_NAMESPACE_BLOCK",
2285
+ message: `SQLite does not support \`namespace ${namespace.name} { … }\` blocks (SQLite has no schema concept; declare models at the document top level instead).`,
2286
+ sourceId: input.sourceId,
2287
+ span: nodePslSpan(namespace.node.syntax, input.sourceFile)
2288
+ });
1928
2289
  return;
1929
2290
  }
1930
2291
  if (input.targetId === "postgres") {
1931
- const namedBlocks = input.namespaces.filter((ns) => ns.name !== UNSPECIFIED_PSL_NAMESPACE_NAME);
1932
- const hasUnbound = namedBlocks.some((ns) => ns.name === "unbound");
1933
- const hasSibling = namedBlocks.some((ns) => ns.name !== "unbound");
2292
+ const hasUnbound = input.namespaces.some((ns) => ns.name === "unbound");
2293
+ const hasSibling = input.namespaces.some((ns) => ns.name !== "unbound");
1934
2294
  if (hasUnbound && hasSibling) {
1935
- const unboundBlock = namedBlocks.find((ns) => ns.name === "unbound");
2295
+ const unboundBlock = input.namespaces.find((ns) => ns.name === "unbound");
1936
2296
  input.diagnostics.push({
1937
2297
  code: "PSL_RESERVED_NAMESPACE_NAME",
1938
2298
  message: "Namespace \"unbound\" is reserved for the late-binding sentinel mapping and cannot appear alongside other named namespace blocks. Use `namespace unbound { … }` alone (no sibling named namespaces) for late-binding multi-tenant contracts.",
1939
2299
  sourceId: input.sourceId,
1940
- ...ifDefined("span", unboundBlock?.span)
2300
+ ...unboundBlock !== void 0 ? { span: nodePslSpan(unboundBlock.node.syntax, input.sourceFile) } : {}
1941
2301
  });
1942
2302
  }
1943
2303
  }
1944
2304
  }
1945
2305
  function processEnumDeclarations(input) {
1946
- const storageTypes = {};
2306
+ const enumHandles = {};
1947
2307
  const enumTypeDescriptors = /* @__PURE__ */ new Map();
1948
- if (input.enums.length === 0) return {
1949
- storageTypes,
2308
+ if (input.enumBlocks.length === 0) return {
2309
+ enumHandles,
1950
2310
  enumTypeDescriptors
1951
2311
  };
1952
- if (!input.enumEntityDescriptor) {
1953
- for (const enumDeclaration of input.enums) input.diagnostics.push({
1954
- code: "PSL_UNSUPPORTED_NAMED_TYPE_BASE",
1955
- message: `Enum "${enumDeclaration.name}" requires the active target pack to contribute an enum entity-type helper`,
2312
+ const enumDescriptor = getAuthoringEntity(input.authoringContributions, ["enum"]);
2313
+ if (!enumDescriptor) {
2314
+ for (const decl of input.enumBlocks) input.diagnostics.push({
2315
+ code: "PSL_ENUM_MISSING_FACTORY",
2316
+ message: `enum "${decl.name}" requires an "enum" entityType factory in the active authoring contributions`,
1956
2317
  sourceId: input.sourceId,
1957
- span: enumDeclaration.span
2318
+ span: decl.span
1958
2319
  });
1959
2320
  return {
1960
- storageTypes,
2321
+ enumHandles,
1961
2322
  enumTypeDescriptors
1962
2323
  };
1963
2324
  }
1964
- for (const enumDeclaration of input.enums) {
1965
- const nativeType = parseMapName({
1966
- attribute: getAttribute(enumDeclaration.attributes, "map"),
1967
- defaultValue: enumDeclaration.name,
1968
- sourceId: input.sourceId,
1969
- diagnostics: input.diagnostics,
1970
- entityLabel: `Enum "${enumDeclaration.name}"`,
1971
- span: enumDeclaration.span
1972
- });
1973
- const values = enumDeclaration.values.map((value) => value.name);
1974
- const constructed = instantiateAuthoringEntityType("enum", input.enumEntityDescriptor, [{
1975
- name: enumDeclaration.name,
1976
- nativeType,
1977
- values
1978
- }], input.entityContext);
1979
- if (!isPostgresEnumStorageEntry(constructed)) {
1980
- input.diagnostics.push({
1981
- code: "PSL_UNSUPPORTED_NAMED_TYPE_BASE",
1982
- message: `Enum "${enumDeclaration.name}": enum entity-type factory must return a PostgresEnumStorageEntry-shaped value (kind: 'postgres-enum')`,
1983
- sourceId: input.sourceId,
1984
- span: enumDeclaration.span
1985
- });
1986
- continue;
1987
- }
1988
- const descriptor = {
1989
- codecId: constructed.codecId,
1990
- nativeType: constructed.nativeType,
1991
- typeRef: enumDeclaration.name
1992
- };
1993
- enumTypeDescriptors.set(enumDeclaration.name, descriptor);
1994
- storageTypes[enumDeclaration.name] = constructed;
1995
- }
1996
- return {
1997
- storageTypes,
1998
- enumTypeDescriptors
1999
- };
2000
- }
2001
- function validateNamedTypeAttributes(input) {
2002
- const [dbNativeTypeAttribute, ...extraDbNativeTypeAttributes] = input.allowDbNativeType ? input.declaration.attributes.filter((attribute) => attribute.name.startsWith("db.")) : [];
2003
- let hasUnsupportedNamedTypeAttribute = false;
2004
- for (const extra of extraDbNativeTypeAttributes) {
2005
- input.diagnostics.push({
2006
- code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
2007
- message: `Named type "${input.declaration.name}" can declare at most one @db.* attribute`,
2008
- sourceId: input.sourceId,
2009
- span: extra.span
2010
- });
2011
- hasUnsupportedNamedTypeAttribute = true;
2012
- }
2013
- for (const attribute of input.declaration.attributes) {
2014
- if (input.allowDbNativeType && attribute.name.startsWith("db.")) continue;
2015
- const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
2016
- familyId: input.familyId,
2017
- targetId: input.targetId,
2018
- authoringContributions: input.authoringContributions
2019
- });
2020
- if (uncomposedNamespace) {
2021
- reportUncomposedNamespace({
2022
- subjectLabel: `Attribute "@${attribute.name}"`,
2023
- namespace: uncomposedNamespace,
2024
- sourceId: input.sourceId,
2025
- span: attribute.span,
2026
- diagnostics: input.diagnostics
2027
- });
2028
- hasUnsupportedNamedTypeAttribute = true;
2029
- continue;
2030
- }
2031
- input.diagnostics.push({
2032
- code: "PSL_UNSUPPORTED_NAMED_TYPE_ATTRIBUTE",
2033
- message: `Named type "${input.declaration.name}" uses unsupported attribute "${attribute.name}"`,
2034
- sourceId: input.sourceId,
2035
- span: attribute.span
2325
+ for (const decl of input.enumBlocks) {
2326
+ const handle = instantiateAuthoringEntityType("enum", enumDescriptor, [decl], input.entityContext);
2327
+ if (handle === void 0 || handle === null) continue;
2328
+ const enumHandle = blindCast(handle);
2329
+ enumHandles[decl.name] = enumHandle;
2330
+ enumTypeDescriptors.set(decl.name, {
2331
+ codecId: enumHandle.codecId,
2332
+ nativeType: enumHandle.nativeType
2036
2333
  });
2037
- hasUnsupportedNamedTypeAttribute = true;
2038
2334
  }
2039
2335
  return {
2040
- dbNativeTypeAttribute,
2041
- hasUnsupportedNamedTypeAttribute
2336
+ enumHandles,
2337
+ enumTypeDescriptors
2042
2338
  };
2043
2339
  }
2044
- function resolveNamedTypeDeclarations(input) {
2045
- const storageTypes = {};
2046
- const namedTypeDescriptors = /* @__PURE__ */ new Map();
2047
- for (const declaration of input.declarations) {
2048
- if (declaration.typeConstructor) {
2049
- const { hasUnsupportedNamedTypeAttribute } = validateNamedTypeAttributes({
2050
- declaration,
2051
- sourceId: input.sourceId,
2052
- diagnostics: input.diagnostics,
2053
- composedExtensions: input.composedExtensions,
2054
- authoringContributions: input.authoringContributions,
2055
- allowDbNativeType: false,
2056
- familyId: input.familyId,
2057
- targetId: input.targetId
2058
- });
2059
- if (hasUnsupportedNamedTypeAttribute) continue;
2060
- const helperPath = declaration.typeConstructor.path.join(".");
2061
- const typeConstructor = resolvePslTypeConstructorDescriptor({
2062
- call: declaration.typeConstructor,
2063
- authoringContributions: input.authoringContributions,
2064
- composedExtensions: input.composedExtensions,
2065
- familyId: input.familyId,
2066
- targetId: input.targetId,
2067
- diagnostics: input.diagnostics,
2068
- sourceId: input.sourceId,
2069
- unsupportedCode: "PSL_UNSUPPORTED_NAMED_TYPE_CONSTRUCTOR",
2070
- unsupportedMessage: `Named type "${declaration.name}" references unsupported constructor "${helperPath}"`
2071
- });
2072
- if (!typeConstructor) continue;
2073
- const storageType = instantiatePslTypeConstructor({
2074
- call: declaration.typeConstructor,
2075
- descriptor: typeConstructor,
2076
- diagnostics: input.diagnostics,
2077
- sourceId: input.sourceId,
2078
- entityLabel: `Named type "${declaration.name}"`
2079
- });
2080
- if (!storageType) continue;
2081
- namedTypeDescriptors.set(declaration.name, toNamedTypeFieldDescriptor(declaration.name, storageType));
2082
- storageTypes[declaration.name] = {
2083
- kind: "codec-instance",
2084
- codecId: storageType.codecId,
2085
- nativeType: storageType.nativeType,
2086
- typeParams: storageType.typeParams ?? {}
2087
- };
2088
- continue;
2089
- }
2090
- if (declaration.baseType === void 0) {
2091
- input.diagnostics.push({
2092
- code: "PSL_UNSUPPORTED_NAMED_TYPE_BASE",
2093
- message: `Named type "${declaration.name}" must declare a base type or constructor`,
2094
- sourceId: input.sourceId,
2095
- span: declaration.span
2096
- });
2097
- continue;
2098
- }
2099
- const { baseType } = declaration;
2100
- const baseDescriptor = input.enumTypeDescriptors.get(baseType) ?? input.scalarTypeDescriptors.get(baseType);
2101
- if (!baseDescriptor) {
2102
- input.diagnostics.push({
2103
- code: "PSL_UNSUPPORTED_NAMED_TYPE_BASE",
2104
- message: `Named type "${declaration.name}" references unsupported base type "${baseType}"`,
2105
- sourceId: input.sourceId,
2106
- span: declaration.span
2107
- });
2108
- continue;
2109
- }
2110
- const { dbNativeTypeAttribute, hasUnsupportedNamedTypeAttribute } = validateNamedTypeAttributes({
2111
- declaration,
2112
- sourceId: input.sourceId,
2113
- diagnostics: input.diagnostics,
2114
- composedExtensions: input.composedExtensions,
2115
- authoringContributions: input.authoringContributions,
2116
- allowDbNativeType: true,
2117
- familyId: input.familyId,
2118
- targetId: input.targetId
2119
- });
2120
- if (hasUnsupportedNamedTypeAttribute) continue;
2121
- if (dbNativeTypeAttribute) {
2122
- const descriptor = resolveDbNativeTypeAttribute({
2123
- attribute: dbNativeTypeAttribute,
2124
- baseType,
2125
- baseDescriptor,
2126
- diagnostics: input.diagnostics,
2127
- sourceId: input.sourceId,
2128
- entityLabel: `Named type "${declaration.name}"`
2129
- });
2130
- if (!descriptor) continue;
2131
- namedTypeDescriptors.set(declaration.name, toNamedTypeFieldDescriptor(declaration.name, descriptor));
2132
- storageTypes[declaration.name] = {
2133
- kind: "codec-instance",
2134
- codecId: descriptor.codecId,
2135
- nativeType: descriptor.nativeType,
2136
- typeParams: descriptor.typeParams ?? {}
2137
- };
2138
- continue;
2139
- }
2140
- const descriptor = toNamedTypeFieldDescriptor(declaration.name, baseDescriptor);
2141
- namedTypeDescriptors.set(declaration.name, descriptor);
2142
- storageTypes[declaration.name] = {
2143
- kind: "codec-instance",
2144
- codecId: baseDescriptor.codecId,
2145
- nativeType: baseDescriptor.nativeType,
2146
- typeParams: {}
2147
- };
2148
- }
2149
- return {
2150
- storageTypes,
2151
- namedTypeDescriptors
2152
- };
2340
+ /** Generic top-level blocks are supported only when a composed descriptor claims their keyword. */
2341
+ function composedBlockKeywords(authoringContributions) {
2342
+ const keywords = /* @__PURE__ */ new Set();
2343
+ const descriptors = authoringContributions?.pslBlockDescriptors ?? {};
2344
+ for (const [keyword, value] of Object.entries(descriptors)) if (isAuthoringPslBlockDescriptor(value)) keywords.add(keyword);
2345
+ return keywords;
2153
2346
  }
2154
2347
  function buildModelNodeFromPsl(input) {
2155
2348
  const { model, mapping, sourceId, diagnostics } = input;
@@ -2169,7 +2362,8 @@ function buildModelNodeFromPsl(input) {
2169
2362
  generatorDescriptorById: input.generatorDescriptorById,
2170
2363
  diagnostics,
2171
2364
  sourceId,
2172
- scalarTypeDescriptors: input.scalarTypeDescriptors
2365
+ scalarTypeDescriptors: input.scalarTypeDescriptors,
2366
+ ...ifDefined("enumHandles", input.enumHandles)
2173
2367
  });
2174
2368
  const inlineIdFields = resolvedFields.filter((field) => field.isId);
2175
2369
  if (inlineIdFields.length > 1) diagnostics.push({
@@ -2188,7 +2382,7 @@ function buildModelNodeFromPsl(input) {
2188
2382
  let controlPolicyDeclared = false;
2189
2383
  let controlPolicy;
2190
2384
  const resultBackrelationCandidates = [];
2191
- for (const field of model.fields) {
2385
+ for (const field of Object.values(model.fields)) {
2192
2386
  if (!field.list || !input.modelNames.has(field.typeName)) continue;
2193
2387
  const attributesValid = validateNavigationListFieldAttributes({
2194
2388
  modelName: model.name,
@@ -2240,7 +2434,7 @@ function buildModelNodeFromPsl(input) {
2240
2434
  ...ifDefined("relationName", relationName)
2241
2435
  });
2242
2436
  }
2243
- const relationAttributes = model.fields.map((field) => ({
2437
+ const relationAttributes = Object.values(model.fields).map((field) => ({
2244
2438
  field,
2245
2439
  relation: getAttribute(field.attributes, "relation")
2246
2440
  })).filter((entry) => Boolean(entry.relation));
@@ -2311,7 +2505,7 @@ function buildModelNodeFromPsl(input) {
2311
2505
  });
2312
2506
  continue;
2313
2507
  }
2314
- const nullableFieldName = fieldNames.find((name) => model.fields.find((f) => f.name === name)?.optional === true);
2508
+ const nullableFieldName = fieldNames.find((name) => model.fields[name]?.optional === true);
2315
2509
  if (nullableFieldName !== void 0) {
2316
2510
  diagnostics.push({
2317
2511
  code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
@@ -2698,6 +2892,7 @@ function buildModelNodeFromPsl(input) {
2698
2892
  declaringModelName: model.name,
2699
2893
  declaringFieldName: relationAttribute.field.name,
2700
2894
  declaringTableName: tableName,
2895
+ ...ifDefined("declaringNamespaceId", input.modelNamespaceIds.get(model.name)),
2701
2896
  targetModelName: targetMapping.model.name,
2702
2897
  targetTableName: targetMapping.tableName,
2703
2898
  ...ifDefined("targetNamespaceId", targetNamespaceId),
@@ -2710,14 +2905,18 @@ function buildModelNodeFromPsl(input) {
2710
2905
  modelNode: {
2711
2906
  modelName: model.name,
2712
2907
  tableName,
2713
- fields: resolvedFields.map((resolvedField) => ({
2714
- fieldName: resolvedField.field.name,
2715
- columnName: resolvedField.columnName,
2716
- descriptor: resolvedField.descriptor,
2717
- nullable: resolvedField.field.optional,
2718
- ...ifDefined("default", resolvedField.defaultValue),
2719
- ...ifDefined("executionDefaults", resolvedField.executionDefaults)
2720
- })),
2908
+ fields: resolvedFields.map((resolvedField) => {
2909
+ const enumHandle = input.enumHandles?.get(resolvedField.field.typeName);
2910
+ return {
2911
+ fieldName: resolvedField.field.name,
2912
+ columnName: resolvedField.columnName,
2913
+ descriptor: resolvedField.descriptor,
2914
+ nullable: resolvedField.nullable,
2915
+ ...ifDefined("default", resolvedField.defaultValue),
2916
+ ...ifDefined("executionDefaults", resolvedField.executionDefaults),
2917
+ ...ifDefined("enumTypeHandle", enumHandle)
2918
+ };
2919
+ }),
2721
2920
  ...ifDefined("id", primaryKey),
2722
2921
  ...uniqueConstraints.length > 0 ? { uniques: uniqueConstraints } : {},
2723
2922
  ...indexNodes.length > 0 ? { indexes: indexNodes } : {},
@@ -2736,7 +2935,7 @@ function buildValueObjects(input) {
2736
2935
  const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
2737
2936
  for (const compositeType of compositeTypes) {
2738
2937
  const fields = {};
2739
- for (const field of compositeType.fields) {
2938
+ for (const field of Object.values(compositeType.fields)) {
2740
2939
  if (compositeTypeNames.has(field.typeName)) {
2741
2940
  const result = {
2742
2941
  type: {
@@ -2842,7 +3041,7 @@ function collectPolymorphismDeclarations(models, sourceId, diagnostics) {
2842
3041
  });
2843
3042
  continue;
2844
3043
  }
2845
- const discField = model.fields.find((f) => f.name === fieldName);
3044
+ const discField = model.fields[fieldName];
2846
3045
  if (discField && discField.typeName !== "String") {
2847
3046
  diagnostics.push({
2848
3047
  code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
@@ -3147,7 +3346,7 @@ function stripStorageOnlyDomainFields(model, fieldNames) {
3147
3346
  };
3148
3347
  }
3149
3348
  function interpretPslDocumentToSqlContract(input) {
3150
- const sourceId = input.document.ast.sourceId;
3349
+ const sourceId = input.sourceId;
3151
3350
  if (!input.target) return notOk({
3152
3351
  summary: "PSL to SQL contract interpretation failed",
3153
3352
  diagnostics: [{
@@ -3164,22 +3363,27 @@ function interpretPslDocumentToSqlContract(input) {
3164
3363
  sourceId
3165
3364
  }]
3166
3365
  });
3167
- const diagnostics = mapParserDiagnostics(input.document);
3366
+ const { topLevel } = input.symbolTable;
3367
+ const sourceFile = input.sourceFile;
3368
+ const namespaceSymbols = Object.values(topLevel.namespaces);
3369
+ const diagnostics = [...input.seedDiagnostics ?? []];
3168
3370
  validateNamespaceBlocksForSqlTarget({
3169
- namespaces: input.document.ast.namespaces,
3371
+ namespaces: namespaceSymbols,
3170
3372
  targetId: input.target.targetId,
3171
3373
  sourceId,
3374
+ sourceFile,
3172
3375
  diagnostics
3173
3376
  });
3174
3377
  const models = [];
3175
3378
  const modelEntries = [];
3176
3379
  const modelNamespaceIds = /* @__PURE__ */ new Map();
3177
- for (const namespace of input.document.ast.namespaces) {
3380
+ const compositeTypes = [];
3381
+ const collectScope = (bucketName, scopeModels, scopeCompositeTypes) => {
3178
3382
  const resolvedNamespaceId = resolveNamespaceIdForSqlTarget({
3179
- bucketName: namespace.name,
3383
+ bucketName,
3180
3384
  targetId: input.target.targetId
3181
3385
  });
3182
- for (const model of namespace.models) {
3386
+ for (const model of scopeModels) {
3183
3387
  models.push(model);
3184
3388
  modelEntries.push({
3185
3389
  model,
@@ -3187,21 +3391,11 @@ function interpretPslDocumentToSqlContract(input) {
3187
3391
  });
3188
3392
  if (resolvedNamespaceId !== void 0) modelNamespaceIds.set(model.name, resolvedNamespaceId);
3189
3393
  }
3190
- }
3394
+ for (const compositeType of scopeCompositeTypes) compositeTypes.push(compositeType);
3395
+ };
3396
+ collectScope(UNSPECIFIED_PSL_NAMESPACE_NAME, Object.values(topLevel.models), Object.values(topLevel.compositeTypes));
3397
+ for (const namespace of namespaceSymbols) collectScope(namespace.name, Object.values(namespace.models), Object.values(namespace.compositeTypes));
3191
3398
  const defaultNamespaceId = input.target.defaultNamespaceId;
3192
- const topLevelEnums = input.document.ast.namespaces.filter((ns) => ns.name === UNSPECIFIED_PSL_NAMESPACE_NAME).flatMap((ns) => ns.enums);
3193
- const namedNamespaceEnumsByNsId = /* @__PURE__ */ new Map();
3194
- for (const ns of input.document.ast.namespaces) {
3195
- if (ns.name === UNSPECIFIED_PSL_NAMESPACE_NAME || ns.enums.length === 0) continue;
3196
- const resolvedId = resolveNamespaceIdForSqlTarget({
3197
- bucketName: ns.name,
3198
- targetId: input.target.targetId
3199
- });
3200
- if (resolvedId === void 0) continue;
3201
- const existing = namedNamespaceEnumsByNsId.get(resolvedId) ?? [];
3202
- namedNamespaceEnumsByNsId.set(resolvedId, [...existing, ...ns.enums]);
3203
- }
3204
- const compositeTypes = input.document.ast.namespaces.flatMap((ns) => ns.compositeTypes);
3205
3399
  const modelNames = new Set(models.map((model) => model.name));
3206
3400
  const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
3207
3401
  const composedExtensions = new Set(input.composedExtensionPacks ?? []);
@@ -3210,35 +3404,55 @@ function interpretPslDocumentToSqlContract(input) {
3210
3404
  const generatorDescriptors = input.controlMutationDefaults?.generatorDescriptors ?? [];
3211
3405
  const generatorDescriptorById = /* @__PURE__ */ new Map();
3212
3406
  for (const descriptor of generatorDescriptors) generatorDescriptorById.set(descriptor.id, descriptor);
3213
- const enumEntityDescriptor = getAuthoringEntity(input.authoringContributions, ["enum"]);
3214
- const enumEntityContext = {
3215
- family: input.target.familyId,
3216
- target: input.target.targetId
3407
+ const isEnumBlock = (block) => block.keyword === "enum";
3408
+ const legitimateBlockKeywords = composedBlockKeywords(input.authoringContributions);
3409
+ const reportUnsupportedTopLevelBlock = (block) => {
3410
+ diagnostics.push({
3411
+ code: "PSL_UNSUPPORTED_TOP_LEVEL_BLOCK",
3412
+ message: `Unsupported top-level block "${block.keyword}"`,
3413
+ sourceId,
3414
+ span: keywordPslSpan(block.node.syntax, block.keyword, sourceFile)
3415
+ });
3217
3416
  };
3417
+ const topLevelEnums = Object.values(topLevel.blocks).filter((block) => {
3418
+ if (!legitimateBlockKeywords.has(block.keyword)) {
3419
+ reportUnsupportedTopLevelBlock(block);
3420
+ return false;
3421
+ }
3422
+ return isEnumBlock(block);
3423
+ }).map((block) => block.block);
3424
+ for (const namespace of namespaceSymbols) for (const block of Object.values(namespace.blocks)) {
3425
+ if (isEnumBlock(block)) {
3426
+ diagnostics.push({
3427
+ code: "PSL_ENUM_NAMESPACE_NOT_SUPPORTED",
3428
+ message: `enum "${block.name}" inside namespace "${namespace.name}" is not supported; declare enum at the top level`,
3429
+ sourceId,
3430
+ span: nodePslSpan(block.node.syntax, sourceFile)
3431
+ });
3432
+ continue;
3433
+ }
3434
+ if (!legitimateBlockKeywords.has(block.keyword)) reportUnsupportedTopLevelBlock(block);
3435
+ }
3218
3436
  const enumResult = processEnumDeclarations({
3219
- enums: topLevelEnums,
3437
+ enumBlocks: topLevelEnums,
3220
3438
  sourceId,
3221
- enumEntityDescriptor,
3222
- entityContext: enumEntityContext,
3439
+ authoringContributions: input.authoringContributions,
3440
+ entityContext: {
3441
+ family: input.target.familyId,
3442
+ target: input.target.targetId,
3443
+ ...ifDefined("codecLookup", input.codecLookup),
3444
+ sourceId,
3445
+ diagnostics: { push: (d) => {
3446
+ diagnostics.push(blindCast(d));
3447
+ } }
3448
+ },
3223
3449
  diagnostics
3224
3450
  });
3225
3451
  const allEnumTypeDescriptors = new Map(enumResult.enumTypeDescriptors);
3226
- const namespaceEnumStorageTypes = {};
3227
- for (const [nsId, nsEnums] of namedNamespaceEnumsByNsId) {
3228
- const nsEnumResult = processEnumDeclarations({
3229
- enums: nsEnums,
3230
- sourceId,
3231
- enumEntityDescriptor,
3232
- entityContext: enumEntityContext,
3233
- diagnostics
3234
- });
3235
- for (const [name, descriptor] of nsEnumResult.enumTypeDescriptors) allEnumTypeDescriptors.set(name, descriptor);
3236
- const nsEntries = {};
3237
- for (const [name, entry] of Object.entries(nsEnumResult.storageTypes)) if (isPostgresEnumStorageEntry(entry)) nsEntries[name] = entry;
3238
- if (Object.keys(nsEntries).length > 0) namespaceEnumStorageTypes[nsId] = nsEntries;
3239
- }
3452
+ const validEnumHandles = { ...enumResult.enumHandles };
3453
+ const enumHandlesByName = new Map(Object.entries(validEnumHandles));
3240
3454
  const namedTypeResult = resolveNamedTypeDeclarations({
3241
- declarations: input.document.ast.types?.declarations ?? [],
3455
+ declarations: [...Object.values(topLevel.scalars), ...Object.values(topLevel.typeAliases)],
3242
3456
  sourceId,
3243
3457
  enumTypeDescriptors: allEnumTypeDescriptors,
3244
3458
  scalarTypeDescriptors: input.scalarTypeDescriptors,
@@ -3248,10 +3462,7 @@ function interpretPslDocumentToSqlContract(input) {
3248
3462
  authoringContributions: input.authoringContributions,
3249
3463
  diagnostics
3250
3464
  });
3251
- const storageTypes = {
3252
- ...enumResult.storageTypes,
3253
- ...namedTypeResult.storageTypes
3254
- };
3465
+ const storageTypes = { ...namedTypeResult.storageTypes };
3255
3466
  const modelMappingsByCoordinate = buildModelMappings(modelEntries, defaultNamespaceId, diagnostics, sourceId);
3256
3467
  const modelMappings = /* @__PURE__ */ new Map();
3257
3468
  for (const mapping of modelMappingsByCoordinate.values()) modelMappings.set(mapping.model.name, mapping);
@@ -3283,7 +3494,8 @@ function interpretPslDocumentToSqlContract(input) {
3283
3494
  scalarTypeDescriptors: input.scalarTypeDescriptors,
3284
3495
  sourceId,
3285
3496
  diagnostics,
3286
- modelNamespaceIds
3497
+ modelNamespaceIds,
3498
+ ...enumHandlesByName.size > 0 ? { enumHandles: enumHandlesByName } : {}
3287
3499
  });
3288
3500
  modelNodes.push(namespaceId !== void 0 ? {
3289
3501
  ...result.modelNode,
@@ -3297,10 +3509,14 @@ function interpretPslDocumentToSqlContract(input) {
3297
3509
  crossSpaceRelationsByModel.set(model.name, [...existing, ...result.crossSpaceRelations]);
3298
3510
  }
3299
3511
  }
3300
- const { modelRelations, fkRelationsByPair } = indexFkRelations({ fkRelationMetadata });
3512
+ const { modelRelations, fkRelationsByPair, fkRelationsByDeclaringModel } = indexFkRelations({ fkRelationMetadata });
3513
+ const modelIdColumns = /* @__PURE__ */ new Map();
3514
+ for (const modelNode of modelNodes) if (modelNode.id) modelIdColumns.set(modelNode.modelName, modelNode.id.columns);
3301
3515
  applyBackrelationCandidates({
3302
3516
  backrelationCandidates,
3303
3517
  fkRelationsByPair,
3518
+ fkRelationsByDeclaringModel,
3519
+ modelIdColumns,
3304
3520
  modelRelations,
3305
3521
  diagnostics,
3306
3522
  sourceId
@@ -3329,13 +3545,13 @@ function interpretPslDocumentToSqlContract(input) {
3329
3545
  });
3330
3546
  if (diagnostics.length > 0) return notOk({
3331
3547
  summary: "PSL to SQL contract interpretation failed",
3332
- diagnostics: dedupeDiagnostics(diagnostics)
3548
+ diagnostics
3333
3549
  });
3334
3550
  const contract = buildSqlContractFromDefinition({
3335
3551
  target: input.target,
3336
3552
  ...ifDefined("extensionPacks", buildComposedExtensionPackRefs(input.target, [...composedExtensions].sort(compareStrings), input.composedExtensionPackRefs)),
3337
3553
  ...Object.keys(storageTypes).length > 0 ? { storageTypes } : {},
3338
- ...Object.keys(namespaceEnumStorageTypes).length > 0 ? { namespaceTypes: namespaceEnumStorageTypes } : {},
3554
+ ...Object.keys(validEnumHandles).length > 0 ? { enums: validEnumHandles } : {},
3339
3555
  ...ifDefined("createNamespace", input.createNamespace),
3340
3556
  models: stiColumnModelNodes.map((model) => ({
3341
3557
  ...model,
@@ -3345,7 +3561,7 @@ function interpretPslDocumentToSqlContract(input) {
3345
3561
  const modelsForPatch = {};
3346
3562
  for (const [namespaceId, namespaceSlice] of Object.entries(contract.domain.namespaces)) for (const [modelName, model] of Object.entries(namespaceSlice.models)) {
3347
3563
  const coordinate = modelCoordinateKey(namespaceId, modelName);
3348
- if (Object.hasOwn(modelsForPatch, coordinate)) throw new Error(`duplicate model "${namespaceId}.${modelName}" during PSL interpretation`);
3564
+ invariant(!Object.hasOwn(modelsForPatch, coordinate), `symbol table guarantees coordinate uniqueness; duplicate model "${namespaceId}.${modelName}" reached interpretation`);
3349
3565
  modelsForPatch[coordinate] = model;
3350
3566
  }
3351
3567
  let patchedModels = patchModelDomainFields(modelsForPatch, modelResolvedFields);
@@ -3362,6 +3578,7 @@ function interpretPslDocumentToSqlContract(input) {
3362
3578
  roots: filteredRoots,
3363
3579
  domain: { namespaces: Object.fromEntries(Object.entries(contract.domain.namespaces).map(([namespaceId, namespaceSlice]) => [namespaceId, {
3364
3580
  models: Object.fromEntries(Object.entries(namespaceSlice.models).map(([modelName, model]) => [modelName, patchedModels[modelCoordinateKey(namespaceId, modelName)] ?? model])),
3581
+ ...namespaceSlice.enum !== void 0 ? { enum: namespaceSlice.enum } : {},
3365
3582
  ...namespaceSlice.valueObjects !== void 0 ? { valueObjects: namespaceSlice.valueObjects } : {},
3366
3583
  ...namespaceId === input.target.defaultNamespaceId && Object.keys(valueObjects).length > 0 ? { valueObjects } : {}
3367
3584
  }])) }
@@ -3370,4 +3587,4 @@ function interpretPslDocumentToSqlContract(input) {
3370
3587
  //#endregion
3371
3588
  export { interpretPslDocumentToSqlContract as t };
3372
3589
 
3373
- //# sourceMappingURL=interpreter-B_KtZusL.mjs.map
3590
+ //# sourceMappingURL=interpreter-CygvamTk.mjs.map