@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.
@@ -14,8 +14,13 @@ import { crossRef } from '@prisma-next/contract/types';
14
14
  import type {
15
15
  AuthoringContributions,
16
16
  AuthoringEntityContext,
17
+ AuthoringPslBlockDescriptorNamespace,
18
+ PslExtensionBlock,
19
+ } from '@prisma-next/framework-components/authoring';
20
+ import {
21
+ instantiateAuthoringEntityType,
22
+ isAuthoringPslBlockDescriptor,
17
23
  } from '@prisma-next/framework-components/authoring';
18
- import { instantiateAuthoringEntityType } from '@prisma-next/framework-components/authoring';
19
24
  import type { CodecLookup } from '@prisma-next/framework-components/codec';
20
25
  import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';
21
26
  import type {
@@ -24,22 +29,21 @@ import type {
24
29
  MutationDefaultGeneratorDescriptor,
25
30
  } from '@prisma-next/framework-components/control';
26
31
  import type { Namespace } from '@prisma-next/framework-components/ir';
27
- import { namespacePslExtensionBlocks } from '@prisma-next/framework-components/psl-ast';
28
- import type {
29
- ParsePslDocumentResult,
30
- PslAttribute,
31
- PslCompositeType,
32
- PslExtensionBlock,
33
- PslField,
34
- PslModel,
35
- PslNamedTypeDeclaration,
36
- PslNamespace,
32
+ import {
33
+ type BlockSymbol,
34
+ type CompositeTypeSymbol,
35
+ type FieldSymbol,
36
+ keywordPslSpan,
37
+ type ModelSymbol,
38
+ type NamespaceSymbol,
39
+ nodePslSpan,
40
+ type ResolvedAttribute,
41
+ type ScalarSymbol,
42
+ type SymbolTable,
43
+ type TypeAliasSymbol,
37
44
  } from '@prisma-next/psl-parser';
38
- import type {
39
- SqlModelStorage,
40
- SqlNamespaceTablesInput,
41
- StorageTypeInstance,
42
- } from '@prisma-next/sql-contract/types';
45
+ import type { SourceFile } from '@prisma-next/psl-parser/syntax';
46
+ import type { SqlModelStorage, SqlNamespaceTablesInput } from '@prisma-next/sql-contract/types';
43
47
  import {
44
48
  buildSqlContractFromDefinition,
45
49
  type EnumTypeHandle,
@@ -51,9 +55,11 @@ import {
51
55
  type RelationNode,
52
56
  type UniqueConstraintNode,
53
57
  } from '@prisma-next/sql-contract-ts/contract-builder';
58
+ import { invariant } from '@prisma-next/utils/assertions';
54
59
  import { blindCast } from '@prisma-next/utils/casts';
55
60
  import { ifDefined } from '@prisma-next/utils/defined';
56
61
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
62
+
57
63
  import {
58
64
  findDuplicateFieldName,
59
65
  getAttribute,
@@ -70,12 +76,8 @@ import type { ColumnDescriptor } from './psl-column-resolution';
70
76
  import {
71
77
  checkUncomposedNamespace,
72
78
  getAuthoringEntity,
73
- instantiatePslTypeConstructor,
74
79
  reportUncomposedNamespace,
75
- resolveDbNativeTypeAttribute,
76
80
  resolveFieldTypeDescriptor,
77
- resolvePslTypeConstructorDescriptor,
78
- toNamedTypeFieldDescriptor,
79
81
  } from './psl-column-resolution';
80
82
  import {
81
83
  buildModelMappings,
@@ -85,6 +87,7 @@ import {
85
87
  modelCoordinateKey,
86
88
  type ResolvedField,
87
89
  } from './psl-field-resolution';
90
+ import { resolveNamedTypeDeclarations } from './psl-named-type-resolution';
88
91
  import {
89
92
  applyBackrelationCandidates,
90
93
  type FkRelationMetadata,
@@ -95,8 +98,12 @@ import {
95
98
  validateNavigationListFieldAttributes,
96
99
  } from './psl-relation-resolution';
97
100
 
101
+ type NamedTypeSymbol = ScalarSymbol | TypeAliasSymbol;
102
+
98
103
  export interface InterpretPslDocumentToSqlContractInput {
99
- readonly document: ParsePslDocumentResult;
104
+ readonly symbolTable: SymbolTable;
105
+ readonly sourceFile: SourceFile;
106
+ readonly sourceId: string;
100
107
  readonly target: TargetPackRef<'sql', string>;
101
108
  readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
102
109
  readonly composedExtensionPacks?: readonly string[];
@@ -124,6 +131,7 @@ export interface InterpretPslDocumentToSqlContractInput {
124
131
  */
125
132
  readonly createNamespace?: (input: SqlNamespaceTablesInput) => Namespace;
126
133
  readonly codecLookup?: CodecLookup;
134
+ readonly seedDiagnostics?: readonly ContractSourceDiagnostic[];
127
135
  }
128
136
 
129
137
  function buildComposedExtensionPackRefs(
@@ -152,27 +160,6 @@ function buildComposedExtensionPackRefs(
152
160
  );
153
161
  }
154
162
 
155
- function diagnosticDedupKey(diagnostic: ContractSourceDiagnostic): string {
156
- const span = diagnostic.span;
157
- const spanKey = span
158
- ? `${span.start.offset}:${span.end.offset}:${span.start.line}:${span.end.line}`
159
- : '';
160
- return `${diagnostic.code}\u0000${diagnostic.sourceId}\u0000${spanKey}\u0000${diagnostic.message}`;
161
- }
162
-
163
- function dedupeDiagnostics(
164
- diagnostics: readonly ContractSourceDiagnostic[],
165
- ): ContractSourceDiagnostic[] {
166
- const seen = new Map<string, ContractSourceDiagnostic>();
167
- for (const diagnostic of diagnostics) {
168
- const key = diagnosticDedupKey(diagnostic);
169
- if (!seen.has(key)) {
170
- seen.set(key, diagnostic);
171
- }
172
- }
173
- return [...seen.values()];
174
- }
175
-
176
163
  function compareStrings(left: string, right: string): -1 | 0 | 1 {
177
164
  if (left < right) {
178
165
  return -1;
@@ -183,15 +170,6 @@ function compareStrings(left: string, right: string): -1 | 0 | 1 {
183
170
  return 0;
184
171
  }
185
172
 
186
- function mapParserDiagnostics(document: ParsePslDocumentResult): ContractSourceDiagnostic[] {
187
- return document.diagnostics.map((diagnostic) => ({
188
- code: diagnostic.code,
189
- message: diagnostic.message,
190
- sourceId: diagnostic.sourceId,
191
- span: diagnostic.span,
192
- }));
193
- }
194
-
195
173
  /**
196
174
  * Name of the framework-parser synthesised bucket for top-level
197
175
  * declarations. Re-declared here so the per-target dispatch does not
@@ -263,39 +241,38 @@ function resolveNamespaceIdForSqlTarget(input: {
263
241
  }
264
242
 
265
243
  function validateNamespaceBlocksForSqlTarget(input: {
266
- readonly namespaces: readonly PslNamespace[];
244
+ readonly namespaces: readonly NamespaceSymbol[];
267
245
  readonly targetId: string;
268
246
  readonly sourceId: string;
247
+ readonly sourceFile: SourceFile;
269
248
  readonly diagnostics: ContractSourceDiagnostic[];
270
249
  }): void {
271
250
  if (input.targetId === 'sqlite') {
272
251
  for (const namespace of input.namespaces) {
273
- if (namespace.name === UNSPECIFIED_PSL_NAMESPACE_NAME) {
274
- continue;
275
- }
276
252
  input.diagnostics.push({
277
253
  code: 'PSL_UNSUPPORTED_NAMESPACE_BLOCK',
278
254
  message: `SQLite does not support \`namespace ${namespace.name} { … }\` blocks (SQLite has no schema concept; declare models at the document top level instead).`,
279
255
  sourceId: input.sourceId,
280
- span: namespace.span,
256
+ span: nodePslSpan(namespace.node.syntax, input.sourceFile),
281
257
  });
282
258
  }
283
259
  return;
284
260
  }
285
261
 
286
262
  if (input.targetId === 'postgres') {
287
- const namedBlocks = input.namespaces.filter((ns) => ns.name !== UNSPECIFIED_PSL_NAMESPACE_NAME);
288
- const hasUnbound = namedBlocks.some((ns) => ns.name === 'unbound');
289
- const hasSibling = namedBlocks.some((ns) => ns.name !== 'unbound');
263
+ const hasUnbound = input.namespaces.some((ns) => ns.name === 'unbound');
264
+ const hasSibling = input.namespaces.some((ns) => ns.name !== 'unbound');
290
265
  if (hasUnbound && hasSibling) {
291
- const unboundBlock = namedBlocks.find((ns) => ns.name === 'unbound');
266
+ const unboundBlock = input.namespaces.find((ns) => ns.name === 'unbound');
292
267
  input.diagnostics.push({
293
268
  code: 'PSL_RESERVED_NAMESPACE_NAME',
294
269
  message:
295
270
  'Namespace "unbound" is reserved for the late-binding sentinel mapping and cannot appear alongside other named namespace blocks. ' +
296
271
  'Use `namespace unbound { … }` alone (no sibling named namespaces) for late-binding multi-tenant contracts.',
297
272
  sourceId: input.sourceId,
298
- ...ifDefined('span', unboundBlock?.span),
273
+ ...(unboundBlock !== undefined
274
+ ? { span: nodePslSpan(unboundBlock.node.syntax, input.sourceFile) }
275
+ : {}),
299
276
  });
300
277
  }
301
278
  }
@@ -354,225 +331,23 @@ function processEnumDeclarations(input: ProcessEnumDeclarationsInput): {
354
331
  return { enumHandles, enumTypeDescriptors };
355
332
  }
356
333
 
357
- interface ResolveNamedTypeDeclarationsInput {
358
- readonly declarations: readonly PslNamedTypeDeclaration[];
359
- readonly sourceId: string;
360
- readonly enumTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
361
- readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
362
- readonly composedExtensions: ReadonlySet<string>;
363
- readonly familyId: string;
364
- readonly targetId: string;
365
- readonly authoringContributions: AuthoringContributions | undefined;
366
- readonly diagnostics: ContractSourceDiagnostic[];
367
- }
368
-
369
- function validateNamedTypeAttributes(input: {
370
- readonly declaration: PslNamedTypeDeclaration;
371
- readonly sourceId: string;
372
- readonly diagnostics: ContractSourceDiagnostic[];
373
- readonly composedExtensions: ReadonlySet<string>;
374
- readonly authoringContributions: AuthoringContributions | undefined;
375
- readonly allowDbNativeType: boolean;
376
- readonly familyId: string;
377
- readonly targetId: string;
378
- }): {
379
- readonly dbNativeTypeAttribute: PslAttribute | undefined;
380
- readonly hasUnsupportedNamedTypeAttribute: boolean;
381
- } {
382
- const dbNativeTypeAttributes = input.allowDbNativeType
383
- ? input.declaration.attributes.filter((attribute) => attribute.name.startsWith('db.'))
384
- : [];
385
- const [dbNativeTypeAttribute, ...extraDbNativeTypeAttributes] = dbNativeTypeAttributes;
386
- let hasUnsupportedNamedTypeAttribute = false;
387
-
388
- for (const extra of extraDbNativeTypeAttributes) {
389
- input.diagnostics.push({
390
- code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
391
- message: `Named type "${input.declaration.name}" can declare at most one @db.* attribute`,
392
- sourceId: input.sourceId,
393
- span: extra.span,
394
- });
395
- hasUnsupportedNamedTypeAttribute = true;
396
- }
397
-
398
- for (const attribute of input.declaration.attributes) {
399
- if (input.allowDbNativeType && attribute.name.startsWith('db.')) {
400
- continue;
334
+ /** Generic top-level blocks are supported only when a composed descriptor claims their keyword. */
335
+ function composedBlockKeywords(
336
+ authoringContributions: AuthoringContributions | undefined,
337
+ ): ReadonlySet<string> {
338
+ const keywords = new Set<string>();
339
+ const descriptors: AuthoringPslBlockDescriptorNamespace =
340
+ authoringContributions?.pslBlockDescriptors ?? {};
341
+ for (const [keyword, value] of Object.entries(descriptors)) {
342
+ if (isAuthoringPslBlockDescriptor(value)) {
343
+ keywords.add(keyword);
401
344
  }
402
-
403
- const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
404
- familyId: input.familyId,
405
- targetId: input.targetId,
406
- authoringContributions: input.authoringContributions,
407
- });
408
- if (uncomposedNamespace) {
409
- reportUncomposedNamespace({
410
- subjectLabel: `Attribute "@${attribute.name}"`,
411
- namespace: uncomposedNamespace,
412
- sourceId: input.sourceId,
413
- span: attribute.span,
414
- diagnostics: input.diagnostics,
415
- });
416
- hasUnsupportedNamedTypeAttribute = true;
417
- continue;
418
- }
419
-
420
- input.diagnostics.push({
421
- code: 'PSL_UNSUPPORTED_NAMED_TYPE_ATTRIBUTE',
422
- message: `Named type "${input.declaration.name}" uses unsupported attribute "${attribute.name}"`,
423
- sourceId: input.sourceId,
424
- span: attribute.span,
425
- });
426
- hasUnsupportedNamedTypeAttribute = true;
427
345
  }
428
-
429
- return { dbNativeTypeAttribute, hasUnsupportedNamedTypeAttribute };
430
- }
431
-
432
- function resolveNamedTypeDeclarations(input: ResolveNamedTypeDeclarationsInput): {
433
- readonly storageTypes: Record<string, StorageTypeInstance>;
434
- readonly namedTypeDescriptors: Map<string, ColumnDescriptor>;
435
- } {
436
- const storageTypes: Record<string, StorageTypeInstance> = {};
437
- const namedTypeDescriptors = new Map<string, ColumnDescriptor>();
438
-
439
- for (const declaration of input.declarations) {
440
- if (declaration.typeConstructor) {
441
- const { hasUnsupportedNamedTypeAttribute } = validateNamedTypeAttributes({
442
- declaration,
443
- sourceId: input.sourceId,
444
- diagnostics: input.diagnostics,
445
- composedExtensions: input.composedExtensions,
446
- authoringContributions: input.authoringContributions,
447
- allowDbNativeType: false,
448
- familyId: input.familyId,
449
- targetId: input.targetId,
450
- });
451
- if (hasUnsupportedNamedTypeAttribute) {
452
- continue;
453
- }
454
-
455
- const helperPath = declaration.typeConstructor.path.join('.');
456
- const typeConstructor = resolvePslTypeConstructorDescriptor({
457
- call: declaration.typeConstructor,
458
- authoringContributions: input.authoringContributions,
459
- composedExtensions: input.composedExtensions,
460
- familyId: input.familyId,
461
- targetId: input.targetId,
462
- diagnostics: input.diagnostics,
463
- sourceId: input.sourceId,
464
- unsupportedCode: 'PSL_UNSUPPORTED_NAMED_TYPE_CONSTRUCTOR',
465
- unsupportedMessage: `Named type "${declaration.name}" references unsupported constructor "${helperPath}"`,
466
- });
467
- if (!typeConstructor) {
468
- continue;
469
- }
470
-
471
- const storageType = instantiatePslTypeConstructor({
472
- call: declaration.typeConstructor,
473
- descriptor: typeConstructor,
474
- diagnostics: input.diagnostics,
475
- sourceId: input.sourceId,
476
- entityLabel: `Named type "${declaration.name}"`,
477
- });
478
- if (!storageType) {
479
- continue;
480
- }
481
-
482
- namedTypeDescriptors.set(
483
- declaration.name,
484
- toNamedTypeFieldDescriptor(declaration.name, storageType),
485
- );
486
- storageTypes[declaration.name] = {
487
- kind: 'codec-instance',
488
- codecId: storageType.codecId,
489
- nativeType: storageType.nativeType,
490
- typeParams: storageType.typeParams ?? {},
491
- };
492
- continue;
493
- }
494
-
495
- // Parser invariant: when typeConstructor is absent, baseType is defined.
496
- // The check below narrows `baseType` for TypeScript and guards against a
497
- // parser regression; it is unreachable under a correct parser.
498
- if (declaration.baseType === undefined) {
499
- input.diagnostics.push({
500
- code: 'PSL_UNSUPPORTED_NAMED_TYPE_BASE',
501
- message: `Named type "${declaration.name}" must declare a base type or constructor`,
502
- sourceId: input.sourceId,
503
- span: declaration.span,
504
- });
505
- continue;
506
- }
507
- const { baseType } = declaration;
508
- const baseDescriptor =
509
- input.enumTypeDescriptors.get(baseType) ?? input.scalarTypeDescriptors.get(baseType);
510
- if (!baseDescriptor) {
511
- input.diagnostics.push({
512
- code: 'PSL_UNSUPPORTED_NAMED_TYPE_BASE',
513
- message: `Named type "${declaration.name}" references unsupported base type "${baseType}"`,
514
- sourceId: input.sourceId,
515
- span: declaration.span,
516
- });
517
- continue;
518
- }
519
-
520
- const { dbNativeTypeAttribute, hasUnsupportedNamedTypeAttribute } = validateNamedTypeAttributes(
521
- {
522
- declaration,
523
- sourceId: input.sourceId,
524
- diagnostics: input.diagnostics,
525
- composedExtensions: input.composedExtensions,
526
- authoringContributions: input.authoringContributions,
527
- allowDbNativeType: true,
528
- familyId: input.familyId,
529
- targetId: input.targetId,
530
- },
531
- );
532
- if (hasUnsupportedNamedTypeAttribute) {
533
- continue;
534
- }
535
-
536
- if (dbNativeTypeAttribute) {
537
- const descriptor = resolveDbNativeTypeAttribute({
538
- attribute: dbNativeTypeAttribute,
539
- baseType,
540
- baseDescriptor,
541
- diagnostics: input.diagnostics,
542
- sourceId: input.sourceId,
543
- entityLabel: `Named type "${declaration.name}"`,
544
- });
545
- if (!descriptor) {
546
- continue;
547
- }
548
- namedTypeDescriptors.set(
549
- declaration.name,
550
- toNamedTypeFieldDescriptor(declaration.name, descriptor),
551
- );
552
- storageTypes[declaration.name] = {
553
- kind: 'codec-instance',
554
- codecId: descriptor.codecId,
555
- nativeType: descriptor.nativeType,
556
- typeParams: descriptor.typeParams ?? {},
557
- };
558
- continue;
559
- }
560
-
561
- const descriptor = toNamedTypeFieldDescriptor(declaration.name, baseDescriptor);
562
- namedTypeDescriptors.set(declaration.name, descriptor);
563
- storageTypes[declaration.name] = {
564
- kind: 'codec-instance',
565
- codecId: baseDescriptor.codecId,
566
- nativeType: baseDescriptor.nativeType,
567
- typeParams: {},
568
- };
569
- }
570
-
571
- return { storageTypes, namedTypeDescriptors };
346
+ return keywords;
572
347
  }
573
348
 
574
349
  interface BuildModelNodeInput {
575
- readonly model: PslModel;
350
+ readonly model: ModelSymbol;
576
351
  readonly mapping: ModelNameMapping;
577
352
  readonly modelMappings: ReadonlyMap<string, ModelNameMapping>;
578
353
  /**
@@ -655,7 +430,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
655
430
  let controlPolicy: ControlPolicy | undefined;
656
431
 
657
432
  const resultBackrelationCandidates: ModelBackrelationCandidate[] = [];
658
- for (const field of model.fields) {
433
+ for (const field of Object.values(model.fields)) {
659
434
  if (!field.list || !input.modelNames.has(field.typeName)) {
660
435
  continue;
661
436
  }
@@ -715,12 +490,12 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
715
490
  });
716
491
  }
717
492
 
718
- const relationAttributes = model.fields
493
+ const relationAttributes = Object.values(model.fields)
719
494
  .map((field) => ({
720
495
  field,
721
496
  relation: getAttribute(field.attributes, 'relation'),
722
497
  }))
723
- .filter((entry): entry is { field: PslField; relation: PslAttribute } =>
498
+ .filter((entry): entry is { field: FieldSymbol; relation: ResolvedAttribute } =>
724
499
  Boolean(entry.relation),
725
500
  );
726
501
  const uniqueConstraints: UniqueConstraintNode[] = resolvedFields
@@ -801,9 +576,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
801
576
  });
802
577
  continue;
803
578
  }
804
- const nullableFieldName = fieldNames.find(
805
- (name) => model.fields.find((f) => f.name === name)?.optional === true,
806
- );
579
+ const nullableFieldName = fieldNames.find((name) => model.fields[name]?.optional === true);
807
580
  if (nullableFieldName !== undefined) {
808
581
  diagnostics.push({
809
582
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
@@ -1288,6 +1061,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
1288
1061
  declaringModelName: model.name,
1289
1062
  declaringFieldName: relationAttribute.field.name,
1290
1063
  declaringTableName: tableName,
1064
+ ...ifDefined('declaringNamespaceId', input.modelNamespaceIds.get(model.name)),
1291
1065
  targetModelName: targetMapping.model.name,
1292
1066
  targetTableName: targetMapping.tableName,
1293
1067
  ...ifDefined('targetNamespaceId', targetNamespaceId),
@@ -1307,7 +1081,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
1307
1081
  fieldName: resolvedField.field.name,
1308
1082
  columnName: resolvedField.columnName,
1309
1083
  descriptor: resolvedField.descriptor,
1310
- nullable: resolvedField.field.optional,
1084
+ nullable: resolvedField.nullable,
1311
1085
  ...ifDefined('default', resolvedField.defaultValue),
1312
1086
  ...ifDefined('executionDefaults', resolvedField.executionDefaults),
1313
1087
  ...ifDefined('enumTypeHandle', enumHandle),
@@ -1327,7 +1101,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
1327
1101
  }
1328
1102
 
1329
1103
  interface BuildValueObjectsInput {
1330
- readonly compositeTypes: readonly PslCompositeType[];
1104
+ readonly compositeTypes: readonly CompositeTypeSymbol[];
1331
1105
  readonly enumTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
1332
1106
  readonly namedTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
1333
1107
  readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
@@ -1357,7 +1131,7 @@ function buildValueObjects(input: BuildValueObjectsInput): Record<string, Contra
1357
1131
 
1358
1132
  for (const compositeType of compositeTypes) {
1359
1133
  const fields: Record<string, ContractField> = {};
1360
- for (const field of compositeType.fields) {
1134
+ for (const field of Object.values(compositeType.fields)) {
1361
1135
  if (compositeTypeNames.has(field.typeName)) {
1362
1136
  const result: ContractField = {
1363
1137
  type: { kind: 'valueObject', name: field.typeName },
@@ -1453,7 +1227,7 @@ type BaseDeclaration = {
1453
1227
  };
1454
1228
 
1455
1229
  function collectPolymorphismDeclarations(
1456
- models: readonly PslModel[],
1230
+ models: readonly ModelSymbol[],
1457
1231
  sourceId: string,
1458
1232
  diagnostics: ContractSourceDiagnostic[],
1459
1233
  ): {
@@ -1476,7 +1250,7 @@ function collectPolymorphismDeclarations(
1476
1250
  });
1477
1251
  continue;
1478
1252
  }
1479
- const discField = model.fields.find((f) => f.name === fieldName);
1253
+ const discField = model.fields[fieldName];
1480
1254
  if (discField && discField.typeName !== 'String') {
1481
1255
  diagnostics.push({
1482
1256
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
@@ -1853,7 +1627,7 @@ function stripStorageOnlyDomainFields(
1853
1627
  export function interpretPslDocumentToSqlContract(
1854
1628
  input: InterpretPslDocumentToSqlContractInput,
1855
1629
  ): Result<Contract, ContractSourceDiagnostics> {
1856
- const sourceId = input.document.ast.sourceId;
1630
+ const sourceId = input.sourceId;
1857
1631
  if (!input.target) {
1858
1632
  return notOk({
1859
1633
  summary: 'PSL to SQL contract interpretation failed',
@@ -1879,38 +1653,57 @@ export function interpretPslDocumentToSqlContract(
1879
1653
  });
1880
1654
  }
1881
1655
 
1882
- const diagnostics: ContractSourceDiagnostic[] = mapParserDiagnostics(input.document);
1656
+ const { topLevel } = input.symbolTable;
1657
+ const sourceFile = input.sourceFile;
1658
+ const namespaceSymbols = Object.values(topLevel.namespaces);
1659
+ const diagnostics: ContractSourceDiagnostic[] = [...(input.seedDiagnostics ?? [])];
1883
1660
  validateNamespaceBlocksForSqlTarget({
1884
- namespaces: input.document.ast.namespaces,
1661
+ namespaces: namespaceSymbols,
1885
1662
  targetId: input.target.targetId,
1886
1663
  sourceId,
1664
+ sourceFile,
1887
1665
  diagnostics,
1888
1666
  });
1889
- // Per-target namespace resolution: walk each AST bucket once,
1890
- // recording every model's resolved `namespaceId` for later threading
1891
- // into the `ModelNode` build. The resolution rules are target-local
1892
- // (see `resolveNamespaceIdForSqlTarget`); the flattened model list
1893
- // remains the input to the rest of the interpreter so non-namespace
1894
- // concerns stay structurally identical to before.
1895
- const models: PslModel[] = [];
1667
+ const models: ModelSymbol[] = [];
1896
1668
  const modelEntries: ModelNamespaceEntry[] = [];
1897
1669
  const modelNamespaceIds = new Map<string, string>();
1898
- for (const namespace of input.document.ast.namespaces) {
1670
+ const compositeTypes: CompositeTypeSymbol[] = [];
1671
+
1672
+ const collectScope = (
1673
+ bucketName: string,
1674
+ scopeModels: Iterable<ModelSymbol>,
1675
+ scopeCompositeTypes: Iterable<CompositeTypeSymbol>,
1676
+ ): void => {
1899
1677
  const resolvedNamespaceId = resolveNamespaceIdForSqlTarget({
1900
- bucketName: namespace.name,
1678
+ bucketName,
1901
1679
  targetId: input.target.targetId,
1902
1680
  });
1903
- for (const model of namespace.models) {
1681
+ for (const model of scopeModels) {
1904
1682
  models.push(model);
1905
1683
  modelEntries.push({ model, namespaceId: resolvedNamespaceId });
1906
1684
  if (resolvedNamespaceId !== undefined) {
1907
1685
  modelNamespaceIds.set(model.name, resolvedNamespaceId);
1908
1686
  }
1909
1687
  }
1688
+ for (const compositeType of scopeCompositeTypes) {
1689
+ compositeTypes.push(compositeType);
1690
+ }
1691
+ };
1692
+
1693
+ collectScope(
1694
+ UNSPECIFIED_PSL_NAMESPACE_NAME,
1695
+ Object.values(topLevel.models),
1696
+ Object.values(topLevel.compositeTypes),
1697
+ );
1698
+ for (const namespace of namespaceSymbols) {
1699
+ collectScope(
1700
+ namespace.name,
1701
+ Object.values(namespace.models),
1702
+ Object.values(namespace.compositeTypes),
1703
+ );
1910
1704
  }
1911
1705
  const defaultNamespaceId = input.target.defaultNamespaceId;
1912
1706
 
1913
- const compositeTypes = input.document.ast.namespaces.flatMap((ns) => ns.compositeTypes);
1914
1707
  const modelNames = new Set(models.map((model) => model.name));
1915
1708
  const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
1916
1709
  const composedExtensions = new Set(input.composedExtensionPacks ?? []);
@@ -1924,20 +1717,40 @@ export function interpretPslDocumentToSqlContract(
1924
1717
  generatorDescriptorById.set(descriptor.id, descriptor);
1925
1718
  }
1926
1719
 
1927
- const topLevelEnums = input.document.ast.namespaces
1928
- .filter((ns) => ns.name === UNSPECIFIED_PSL_NAMESPACE_NAME)
1929
- .flatMap((ns) => namespacePslExtensionBlocks(ns).filter((b) => b.kind === 'enum'));
1930
- for (const ns of input.document.ast.namespaces) {
1931
- if (ns.name === UNSPECIFIED_PSL_NAMESPACE_NAME) continue;
1932
- const nsEnums = namespacePslExtensionBlocks(ns).filter((b) => b.kind === 'enum');
1933
- if (nsEnums.length === 0) continue;
1934
- for (const decl of nsEnums) {
1935
- diagnostics.push({
1936
- code: 'PSL_ENUM_NAMESPACE_NOT_SUPPORTED',
1937
- message: `enum "${decl.name}" inside namespace "${ns.name}" is not supported; declare enum at the top level`,
1938
- sourceId,
1939
- span: decl.span,
1940
- });
1720
+ const isEnumBlock = (block: BlockSymbol): boolean => block.keyword === 'enum';
1721
+ const legitimateBlockKeywords = composedBlockKeywords(input.authoringContributions);
1722
+ const reportUnsupportedTopLevelBlock = (block: BlockSymbol): void => {
1723
+ diagnostics.push({
1724
+ code: 'PSL_UNSUPPORTED_TOP_LEVEL_BLOCK',
1725
+ message: `Unsupported top-level block "${block.keyword}"`,
1726
+ sourceId,
1727
+ span: keywordPslSpan(block.node.syntax, block.keyword, sourceFile),
1728
+ });
1729
+ };
1730
+
1731
+ const topLevelEnums = Object.values(topLevel.blocks)
1732
+ .filter((block) => {
1733
+ if (!legitimateBlockKeywords.has(block.keyword)) {
1734
+ reportUnsupportedTopLevelBlock(block);
1735
+ return false;
1736
+ }
1737
+ return isEnumBlock(block);
1738
+ })
1739
+ .map((block) => block.block);
1740
+ for (const namespace of namespaceSymbols) {
1741
+ for (const block of Object.values(namespace.blocks)) {
1742
+ if (isEnumBlock(block)) {
1743
+ diagnostics.push({
1744
+ code: 'PSL_ENUM_NAMESPACE_NOT_SUPPORTED',
1745
+ message: `enum "${block.name}" inside namespace "${namespace.name}" is not supported; declare enum at the top level`,
1746
+ sourceId,
1747
+ span: nodePslSpan(block.node.syntax, sourceFile),
1748
+ });
1749
+ continue;
1750
+ }
1751
+ if (!legitimateBlockKeywords.has(block.keyword)) {
1752
+ reportUnsupportedTopLevelBlock(block);
1753
+ }
1941
1754
  }
1942
1755
  }
1943
1756
 
@@ -1967,8 +1780,13 @@ export function interpretPslDocumentToSqlContract(
1967
1780
 
1968
1781
  const enumHandlesByName = new Map(Object.entries(validEnumHandles));
1969
1782
 
1783
+ const namedTypeSymbols: readonly NamedTypeSymbol[] = [
1784
+ ...Object.values(topLevel.scalars),
1785
+ ...Object.values(topLevel.typeAliases),
1786
+ ];
1787
+
1970
1788
  const namedTypeResult = resolveNamedTypeDeclarations({
1971
- declarations: input.document.ast.types?.declarations ?? [],
1789
+ declarations: namedTypeSymbols,
1972
1790
  sourceId,
1973
1791
  enumTypeDescriptors: allEnumTypeDescriptors,
1974
1792
  scalarTypeDescriptors: input.scalarTypeDescriptors,
@@ -2043,10 +1861,20 @@ export function interpretPslDocumentToSqlContract(
2043
1861
  }
2044
1862
  }
2045
1863
 
2046
- const { modelRelations, fkRelationsByPair } = indexFkRelations({ fkRelationMetadata });
1864
+ const { modelRelations, fkRelationsByPair, fkRelationsByDeclaringModel } = indexFkRelations({
1865
+ fkRelationMetadata,
1866
+ });
1867
+ const modelIdColumns = new Map<string, readonly string[]>();
1868
+ for (const modelNode of modelNodes) {
1869
+ if (modelNode.id) {
1870
+ modelIdColumns.set(modelNode.modelName, modelNode.id.columns);
1871
+ }
1872
+ }
2047
1873
  applyBackrelationCandidates({
2048
1874
  backrelationCandidates,
2049
1875
  fkRelationsByPair,
1876
+ fkRelationsByDeclaringModel,
1877
+ modelIdColumns,
2050
1878
  modelRelations,
2051
1879
  diagnostics,
2052
1880
  sourceId,
@@ -2106,7 +1934,7 @@ export function interpretPslDocumentToSqlContract(
2106
1934
  if (diagnostics.length > 0) {
2107
1935
  return notOk({
2108
1936
  summary: 'PSL to SQL contract interpretation failed',
2109
- diagnostics: dedupeDiagnostics(diagnostics),
1937
+ diagnostics,
2110
1938
  });
2111
1939
  }
2112
1940
 
@@ -2135,16 +1963,16 @@ export function interpretPslDocumentToSqlContract(
2135
1963
  })),
2136
1964
  });
2137
1965
 
2138
- // Keyed by `(namespaceId, modelName)` coordinate so two models that share a
2139
- // bare name across namespaces stay distinct through the patch/polymorphism
2140
- // passes; only a genuine same-namespace duplicate is an error.
1966
+ // Include namespace in patch keys so same bare model names across namespaces
1967
+ // stay distinct; same-coordinate duplicates were already collapsed first-wins.
2141
1968
  const modelsForPatch: Record<string, ContractModel> = {};
2142
1969
  for (const [namespaceId, namespaceSlice] of Object.entries(contract.domain.namespaces)) {
2143
1970
  for (const [modelName, model] of Object.entries(namespaceSlice.models)) {
2144
1971
  const coordinate = modelCoordinateKey(namespaceId, modelName);
2145
- if (Object.hasOwn(modelsForPatch, coordinate)) {
2146
- throw new Error(`duplicate model "${namespaceId}.${modelName}" during PSL interpretation`);
2147
- }
1972
+ invariant(
1973
+ !Object.hasOwn(modelsForPatch, coordinate),
1974
+ `symbol table guarantees coordinate uniqueness; duplicate model "${namespaceId}.${modelName}" reached interpretation`,
1975
+ );
2148
1976
  modelsForPatch[coordinate] = model;
2149
1977
  }
2150
1978
  }