@prisma-next/sql-contract-psl 0.14.0-dev.1 → 0.14.0-dev.11

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 { namespacePslExtensionBlocks } from "@prisma-next/framework-components/psl-ast";
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,
@@ -1301,7 +1307,6 @@ 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);
@@ -1309,8 +1314,18 @@ function resolveColumnDescriptor(field, enumTypeDescriptors, namedTypeDescriptor
1309
1314
  //#endregion
1310
1315
  //#region src/psl-field-resolution.ts
1311
1316
  function lowerEnumDefaultForField(input) {
1312
- const expressionEntry = getPositionalArgumentEntry(input.defaultAttribute);
1313
- if (!expressionEntry) return {};
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
+ }
1314
1329
  const raw = expressionEntry.value.trim();
1315
1330
  const isQuotedString = /^(['"]).*\1$/.test(raw);
1316
1331
  const isFunctionCall = raw.includes("(") && raw.endsWith(")");
@@ -1414,7 +1429,7 @@ function extractFieldConstraintNames(input) {
1414
1429
  function collectResolvedFields(input) {
1415
1430
  const { model, mapping, enumTypeDescriptors, namedTypeDescriptors, modelNames, compositeTypeNames, composedExtensions, authoringContributions, familyId, targetId, defaultFunctionRegistry, generatorDescriptorById, diagnostics, sourceId, scalarTypeDescriptors, enumHandles } = input;
1416
1431
  const resolvedFields = [];
1417
- for (const field of model.fields) {
1432
+ for (const field of Object.values(model.fields)) {
1418
1433
  const isModelField = modelNames.has(field.typeName);
1419
1434
  if (field.list && isModelField) continue;
1420
1435
  validateFieldAttributes({
@@ -1534,7 +1549,7 @@ function collectResolvedFields(input) {
1534
1549
  });
1535
1550
  continue;
1536
1551
  }
1537
- const fieldUsesNamedType = field.typeRef !== void 0 || namedTypeDescriptors.has(field.typeName);
1552
+ const fieldUsesNamedType = namedTypeDescriptors.has(field.typeName);
1538
1553
  if (loweredOnCreate && !fieldUsesNamedType) {
1539
1554
  const generatedDescriptor = generatorDescriptorById.get(loweredOnCreate.id)?.resolveGeneratedColumnDescriptor?.({ generated: loweredOnCreate });
1540
1555
  if (generatedDescriptor) descriptor = generatedDescriptor;
@@ -1571,6 +1586,7 @@ function collectResolvedFields(input) {
1571
1586
  field,
1572
1587
  columnName: mappedColumnName,
1573
1588
  descriptor,
1589
+ nullable: presetContributions?.nullable ?? field.optional,
1574
1590
  ...ifDefined("defaultValue", fieldDefaultValue),
1575
1591
  ...ifDefined("executionDefaults", fieldExecutionDefaults),
1576
1592
  isId: isIdField || Boolean(presetContributions?.id),
@@ -1596,7 +1612,7 @@ function buildModelMappings(modelEntries, defaultNamespaceId, diagnostics, sourc
1596
1612
  span: model.span
1597
1613
  });
1598
1614
  const fieldColumns = /* @__PURE__ */ new Map();
1599
- for (const field of model.fields) {
1615
+ for (const field of Object.values(model.fields)) {
1600
1616
  const columnName = parseMapName({
1601
1617
  attribute: getAttribute(field.attributes, "map"),
1602
1618
  defaultValue: field.name,
@@ -1616,6 +1632,171 @@ function buildModelMappings(modelEntries, defaultNamespaceId, diagnostics, sourc
1616
1632
  return result;
1617
1633
  }
1618
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
1619
1800
  //#region src/psl-relation-resolution.ts
1620
1801
  const REFERENTIAL_ACTION_MAP = {
1621
1802
  NoAction: "noAction",
@@ -1753,7 +1934,11 @@ function parseRelationAttribute(input) {
1753
1934
  function indexFkRelations(input) {
1754
1935
  const modelRelations = /* @__PURE__ */ new Map();
1755
1936
  const fkRelationsByPair = /* @__PURE__ */ new Map();
1937
+ const fkRelationsByDeclaringModel = /* @__PURE__ */ new Map();
1756
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]);
1757
1942
  const existing = modelRelations.get(relation.declaringModelName);
1758
1943
  const current = existing ?? [];
1759
1944
  if (!existing) modelRelations.set(relation.declaringModelName, current);
@@ -1780,8 +1965,146 @@ function indexFkRelations(input) {
1780
1965
  }
1781
1966
  return {
1782
1967
  modelRelations,
1783
- 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
1784
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
2079
+ };
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;
1785
2108
  }
1786
2109
  function applyBackrelationCandidates(input) {
1787
2110
  for (const candidate of input.backrelationCandidates) {
@@ -1789,6 +2112,30 @@ function applyBackrelationCandidates(input) {
1789
2112
  const pairMatches = input.fkRelationsByPair.get(pairKey) ?? [];
1790
2113
  const matches = candidate.relationName ? pairMatches.filter((relation) => relation.relationName === candidate.relationName) : [...pairMatches];
1791
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
+ }
1792
2139
  input.diagnostics.push({
1793
2140
  code: "PSL_ORPHANED_BACKRELATION_LIST",
1794
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.`,
@@ -1809,13 +2156,11 @@ function applyBackrelationCandidates(input) {
1809
2156
  invariant(matches.length === 1, "Backrelation matching requires exactly one match");
1810
2157
  const matched = matches[0];
1811
2158
  assertDefined(matched, "Backrelation matching requires a defined relation match");
1812
- const existing = input.modelRelations.get(candidate.modelName);
1813
- const current = existing ?? [];
1814
- if (!existing) input.modelRelations.set(candidate.modelName, current);
1815
- current.push({
2159
+ relationsForModel(input.modelRelations, candidate.modelName).push({
1816
2160
  fieldName: candidate.field.name,
1817
2161
  toModel: matched.declaringModelName,
1818
2162
  toTable: matched.declaringTableName,
2163
+ ...ifDefined("toNamespaceId", matched.declaringNamespaceId),
1819
2164
  cardinality: "1:N",
1820
2165
  on: {
1821
2166
  parentTable: candidate.tableName,
@@ -1869,32 +2214,11 @@ function buildComposedExtensionPackRefs(target, extensionIds, extensionPackRefs
1869
2214
  version: "0.0.1"
1870
2215
  }]));
1871
2216
  }
1872
- function diagnosticDedupKey(diagnostic) {
1873
- const span = diagnostic.span;
1874
- const spanKey = span ? `${span.start.offset}:${span.end.offset}:${span.start.line}:${span.end.line}` : "";
1875
- return `${diagnostic.code}\u0000${diagnostic.sourceId}\u0000${spanKey}\u0000${diagnostic.message}`;
1876
- }
1877
- function dedupeDiagnostics(diagnostics) {
1878
- const seen = /* @__PURE__ */ new Map();
1879
- for (const diagnostic of diagnostics) {
1880
- const key = diagnosticDedupKey(diagnostic);
1881
- if (!seen.has(key)) seen.set(key, diagnostic);
1882
- }
1883
- return [...seen.values()];
1884
- }
1885
2217
  function compareStrings(left, right) {
1886
2218
  if (left < right) return -1;
1887
2219
  if (left > right) return 1;
1888
2220
  return 0;
1889
2221
  }
1890
- function mapParserDiagnostics(document) {
1891
- return document.diagnostics.map((diagnostic) => ({
1892
- code: diagnostic.code,
1893
- message: diagnostic.message,
1894
- sourceId: diagnostic.sourceId,
1895
- span: diagnostic.span
1896
- }));
1897
- }
1898
2222
  /**
1899
2223
  * Name of the framework-parser synthesised bucket for top-level
1900
2224
  * declarations. Re-declared here so the per-target dispatch does not
@@ -1956,28 +2280,24 @@ function resolveNamespaceIdForSqlTarget(input) {
1956
2280
  }
1957
2281
  function validateNamespaceBlocksForSqlTarget(input) {
1958
2282
  if (input.targetId === "sqlite") {
1959
- for (const namespace of input.namespaces) {
1960
- if (namespace.name === UNSPECIFIED_PSL_NAMESPACE_NAME) continue;
1961
- input.diagnostics.push({
1962
- code: "PSL_UNSUPPORTED_NAMESPACE_BLOCK",
1963
- message: `SQLite does not support \`namespace ${namespace.name} { … }\` blocks (SQLite has no schema concept; declare models at the document top level instead).`,
1964
- sourceId: input.sourceId,
1965
- span: namespace.span
1966
- });
1967
- }
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
+ });
1968
2289
  return;
1969
2290
  }
1970
2291
  if (input.targetId === "postgres") {
1971
- const namedBlocks = input.namespaces.filter((ns) => ns.name !== UNSPECIFIED_PSL_NAMESPACE_NAME);
1972
- const hasUnbound = namedBlocks.some((ns) => ns.name === "unbound");
1973
- 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");
1974
2294
  if (hasUnbound && hasSibling) {
1975
- const unboundBlock = namedBlocks.find((ns) => ns.name === "unbound");
2295
+ const unboundBlock = input.namespaces.find((ns) => ns.name === "unbound");
1976
2296
  input.diagnostics.push({
1977
2297
  code: "PSL_RESERVED_NAMESPACE_NAME",
1978
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.",
1979
2299
  sourceId: input.sourceId,
1980
- ...ifDefined("span", unboundBlock?.span)
2300
+ ...unboundBlock !== void 0 ? { span: nodePslSpan(unboundBlock.node.syntax, input.sourceFile) } : {}
1981
2301
  });
1982
2302
  }
1983
2303
  }
@@ -2017,158 +2337,12 @@ function processEnumDeclarations(input) {
2017
2337
  enumTypeDescriptors
2018
2338
  };
2019
2339
  }
2020
- function validateNamedTypeAttributes(input) {
2021
- const [dbNativeTypeAttribute, ...extraDbNativeTypeAttributes] = input.allowDbNativeType ? input.declaration.attributes.filter((attribute) => attribute.name.startsWith("db.")) : [];
2022
- let hasUnsupportedNamedTypeAttribute = false;
2023
- for (const extra of extraDbNativeTypeAttributes) {
2024
- input.diagnostics.push({
2025
- code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
2026
- message: `Named type "${input.declaration.name}" can declare at most one @db.* attribute`,
2027
- sourceId: input.sourceId,
2028
- span: extra.span
2029
- });
2030
- hasUnsupportedNamedTypeAttribute = true;
2031
- }
2032
- for (const attribute of input.declaration.attributes) {
2033
- if (input.allowDbNativeType && attribute.name.startsWith("db.")) continue;
2034
- const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
2035
- familyId: input.familyId,
2036
- targetId: input.targetId,
2037
- authoringContributions: input.authoringContributions
2038
- });
2039
- if (uncomposedNamespace) {
2040
- reportUncomposedNamespace({
2041
- subjectLabel: `Attribute "@${attribute.name}"`,
2042
- namespace: uncomposedNamespace,
2043
- sourceId: input.sourceId,
2044
- span: attribute.span,
2045
- diagnostics: input.diagnostics
2046
- });
2047
- hasUnsupportedNamedTypeAttribute = true;
2048
- continue;
2049
- }
2050
- input.diagnostics.push({
2051
- code: "PSL_UNSUPPORTED_NAMED_TYPE_ATTRIBUTE",
2052
- message: `Named type "${input.declaration.name}" uses unsupported attribute "${attribute.name}"`,
2053
- sourceId: input.sourceId,
2054
- span: attribute.span
2055
- });
2056
- hasUnsupportedNamedTypeAttribute = true;
2057
- }
2058
- return {
2059
- dbNativeTypeAttribute,
2060
- hasUnsupportedNamedTypeAttribute
2061
- };
2062
- }
2063
- function resolveNamedTypeDeclarations(input) {
2064
- const storageTypes = {};
2065
- const namedTypeDescriptors = /* @__PURE__ */ new Map();
2066
- for (const declaration of input.declarations) {
2067
- if (declaration.typeConstructor) {
2068
- const { hasUnsupportedNamedTypeAttribute } = validateNamedTypeAttributes({
2069
- declaration,
2070
- sourceId: input.sourceId,
2071
- diagnostics: input.diagnostics,
2072
- composedExtensions: input.composedExtensions,
2073
- authoringContributions: input.authoringContributions,
2074
- allowDbNativeType: false,
2075
- familyId: input.familyId,
2076
- targetId: input.targetId
2077
- });
2078
- if (hasUnsupportedNamedTypeAttribute) continue;
2079
- const helperPath = declaration.typeConstructor.path.join(".");
2080
- const typeConstructor = resolvePslTypeConstructorDescriptor({
2081
- call: declaration.typeConstructor,
2082
- authoringContributions: input.authoringContributions,
2083
- composedExtensions: input.composedExtensions,
2084
- familyId: input.familyId,
2085
- targetId: input.targetId,
2086
- diagnostics: input.diagnostics,
2087
- sourceId: input.sourceId,
2088
- unsupportedCode: "PSL_UNSUPPORTED_NAMED_TYPE_CONSTRUCTOR",
2089
- unsupportedMessage: `Named type "${declaration.name}" references unsupported constructor "${helperPath}"`
2090
- });
2091
- if (!typeConstructor) continue;
2092
- const storageType = instantiatePslTypeConstructor({
2093
- call: declaration.typeConstructor,
2094
- descriptor: typeConstructor,
2095
- diagnostics: input.diagnostics,
2096
- sourceId: input.sourceId,
2097
- entityLabel: `Named type "${declaration.name}"`
2098
- });
2099
- if (!storageType) continue;
2100
- namedTypeDescriptors.set(declaration.name, toNamedTypeFieldDescriptor(declaration.name, storageType));
2101
- storageTypes[declaration.name] = {
2102
- kind: "codec-instance",
2103
- codecId: storageType.codecId,
2104
- nativeType: storageType.nativeType,
2105
- typeParams: storageType.typeParams ?? {}
2106
- };
2107
- continue;
2108
- }
2109
- if (declaration.baseType === void 0) {
2110
- input.diagnostics.push({
2111
- code: "PSL_UNSUPPORTED_NAMED_TYPE_BASE",
2112
- message: `Named type "${declaration.name}" must declare a base type or constructor`,
2113
- sourceId: input.sourceId,
2114
- span: declaration.span
2115
- });
2116
- continue;
2117
- }
2118
- const { baseType } = declaration;
2119
- const baseDescriptor = input.enumTypeDescriptors.get(baseType) ?? input.scalarTypeDescriptors.get(baseType);
2120
- if (!baseDescriptor) {
2121
- input.diagnostics.push({
2122
- code: "PSL_UNSUPPORTED_NAMED_TYPE_BASE",
2123
- message: `Named type "${declaration.name}" references unsupported base type "${baseType}"`,
2124
- sourceId: input.sourceId,
2125
- span: declaration.span
2126
- });
2127
- continue;
2128
- }
2129
- const { dbNativeTypeAttribute, hasUnsupportedNamedTypeAttribute } = validateNamedTypeAttributes({
2130
- declaration,
2131
- sourceId: input.sourceId,
2132
- diagnostics: input.diagnostics,
2133
- composedExtensions: input.composedExtensions,
2134
- authoringContributions: input.authoringContributions,
2135
- allowDbNativeType: true,
2136
- familyId: input.familyId,
2137
- targetId: input.targetId
2138
- });
2139
- if (hasUnsupportedNamedTypeAttribute) continue;
2140
- if (dbNativeTypeAttribute) {
2141
- const descriptor = resolveDbNativeTypeAttribute({
2142
- attribute: dbNativeTypeAttribute,
2143
- baseType,
2144
- baseDescriptor,
2145
- diagnostics: input.diagnostics,
2146
- sourceId: input.sourceId,
2147
- entityLabel: `Named type "${declaration.name}"`
2148
- });
2149
- if (!descriptor) continue;
2150
- namedTypeDescriptors.set(declaration.name, toNamedTypeFieldDescriptor(declaration.name, descriptor));
2151
- storageTypes[declaration.name] = {
2152
- kind: "codec-instance",
2153
- codecId: descriptor.codecId,
2154
- nativeType: descriptor.nativeType,
2155
- typeParams: descriptor.typeParams ?? {}
2156
- };
2157
- continue;
2158
- }
2159
- const descriptor = toNamedTypeFieldDescriptor(declaration.name, baseDescriptor);
2160
- namedTypeDescriptors.set(declaration.name, descriptor);
2161
- storageTypes[declaration.name] = {
2162
- kind: "codec-instance",
2163
- codecId: baseDescriptor.codecId,
2164
- nativeType: baseDescriptor.nativeType,
2165
- typeParams: {}
2166
- };
2167
- }
2168
- return {
2169
- storageTypes,
2170
- namedTypeDescriptors
2171
- };
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;
2172
2346
  }
2173
2347
  function buildModelNodeFromPsl(input) {
2174
2348
  const { model, mapping, sourceId, diagnostics } = input;
@@ -2208,7 +2382,7 @@ function buildModelNodeFromPsl(input) {
2208
2382
  let controlPolicyDeclared = false;
2209
2383
  let controlPolicy;
2210
2384
  const resultBackrelationCandidates = [];
2211
- for (const field of model.fields) {
2385
+ for (const field of Object.values(model.fields)) {
2212
2386
  if (!field.list || !input.modelNames.has(field.typeName)) continue;
2213
2387
  const attributesValid = validateNavigationListFieldAttributes({
2214
2388
  modelName: model.name,
@@ -2260,7 +2434,7 @@ function buildModelNodeFromPsl(input) {
2260
2434
  ...ifDefined("relationName", relationName)
2261
2435
  });
2262
2436
  }
2263
- const relationAttributes = model.fields.map((field) => ({
2437
+ const relationAttributes = Object.values(model.fields).map((field) => ({
2264
2438
  field,
2265
2439
  relation: getAttribute(field.attributes, "relation")
2266
2440
  })).filter((entry) => Boolean(entry.relation));
@@ -2331,7 +2505,7 @@ function buildModelNodeFromPsl(input) {
2331
2505
  });
2332
2506
  continue;
2333
2507
  }
2334
- 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);
2335
2509
  if (nullableFieldName !== void 0) {
2336
2510
  diagnostics.push({
2337
2511
  code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
@@ -2718,6 +2892,7 @@ function buildModelNodeFromPsl(input) {
2718
2892
  declaringModelName: model.name,
2719
2893
  declaringFieldName: relationAttribute.field.name,
2720
2894
  declaringTableName: tableName,
2895
+ ...ifDefined("declaringNamespaceId", input.modelNamespaceIds.get(model.name)),
2721
2896
  targetModelName: targetMapping.model.name,
2722
2897
  targetTableName: targetMapping.tableName,
2723
2898
  ...ifDefined("targetNamespaceId", targetNamespaceId),
@@ -2736,7 +2911,7 @@ function buildModelNodeFromPsl(input) {
2736
2911
  fieldName: resolvedField.field.name,
2737
2912
  columnName: resolvedField.columnName,
2738
2913
  descriptor: resolvedField.descriptor,
2739
- nullable: resolvedField.field.optional,
2914
+ nullable: resolvedField.nullable,
2740
2915
  ...ifDefined("default", resolvedField.defaultValue),
2741
2916
  ...ifDefined("executionDefaults", resolvedField.executionDefaults),
2742
2917
  ...ifDefined("enumTypeHandle", enumHandle)
@@ -2760,7 +2935,7 @@ function buildValueObjects(input) {
2760
2935
  const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
2761
2936
  for (const compositeType of compositeTypes) {
2762
2937
  const fields = {};
2763
- for (const field of compositeType.fields) {
2938
+ for (const field of Object.values(compositeType.fields)) {
2764
2939
  if (compositeTypeNames.has(field.typeName)) {
2765
2940
  const result = {
2766
2941
  type: {
@@ -2866,7 +3041,7 @@ function collectPolymorphismDeclarations(models, sourceId, diagnostics) {
2866
3041
  });
2867
3042
  continue;
2868
3043
  }
2869
- const discField = model.fields.find((f) => f.name === fieldName);
3044
+ const discField = model.fields[fieldName];
2870
3045
  if (discField && discField.typeName !== "String") {
2871
3046
  diagnostics.push({
2872
3047
  code: "PSL_INVALID_ATTRIBUTE_ARGUMENT",
@@ -3171,7 +3346,7 @@ function stripStorageOnlyDomainFields(model, fieldNames) {
3171
3346
  };
3172
3347
  }
3173
3348
  function interpretPslDocumentToSqlContract(input) {
3174
- const sourceId = input.document.ast.sourceId;
3349
+ const sourceId = input.sourceId;
3175
3350
  if (!input.target) return notOk({
3176
3351
  summary: "PSL to SQL contract interpretation failed",
3177
3352
  diagnostics: [{
@@ -3188,22 +3363,27 @@ function interpretPslDocumentToSqlContract(input) {
3188
3363
  sourceId
3189
3364
  }]
3190
3365
  });
3191
- 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 ?? []];
3192
3370
  validateNamespaceBlocksForSqlTarget({
3193
- namespaces: input.document.ast.namespaces,
3371
+ namespaces: namespaceSymbols,
3194
3372
  targetId: input.target.targetId,
3195
3373
  sourceId,
3374
+ sourceFile,
3196
3375
  diagnostics
3197
3376
  });
3198
3377
  const models = [];
3199
3378
  const modelEntries = [];
3200
3379
  const modelNamespaceIds = /* @__PURE__ */ new Map();
3201
- for (const namespace of input.document.ast.namespaces) {
3380
+ const compositeTypes = [];
3381
+ const collectScope = (bucketName, scopeModels, scopeCompositeTypes) => {
3202
3382
  const resolvedNamespaceId = resolveNamespaceIdForSqlTarget({
3203
- bucketName: namespace.name,
3383
+ bucketName,
3204
3384
  targetId: input.target.targetId
3205
3385
  });
3206
- for (const model of namespace.models) {
3386
+ for (const model of scopeModels) {
3207
3387
  models.push(model);
3208
3388
  modelEntries.push({
3209
3389
  model,
@@ -3211,9 +3391,11 @@ function interpretPslDocumentToSqlContract(input) {
3211
3391
  });
3212
3392
  if (resolvedNamespaceId !== void 0) modelNamespaceIds.set(model.name, resolvedNamespaceId);
3213
3393
  }
3214
- }
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));
3215
3398
  const defaultNamespaceId = input.target.defaultNamespaceId;
3216
- const compositeTypes = input.document.ast.namespaces.flatMap((ns) => ns.compositeTypes);
3217
3399
  const modelNames = new Set(models.map((model) => model.name));
3218
3400
  const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
3219
3401
  const composedExtensions = new Set(input.composedExtensionPacks ?? []);
@@ -3222,17 +3404,34 @@ function interpretPslDocumentToSqlContract(input) {
3222
3404
  const generatorDescriptors = input.controlMutationDefaults?.generatorDescriptors ?? [];
3223
3405
  const generatorDescriptorById = /* @__PURE__ */ new Map();
3224
3406
  for (const descriptor of generatorDescriptors) generatorDescriptorById.set(descriptor.id, descriptor);
3225
- const topLevelEnums = input.document.ast.namespaces.filter((ns) => ns.name === UNSPECIFIED_PSL_NAMESPACE_NAME).flatMap((ns) => namespacePslExtensionBlocks(ns).filter((b) => b.kind === "enum"));
3226
- for (const ns of input.document.ast.namespaces) {
3227
- if (ns.name === UNSPECIFIED_PSL_NAMESPACE_NAME) continue;
3228
- const nsEnums = namespacePslExtensionBlocks(ns).filter((b) => b.kind === "enum");
3229
- if (nsEnums.length === 0) continue;
3230
- for (const decl of nsEnums) diagnostics.push({
3231
- code: "PSL_ENUM_NAMESPACE_NOT_SUPPORTED",
3232
- message: `enum "${decl.name}" inside namespace "${ns.name}" is not supported; declare enum at the top level`,
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}"`,
3233
3413
  sourceId,
3234
- span: decl.span
3414
+ span: keywordPslSpan(block.node.syntax, block.keyword, sourceFile)
3235
3415
  });
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);
3236
3435
  }
3237
3436
  const enumResult = processEnumDeclarations({
3238
3437
  enumBlocks: topLevelEnums,
@@ -3253,7 +3452,7 @@ function interpretPslDocumentToSqlContract(input) {
3253
3452
  const validEnumHandles = { ...enumResult.enumHandles };
3254
3453
  const enumHandlesByName = new Map(Object.entries(validEnumHandles));
3255
3454
  const namedTypeResult = resolveNamedTypeDeclarations({
3256
- declarations: input.document.ast.types?.declarations ?? [],
3455
+ declarations: [...Object.values(topLevel.scalars), ...Object.values(topLevel.typeAliases)],
3257
3456
  sourceId,
3258
3457
  enumTypeDescriptors: allEnumTypeDescriptors,
3259
3458
  scalarTypeDescriptors: input.scalarTypeDescriptors,
@@ -3310,10 +3509,14 @@ function interpretPslDocumentToSqlContract(input) {
3310
3509
  crossSpaceRelationsByModel.set(model.name, [...existing, ...result.crossSpaceRelations]);
3311
3510
  }
3312
3511
  }
3313
- 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);
3314
3515
  applyBackrelationCandidates({
3315
3516
  backrelationCandidates,
3316
3517
  fkRelationsByPair,
3518
+ fkRelationsByDeclaringModel,
3519
+ modelIdColumns,
3317
3520
  modelRelations,
3318
3521
  diagnostics,
3319
3522
  sourceId
@@ -3342,7 +3545,7 @@ function interpretPslDocumentToSqlContract(input) {
3342
3545
  });
3343
3546
  if (diagnostics.length > 0) return notOk({
3344
3547
  summary: "PSL to SQL contract interpretation failed",
3345
- diagnostics: dedupeDiagnostics(diagnostics)
3548
+ diagnostics
3346
3549
  });
3347
3550
  const contract = buildSqlContractFromDefinition({
3348
3551
  target: input.target,
@@ -3358,7 +3561,7 @@ function interpretPslDocumentToSqlContract(input) {
3358
3561
  const modelsForPatch = {};
3359
3562
  for (const [namespaceId, namespaceSlice] of Object.entries(contract.domain.namespaces)) for (const [modelName, model] of Object.entries(namespaceSlice.models)) {
3360
3563
  const coordinate = modelCoordinateKey(namespaceId, modelName);
3361
- 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`);
3362
3565
  modelsForPatch[coordinate] = model;
3363
3566
  }
3364
3567
  let patchedModels = patchModelDomainFields(modelsForPatch, modelResolvedFields);
@@ -3384,4 +3587,4 @@ function interpretPslDocumentToSqlContract(input) {
3384
3587
  //#endregion
3385
3588
  export { interpretPslDocumentToSqlContract as t };
3386
3589
 
3387
- //# sourceMappingURL=interpreter-B0BsCLKT.mjs.map
3590
+ //# sourceMappingURL=interpreter-CygvamTk.mjs.map