@prisma-next/sql-contract-psl 0.3.0-dev.128 → 0.3.0-dev.146

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.
@@ -3,1464 +3,219 @@ import type {
3
3
  ContractSourceDiagnosticSpan,
4
4
  ContractSourceDiagnostics,
5
5
  } from '@prisma-next/config/config-types';
6
- import type { TargetPackRef } from '@prisma-next/contract/framework-components';
7
- import type { ContractIR } from '@prisma-next/contract/ir';
8
- import type { ColumnDefault, ExecutionMutationDefaultValue } from '@prisma-next/contract/types';
6
+ import type {
7
+ Contract,
8
+ ContractField,
9
+ ContractModel,
10
+ ContractValueObject,
11
+ } from '@prisma-next/contract/types';
12
+ import type {
13
+ AuthoringContributions,
14
+ AuthoringTypeConstructorDescriptor,
15
+ } from '@prisma-next/framework-components/authoring';
16
+ import { instantiateAuthoringTypeConstructor } from '@prisma-next/framework-components/authoring';
17
+ import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';
9
18
  import type {
10
19
  ParsePslDocumentResult,
11
20
  PslAttribute,
21
+ PslCompositeType,
22
+ PslEnum,
12
23
  PslField,
13
24
  PslModel,
25
+ PslNamedTypeDeclaration,
14
26
  PslSpan,
15
27
  } from '@prisma-next/psl-parser';
16
- import { defineContract } from '@prisma-next/sql-contract-ts/contract-builder';
17
- import { assertDefined, invariant } from '@prisma-next/utils/assertions';
28
+ import type { StorageTypeInstance } from '@prisma-next/sql-contract/types';
29
+ import {
30
+ buildSqlContractFromDefinition,
31
+ type ForeignKeyNode,
32
+ type IndexNode,
33
+ type ModelNode,
34
+ type UniqueConstraintNode,
35
+ } from '@prisma-next/sql-contract-ts/contract-builder';
18
36
  import { ifDefined } from '@prisma-next/utils/defined';
19
37
  import { notOk, ok, type Result } from '@prisma-next/utils/result';
20
- import {
21
- type ControlMutationDefaultRegistry,
22
- type ControlMutationDefaults,
23
- lowerDefaultFunctionWithRegistry,
24
- type MutationDefaultGeneratorDescriptor,
25
- parseDefaultFunctionCall,
38
+ import type {
39
+ ControlMutationDefaultRegistry,
40
+ ControlMutationDefaults,
41
+ MutationDefaultGeneratorDescriptor,
26
42
  } from './default-function-registry';
27
-
28
- type ColumnDescriptor = {
29
- readonly codecId: string;
30
- readonly nativeType: string;
31
- readonly typeRef?: string;
32
- readonly typeParams?: Record<string, unknown>;
33
- };
34
-
35
- export interface InterpretPslDocumentToSqlContractIRInput {
43
+ import {
44
+ getAttribute,
45
+ getPositionalArgument,
46
+ mapFieldNamesToColumns,
47
+ parseAttributeFieldList,
48
+ parseConstraintMapArgument,
49
+ parseMapName,
50
+ parseQuotedStringLiteral,
51
+ } from './psl-attribute-parsing';
52
+ import type { ColumnDescriptor } from './psl-column-resolution';
53
+ import {
54
+ getAuthoringTypeConstructor,
55
+ parsePgvectorLength,
56
+ resolveColumnDescriptor,
57
+ resolveDbNativeTypeAttribute,
58
+ toNamedTypeFieldDescriptor,
59
+ } from './psl-column-resolution';
60
+ import {
61
+ buildModelMappings,
62
+ collectResolvedFields,
63
+ type ModelNameMapping,
64
+ type ResolvedField,
65
+ } from './psl-field-resolution';
66
+ import {
67
+ applyBackrelationCandidates,
68
+ type FkRelationMetadata,
69
+ indexFkRelations,
70
+ type ModelBackrelationCandidate,
71
+ normalizeReferentialAction,
72
+ parseRelationAttribute,
73
+ validateNavigationListFieldAttributes,
74
+ } from './psl-relation-resolution';
75
+
76
+ export interface InterpretPslDocumentToSqlContractInput {
36
77
  readonly document: ParsePslDocumentResult;
37
78
  readonly target: TargetPackRef<'sql', 'postgres'>;
38
79
  readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
39
80
  readonly composedExtensionPacks?: readonly string[];
81
+ readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', 'postgres'>[];
40
82
  readonly controlMutationDefaults?: ControlMutationDefaults;
41
- }
42
-
43
- const REFERENTIAL_ACTION_MAP = {
44
- NoAction: 'noAction',
45
- Restrict: 'restrict',
46
- Cascade: 'cascade',
47
- SetNull: 'setNull',
48
- SetDefault: 'setDefault',
49
- noAction: 'noAction',
50
- restrict: 'restrict',
51
- cascade: 'cascade',
52
- setNull: 'setNull',
53
- setDefault: 'setDefault',
54
- } as const;
55
-
56
- type ResolvedField = {
57
- readonly field: PslField;
58
- readonly columnName: string;
59
- readonly descriptor: ColumnDescriptor;
60
- readonly defaultValue?: ColumnDefault;
61
- readonly executionDefault?: ExecutionMutationDefaultValue;
62
- readonly isId: boolean;
63
- readonly isUnique: boolean;
64
- };
65
-
66
- type ParsedRelationAttribute = {
67
- readonly relationName?: string;
68
- readonly fields?: readonly string[];
69
- readonly references?: readonly string[];
70
- readonly constraintName?: string;
71
- readonly onDelete?: string;
72
- readonly onUpdate?: string;
73
- };
74
-
75
- type FkRelationMetadata = {
76
- readonly declaringModelName: string;
77
- readonly declaringFieldName: string;
78
- readonly declaringTableName: string;
79
- readonly targetModelName: string;
80
- readonly targetTableName: string;
81
- readonly relationName?: string;
82
- readonly localColumns: readonly string[];
83
- readonly referencedColumns: readonly string[];
84
- };
85
-
86
- type ModelBackrelationCandidate = {
87
- readonly modelName: string;
88
- readonly tableName: string;
89
- readonly field: PslField;
90
- readonly targetModelName: string;
91
- readonly relationName?: string;
92
- };
93
-
94
- type ModelRelationMetadata = {
95
- readonly fieldName: string;
96
- readonly toModel: string;
97
- readonly toTable: string;
98
- readonly cardinality: '1:N' | 'N:1';
99
- readonly parentTable: string;
100
- readonly parentColumns: readonly string[];
101
- readonly childTable: string;
102
- readonly childColumns: readonly string[];
103
- };
104
-
105
- type ResolvedModelEntry = {
106
- readonly model: PslModel;
107
- readonly mapping: ModelNameMapping;
108
- readonly resolvedFields: readonly ResolvedField[];
109
- };
110
-
111
- function fkRelationPairKey(declaringModelName: string, targetModelName: string): string {
112
- // NOTE: We assume PSL model identifiers do not contain the `::` separator.
113
- return `${declaringModelName}::${targetModelName}`;
114
- }
115
-
116
- type ModelNameMapping = {
117
- readonly model: PslModel;
118
- readonly tableName: string;
119
- readonly fieldColumns: Map<string, string>;
120
- };
121
-
122
- type DynamicTableBuilder = {
123
- column(
124
- name: string,
125
- options: { type: ColumnDescriptor; nullable?: true; default?: ColumnDefault },
126
- ): DynamicTableBuilder;
127
- generated(
128
- name: string,
129
- options: { type: ColumnDescriptor; generated: ExecutionMutationDefaultValue },
130
- ): DynamicTableBuilder;
131
- unique(columns: readonly string[]): DynamicTableBuilder;
132
- primaryKey(columns: readonly string[]): DynamicTableBuilder;
133
- index(columns: readonly string[]): DynamicTableBuilder;
134
- foreignKey(
135
- columns: readonly string[],
136
- references: { table: string; columns: readonly string[] },
137
- options?: { name?: string; onDelete?: string; onUpdate?: string },
138
- ): DynamicTableBuilder;
139
- };
140
-
141
- type DynamicModelBuilder = {
142
- field(name: string, column: string): DynamicModelBuilder;
143
- relation(
144
- name: string,
145
- options: {
146
- toModel: string;
147
- toTable: string;
148
- cardinality: '1:1' | '1:N' | 'N:1';
149
- on: {
150
- parentTable: string;
151
- parentColumns: readonly string[];
152
- childTable: string;
153
- childColumns: readonly string[];
154
- };
155
- },
156
- ): DynamicModelBuilder;
157
- };
158
-
159
- type DynamicContractBuilder = {
160
- target(target: TargetPackRef<'sql', 'postgres'>): DynamicContractBuilder;
161
- storageType(
162
- name: string,
163
- typeInstance: {
164
- codecId: string;
165
- nativeType: string;
166
- typeParams: Record<string, unknown>;
167
- },
168
- ): DynamicContractBuilder;
169
- table(
170
- name: string,
171
- callback: (tableBuilder: DynamicTableBuilder) => DynamicTableBuilder,
172
- ): DynamicContractBuilder;
173
- model(
174
- name: string,
175
- table: string,
176
- callback: (modelBuilder: DynamicModelBuilder) => DynamicModelBuilder,
177
- ): DynamicContractBuilder;
178
- build(): ContractIR;
179
- };
180
-
181
- function lowerFirst(value: string): string {
182
- if (value.length === 0) return value;
183
- return value[0]?.toLowerCase() + value.slice(1);
184
- }
185
-
186
- function getAttribute(
187
- attributes: readonly PslAttribute[] | undefined,
188
- name: string,
189
- ): PslAttribute | undefined {
190
- return attributes?.find((attribute) => attribute.name === name);
191
- }
192
-
193
- function getNamedArgument(attribute: PslAttribute, name: string): string | undefined {
194
- const entry = attribute.args.find((arg) => arg.kind === 'named' && arg.name === name);
195
- if (!entry || entry.kind !== 'named') {
196
- return undefined;
197
- }
198
- return entry.value;
199
- }
200
-
201
- function getPositionalArgument(attribute: PslAttribute, index = 0): string | undefined {
202
- const entries = attribute.args.filter((arg) => arg.kind === 'positional');
203
- const entry = entries[index];
204
- if (!entry || entry.kind !== 'positional') {
205
- return undefined;
206
- }
207
- return entry.value;
208
- }
209
-
210
- function getPositionalArgumentEntry(
211
- attribute: PslAttribute,
212
- index = 0,
213
- ): { value: string; span: PslSpan } | undefined {
214
- const entries = attribute.args.filter((arg) => arg.kind === 'positional');
215
- const entry = entries[index];
216
- if (!entry || entry.kind !== 'positional') {
217
- return undefined;
218
- }
219
- return {
220
- value: entry.value,
221
- span: entry.span,
222
- };
223
- }
224
-
225
- function unquoteStringLiteral(value: string): string {
226
- const trimmed = value.trim();
227
- const match = trimmed.match(/^(['"])(.*)\1$/);
228
- if (!match) {
229
- return trimmed;
230
- }
231
- return match[2] ?? '';
232
- }
233
-
234
- function parseQuotedStringLiteral(value: string): string | undefined {
235
- const trimmed = value.trim();
236
- // This intentionally accepts either '...' or "..." and relies on PSL's
237
- // own string literal rules to disallow unescaped interior delimiters.
238
- const match = trimmed.match(/^(['"])(.*)\1$/);
239
- if (!match) {
240
- return undefined;
241
- }
242
- return match[2] ?? '';
243
- }
244
-
245
- function parseFieldList(value: string): readonly string[] | undefined {
246
- const trimmed = value.trim();
247
- if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {
248
- return undefined;
249
- }
250
- const body = trimmed.slice(1, -1);
251
- const parts = body
252
- .split(',')
253
- .map((entry) => entry.trim())
254
- .filter((entry) => entry.length > 0);
255
- return parts;
256
- }
257
-
258
- function parseDefaultLiteralValue(expression: string): ColumnDefault | undefined {
259
- const trimmed = expression.trim();
260
- if (trimmed === 'true' || trimmed === 'false') {
261
- return { kind: 'literal', value: trimmed === 'true' };
262
- }
263
- const numericValue = Number(trimmed);
264
- if (!Number.isNaN(numericValue) && trimmed.length > 0 && !/^(['"]).*\1$/.test(trimmed)) {
265
- return { kind: 'literal', value: numericValue };
266
- }
267
- if (/^(['"]).*\1$/.test(trimmed)) {
268
- return { kind: 'literal', value: unquoteStringLiteral(trimmed) };
269
- }
270
- return undefined;
271
- }
272
-
273
- function lowerDefaultForField(input: {
274
- readonly modelName: string;
275
- readonly fieldName: string;
276
- readonly defaultAttribute: PslAttribute;
277
- readonly columnDescriptor: ColumnDescriptor;
278
- readonly generatorDescriptorById: ReadonlyMap<string, MutationDefaultGeneratorDescriptor>;
279
- readonly sourceId: string;
280
- readonly defaultFunctionRegistry: ControlMutationDefaultRegistry;
281
- readonly diagnostics: ContractSourceDiagnostic[];
282
- }): {
283
- readonly defaultValue?: ColumnDefault;
284
- readonly executionDefault?: ExecutionMutationDefaultValue;
285
- } {
286
- const positionalEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'positional');
287
- const namedEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'named');
288
-
289
- if (namedEntries.length > 0 || positionalEntries.length !== 1) {
290
- input.diagnostics.push({
291
- code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
292
- message: `Field "${input.modelName}.${input.fieldName}" requires exactly one positional @default(...) expression.`,
293
- sourceId: input.sourceId,
294
- span: input.defaultAttribute.span,
295
- });
296
- return {};
297
- }
298
-
299
- const expressionEntry = getPositionalArgumentEntry(input.defaultAttribute);
300
- if (!expressionEntry) {
301
- input.diagnostics.push({
302
- code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
303
- message: `Field "${input.modelName}.${input.fieldName}" requires a positional @default(...) expression.`,
304
- sourceId: input.sourceId,
305
- span: input.defaultAttribute.span,
306
- });
307
- return {};
308
- }
309
-
310
- const literalDefault = parseDefaultLiteralValue(expressionEntry.value);
311
- if (literalDefault) {
312
- return { defaultValue: literalDefault };
313
- }
314
-
315
- const defaultFunctionCall = parseDefaultFunctionCall(expressionEntry.value, expressionEntry.span);
316
- if (!defaultFunctionCall) {
317
- input.diagnostics.push({
318
- code: 'PSL_INVALID_DEFAULT_VALUE',
319
- message: `Unsupported default value "${expressionEntry.value}"`,
320
- sourceId: input.sourceId,
321
- span: input.defaultAttribute.span,
322
- });
323
- return {};
324
- }
325
-
326
- const lowered = lowerDefaultFunctionWithRegistry({
327
- call: defaultFunctionCall,
328
- registry: input.defaultFunctionRegistry,
329
- context: {
330
- sourceId: input.sourceId,
331
- modelName: input.modelName,
332
- fieldName: input.fieldName,
333
- columnCodecId: input.columnDescriptor.codecId,
334
- },
335
- });
336
-
337
- if (!lowered.ok) {
338
- input.diagnostics.push(lowered.diagnostic);
339
- return {};
340
- }
341
-
342
- if (lowered.value.kind === 'storage') {
343
- return { defaultValue: lowered.value.defaultValue };
344
- }
345
-
346
- const generatorDescriptor = input.generatorDescriptorById.get(lowered.value.generated.id);
347
- if (!generatorDescriptor) {
348
- input.diagnostics.push({
349
- code: 'PSL_INVALID_DEFAULT_APPLICABILITY',
350
- message: `Default generator "${lowered.value.generated.id}" is not available in the composed mutation default registry.`,
351
- sourceId: input.sourceId,
352
- span: expressionEntry.span,
353
- });
354
- return {};
355
- }
356
-
357
- if (!generatorDescriptor.applicableCodecIds.includes(input.columnDescriptor.codecId)) {
358
- input.diagnostics.push({
359
- code: 'PSL_INVALID_DEFAULT_APPLICABILITY',
360
- message: `Default generator "${generatorDescriptor.id}" is not applicable to "${input.modelName}.${input.fieldName}" with codecId "${input.columnDescriptor.codecId}".`,
361
- sourceId: input.sourceId,
362
- span: expressionEntry.span,
363
- });
364
- return {};
365
- }
366
-
367
- return { executionDefault: lowered.value.generated };
368
- }
369
-
370
- function parseMapName(input: {
371
- readonly attribute: PslAttribute | undefined;
372
- readonly defaultValue: string;
373
- readonly sourceId: string;
374
- readonly diagnostics: ContractSourceDiagnostic[];
375
- readonly entityLabel: string;
376
- readonly span: PslSpan;
377
- }): string {
378
- if (!input.attribute) {
379
- return input.defaultValue;
380
- }
381
-
382
- const value = getPositionalArgument(input.attribute);
383
- if (!value) {
384
- input.diagnostics.push({
385
- code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
386
- message: `${input.entityLabel} @map requires a positional quoted string literal argument`,
387
- sourceId: input.sourceId,
388
- span: input.attribute.span,
389
- });
390
- return input.defaultValue;
391
- }
392
- const parsed = parseQuotedStringLiteral(value);
393
- if (parsed === undefined) {
394
- input.diagnostics.push({
395
- code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
396
- message: `${input.entityLabel} @map requires a positional quoted string literal argument`,
397
- sourceId: input.sourceId,
398
- span: input.attribute.span,
399
- });
400
- return input.defaultValue;
401
- }
402
- return parsed;
403
- }
404
-
405
- function parsePgvectorLength(input: {
406
- readonly attribute: PslAttribute;
407
- readonly diagnostics: ContractSourceDiagnostic[];
408
- readonly sourceId: string;
409
- }): number | undefined {
410
- const namedLength = getNamedArgument(input.attribute, 'length');
411
- const namedDim = getNamedArgument(input.attribute, 'dim');
412
- const positional = getPositionalArgument(input.attribute);
413
- const raw = namedLength ?? namedDim ?? positional;
414
- if (!raw) {
415
- input.diagnostics.push({
416
- code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
417
- message: '@pgvector.column requires length/dim argument',
418
- sourceId: input.sourceId,
419
- span: input.attribute.span,
420
- });
421
- return undefined;
422
- }
423
- const parsed = Number(unquoteStringLiteral(raw));
424
- if (!Number.isInteger(parsed) || parsed < 1) {
425
- input.diagnostics.push({
426
- code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
427
- message: '@pgvector.column length/dim must be a positive integer',
428
- sourceId: input.sourceId,
429
- span: input.attribute.span,
430
- });
431
- return undefined;
432
- }
433
- return parsed;
434
- }
435
-
436
- function getPositionalArguments(attribute: PslAttribute): readonly string[] {
437
- return attribute.args
438
- .filter((arg) => arg.kind === 'positional')
439
- .map((arg) => (arg.kind === 'positional' ? arg.value : ''));
440
- }
441
-
442
- function pushInvalidAttributeArgument(input: {
443
- readonly diagnostics: ContractSourceDiagnostic[];
444
- readonly sourceId: string;
445
- readonly span: PslSpan;
446
- readonly message: string;
447
- }): undefined {
448
- input.diagnostics.push({
449
- code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
450
- message: input.message,
451
- sourceId: input.sourceId,
452
- span: input.span,
453
- });
454
- return undefined;
455
- }
456
-
457
- function parseOptionalSingleIntegerArgument(input: {
458
- readonly attribute: PslAttribute;
459
- readonly diagnostics: ContractSourceDiagnostic[];
460
- readonly sourceId: string;
461
- readonly entityLabel: string;
462
- readonly minimum: number;
463
- readonly valueLabel: string;
464
- }): number | null | undefined {
465
- if (input.attribute.args.some((arg) => arg.kind === 'named')) {
466
- return pushInvalidAttributeArgument({
467
- diagnostics: input.diagnostics,
468
- sourceId: input.sourceId,
469
- span: input.attribute.span,
470
- message: `${input.entityLabel} @${input.attribute.name} accepts zero or one positional integer argument.`,
471
- });
472
- }
473
-
474
- const positionalArguments = getPositionalArguments(input.attribute);
475
- if (positionalArguments.length > 1) {
476
- return pushInvalidAttributeArgument({
477
- diagnostics: input.diagnostics,
478
- sourceId: input.sourceId,
479
- span: input.attribute.span,
480
- message: `${input.entityLabel} @${input.attribute.name} accepts zero or one positional integer argument.`,
481
- });
482
- }
483
- if (positionalArguments.length === 0) {
484
- return null;
485
- }
486
-
487
- const parsed = Number(unquoteStringLiteral(positionalArguments[0] ?? ''));
488
- if (!Number.isInteger(parsed) || parsed < input.minimum) {
489
- return pushInvalidAttributeArgument({
490
- diagnostics: input.diagnostics,
491
- sourceId: input.sourceId,
492
- span: input.attribute.span,
493
- message: `${input.entityLabel} @${input.attribute.name} requires a ${input.valueLabel}.`,
494
- });
495
- }
496
-
497
- return parsed;
498
- }
499
-
500
- function parseOptionalNumericArguments(input: {
501
- readonly attribute: PslAttribute;
502
- readonly diagnostics: ContractSourceDiagnostic[];
503
- readonly sourceId: string;
504
- readonly entityLabel: string;
505
- }): { precision: number; scale?: number } | null | undefined {
506
- if (input.attribute.args.some((arg) => arg.kind === 'named')) {
507
- return pushInvalidAttributeArgument({
508
- diagnostics: input.diagnostics,
509
- sourceId: input.sourceId,
510
- span: input.attribute.span,
511
- message: `${input.entityLabel} @${input.attribute.name} accepts zero, one, or two positional integer arguments.`,
512
- });
513
- }
514
-
515
- const positionalArguments = getPositionalArguments(input.attribute);
516
- if (positionalArguments.length > 2) {
517
- return pushInvalidAttributeArgument({
518
- diagnostics: input.diagnostics,
519
- sourceId: input.sourceId,
520
- span: input.attribute.span,
521
- message: `${input.entityLabel} @${input.attribute.name} accepts zero, one, or two positional integer arguments.`,
522
- });
523
- }
524
- if (positionalArguments.length === 0) {
525
- return null;
526
- }
527
-
528
- const precision = Number(unquoteStringLiteral(positionalArguments[0] ?? ''));
529
- if (!Number.isInteger(precision) || precision < 1) {
530
- return pushInvalidAttributeArgument({
531
- diagnostics: input.diagnostics,
532
- sourceId: input.sourceId,
533
- span: input.attribute.span,
534
- message: `${input.entityLabel} @${input.attribute.name} requires a positive integer precision.`,
535
- });
536
- }
537
-
538
- if (positionalArguments.length === 1) {
539
- return { precision };
540
- }
541
-
542
- const scale = Number(unquoteStringLiteral(positionalArguments[1] ?? ''));
543
- if (!Number.isInteger(scale) || scale < 0) {
544
- return pushInvalidAttributeArgument({
545
- diagnostics: input.diagnostics,
546
- sourceId: input.sourceId,
547
- span: input.attribute.span,
548
- message: `${input.entityLabel} @${input.attribute.name} requires a non-negative integer scale.`,
549
- });
550
- }
551
-
552
- return { precision, scale };
553
- }
554
-
555
- /**
556
- * Declarative specification for @db.* native type attributes.
557
- *
558
- * Argument kinds:
559
- * - `noArgs`: No arguments accepted; `codecId: null` means inherit from baseDescriptor.
560
- * - `optionalLength`: Zero or one positional integer (minimum 1), stored as `{ length }`.
561
- * - `optionalPrecision`: Zero or one positional integer (minimum 0), stored as `{ precision }`.
562
- * - `optionalNumeric`: Zero, one, or two positional integers (precision + scale).
563
- */
564
- type NativeTypeSpec =
565
- | {
566
- readonly args: 'noArgs';
567
- readonly baseType: string;
568
- readonly codecId: string | null;
569
- readonly nativeType: string;
570
- }
571
- | {
572
- readonly args: 'optionalLength';
573
- readonly baseType: string;
574
- readonly codecId: string;
575
- readonly nativeType: string;
576
- }
577
- | {
578
- readonly args: 'optionalPrecision';
579
- readonly baseType: string;
580
- readonly codecId: string;
581
- readonly nativeType: string;
582
- }
583
- | {
584
- readonly args: 'optionalNumeric';
585
- readonly baseType: string;
586
- readonly codecId: string;
587
- readonly nativeType: string;
588
- };
589
-
590
- const NATIVE_TYPE_SPECS: Readonly<Record<string, NativeTypeSpec>> = {
591
- 'db.VarChar': {
592
- args: 'optionalLength',
593
- baseType: 'String',
594
- codecId: 'sql/varchar@1',
595
- nativeType: 'character varying',
596
- },
597
- 'db.Char': {
598
- args: 'optionalLength',
599
- baseType: 'String',
600
- codecId: 'sql/char@1',
601
- nativeType: 'character',
602
- },
603
- 'db.Uuid': { args: 'noArgs', baseType: 'String', codecId: null, nativeType: 'uuid' },
604
- 'db.SmallInt': { args: 'noArgs', baseType: 'Int', codecId: 'pg/int2@1', nativeType: 'int2' },
605
- 'db.Real': { args: 'noArgs', baseType: 'Float', codecId: 'pg/float4@1', nativeType: 'float4' },
606
- 'db.Numeric': {
607
- args: 'optionalNumeric',
608
- baseType: 'Decimal',
609
- codecId: 'pg/numeric@1',
610
- nativeType: 'numeric',
611
- },
612
- 'db.Timestamp': {
613
- args: 'optionalPrecision',
614
- baseType: 'DateTime',
615
- codecId: 'pg/timestamp@1',
616
- nativeType: 'timestamp',
617
- },
618
- 'db.Timestamptz': {
619
- args: 'optionalPrecision',
620
- baseType: 'DateTime',
621
- codecId: 'pg/timestamptz@1',
622
- nativeType: 'timestamptz',
623
- },
624
- 'db.Date': { args: 'noArgs', baseType: 'DateTime', codecId: null, nativeType: 'date' },
625
- 'db.Time': {
626
- args: 'optionalPrecision',
627
- baseType: 'DateTime',
628
- codecId: 'pg/time@1',
629
- nativeType: 'time',
630
- },
631
- 'db.Timetz': {
632
- args: 'optionalPrecision',
633
- baseType: 'DateTime',
634
- codecId: 'pg/timetz@1',
635
- nativeType: 'timetz',
636
- },
637
- 'db.Json': { args: 'noArgs', baseType: 'Json', codecId: 'pg/json@1', nativeType: 'json' },
638
- };
639
-
640
- function resolveDbNativeTypeAttribute(input: {
641
- readonly attribute: PslAttribute;
642
- readonly baseType: string;
643
- readonly baseDescriptor: ColumnDescriptor;
644
- readonly diagnostics: ContractSourceDiagnostic[];
645
- readonly sourceId: string;
646
- readonly entityLabel: string;
647
- }): ColumnDescriptor | undefined {
648
- const spec = NATIVE_TYPE_SPECS[input.attribute.name];
649
- if (!spec) {
650
- input.diagnostics.push({
651
- code: 'PSL_UNSUPPORTED_NAMED_TYPE_ATTRIBUTE',
652
- message: `${input.entityLabel} uses unsupported attribute "@${input.attribute.name}"`,
653
- sourceId: input.sourceId,
654
- span: input.attribute.span,
655
- });
656
- return undefined;
657
- }
658
-
659
- if (input.baseType !== spec.baseType) {
660
- return pushInvalidAttributeArgument({
661
- diagnostics: input.diagnostics,
662
- sourceId: input.sourceId,
663
- span: input.attribute.span,
664
- message: `${input.entityLabel} uses @${input.attribute.name} on unsupported base type "${input.baseType}". Expected "${spec.baseType}".`,
665
- });
666
- }
667
-
668
- switch (spec.args) {
669
- case 'noArgs': {
670
- if (getPositionalArguments(input.attribute).length > 0 || input.attribute.args.length > 0) {
671
- return pushInvalidAttributeArgument({
672
- diagnostics: input.diagnostics,
673
- sourceId: input.sourceId,
674
- span: input.attribute.span,
675
- message: `${input.entityLabel} @${input.attribute.name} does not accept arguments.`,
676
- });
677
- }
678
- return {
679
- codecId: spec.codecId ?? input.baseDescriptor.codecId,
680
- nativeType: spec.nativeType,
681
- };
682
- }
683
- case 'optionalLength': {
684
- const length = parseOptionalSingleIntegerArgument({
685
- attribute: input.attribute,
686
- diagnostics: input.diagnostics,
687
- sourceId: input.sourceId,
688
- entityLabel: input.entityLabel,
689
- minimum: 1,
690
- valueLabel: 'positive integer length',
691
- });
692
- if (length === undefined) {
693
- return undefined;
694
- }
695
- return {
696
- codecId: spec.codecId,
697
- nativeType: spec.nativeType,
698
- ...(length === null ? {} : { typeParams: { length } }),
699
- };
700
- }
701
- case 'optionalPrecision': {
702
- const precision = parseOptionalSingleIntegerArgument({
703
- attribute: input.attribute,
704
- diagnostics: input.diagnostics,
705
- sourceId: input.sourceId,
706
- entityLabel: input.entityLabel,
707
- minimum: 0,
708
- valueLabel: 'non-negative integer precision',
709
- });
710
- if (precision === undefined) {
711
- return undefined;
712
- }
713
- return {
714
- codecId: spec.codecId,
715
- nativeType: spec.nativeType,
716
- ...(precision === null ? {} : { typeParams: { precision } }),
717
- };
718
- }
719
- case 'optionalNumeric': {
720
- const numeric = parseOptionalNumericArguments({
721
- attribute: input.attribute,
722
- diagnostics: input.diagnostics,
723
- sourceId: input.sourceId,
724
- entityLabel: input.entityLabel,
725
- });
726
- if (numeric === undefined) {
727
- return undefined;
728
- }
729
- return {
730
- codecId: spec.codecId,
731
- nativeType: spec.nativeType,
732
- ...(numeric === null ? {} : { typeParams: numeric }),
733
- };
734
- }
735
- }
736
- }
737
-
738
- function resolveColumnDescriptor(
739
- field: PslField,
740
- enumTypeDescriptors: Map<string, ColumnDescriptor>,
741
- namedTypeDescriptors: Map<string, ColumnDescriptor>,
742
- scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
743
- ): ColumnDescriptor | undefined {
744
- if (field.typeRef && namedTypeDescriptors.has(field.typeRef)) {
745
- return namedTypeDescriptors.get(field.typeRef);
746
- }
747
- if (namedTypeDescriptors.has(field.typeName)) {
748
- return namedTypeDescriptors.get(field.typeName);
749
- }
750
- if (enumTypeDescriptors.has(field.typeName)) {
751
- return enumTypeDescriptors.get(field.typeName);
752
- }
753
- return scalarTypeDescriptors.get(field.typeName);
754
- }
755
-
756
- function collectResolvedFields(
757
- model: PslModel,
758
- mapping: ModelNameMapping,
759
- enumTypeDescriptors: Map<string, ColumnDescriptor>,
760
- namedTypeDescriptors: Map<string, ColumnDescriptor>,
761
- namedTypeBaseTypes: Map<string, string>,
762
- modelNames: Set<string>,
763
- composedExtensions: Set<string>,
764
- defaultFunctionRegistry: ControlMutationDefaultRegistry,
765
- generatorDescriptorById: ReadonlyMap<string, MutationDefaultGeneratorDescriptor>,
766
- diagnostics: ContractSourceDiagnostic[],
767
- sourceId: string,
768
- scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
769
- ): ResolvedField[] {
770
- const resolvedFields: ResolvedField[] = [];
771
-
772
- for (const field of model.fields) {
773
- if (field.list) {
774
- if (modelNames.has(field.typeName)) {
775
- continue;
776
- }
777
- diagnostics.push({
778
- code: 'PSL_UNSUPPORTED_FIELD_LIST',
779
- message: `Field "${model.name}.${field.name}" uses a scalar/storage list type, which is not supported in SQL PSL provider v1. Model-typed lists are only supported as backrelation navigation fields when they match an FK-side relation.`,
780
- sourceId,
781
- span: field.span,
782
- });
783
- continue;
784
- }
785
-
786
- for (const attribute of field.attributes) {
787
- if (
788
- attribute.name === 'id' ||
789
- attribute.name === 'unique' ||
790
- attribute.name === 'default' ||
791
- attribute.name === 'relation' ||
792
- attribute.name === 'map' ||
793
- attribute.name === 'pgvector.column'
794
- ) {
795
- continue;
796
- }
797
- if (attribute.name.startsWith('pgvector.') && !composedExtensions.has('pgvector')) {
798
- diagnostics.push({
799
- code: 'PSL_EXTENSION_NAMESPACE_NOT_COMPOSED',
800
- message: `Attribute "@${attribute.name}" uses unrecognized namespace "pgvector". Add extension pack "pgvector" to extensionPacks in prisma-next.config.ts.`,
801
- sourceId,
802
- span: attribute.span,
803
- });
804
- continue;
805
- }
806
- diagnostics.push({
807
- code: 'PSL_UNSUPPORTED_FIELD_ATTRIBUTE',
808
- message: `Field "${model.name}.${field.name}" uses unsupported attribute "@${attribute.name}"`,
809
- sourceId,
810
- span: attribute.span,
811
- });
812
- }
813
-
814
- const relationAttribute = getAttribute(field.attributes, 'relation');
815
- if (relationAttribute && modelNames.has(field.typeName)) {
816
- continue;
817
- }
818
-
819
- let descriptor = resolveColumnDescriptor(
820
- field,
821
- enumTypeDescriptors,
822
- namedTypeDescriptors,
823
- scalarTypeDescriptors,
824
- );
825
- const pgvectorColumnAttribute = getAttribute(field.attributes, 'pgvector.column');
826
- if (pgvectorColumnAttribute) {
827
- if (!composedExtensions.has('pgvector')) {
828
- diagnostics.push({
829
- code: 'PSL_EXTENSION_NAMESPACE_NOT_COMPOSED',
830
- message:
831
- 'Attribute "@pgvector.column" uses unrecognized namespace "pgvector". Add extension pack "pgvector" to extensionPacks in prisma-next.config.ts.',
832
- sourceId,
833
- span: pgvectorColumnAttribute.span,
834
- });
835
- } else {
836
- const isBytesBase =
837
- field.typeName === 'Bytes' ||
838
- namedTypeBaseTypes.get(field.typeRef ?? field.typeName) === 'Bytes';
839
- if (!isBytesBase) {
840
- diagnostics.push({
841
- code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
842
- message: `Field "${model.name}.${field.name}" uses @pgvector.column on unsupported base type "${field.typeName}"`,
843
- sourceId,
844
- span: pgvectorColumnAttribute.span,
845
- });
846
- } else {
847
- const length = parsePgvectorLength({
848
- attribute: pgvectorColumnAttribute,
849
- diagnostics,
850
- sourceId,
851
- });
852
- if (length !== undefined) {
853
- descriptor = {
854
- codecId: 'pg/vector@1',
855
- nativeType: 'vector',
856
- typeParams: { length },
857
- };
858
- }
859
- }
860
- }
861
- }
862
-
863
- if (!descriptor) {
864
- diagnostics.push({
865
- code: 'PSL_UNSUPPORTED_FIELD_TYPE',
866
- message: `Field "${model.name}.${field.name}" type "${field.typeName}" is not supported in SQL PSL provider v1`,
867
- sourceId,
868
- span: field.span,
869
- });
870
- continue;
871
- }
872
-
873
- const defaultAttribute = getAttribute(field.attributes, 'default');
874
- const loweredDefault = defaultAttribute
875
- ? lowerDefaultForField({
876
- modelName: model.name,
877
- fieldName: field.name,
878
- defaultAttribute,
879
- columnDescriptor: descriptor,
880
- generatorDescriptorById,
881
- sourceId,
882
- defaultFunctionRegistry,
883
- diagnostics,
884
- })
885
- : {};
886
- if (field.optional && loweredDefault.executionDefault) {
887
- const generatorDescription =
888
- loweredDefault.executionDefault.kind === 'generator'
889
- ? `"${loweredDefault.executionDefault.id}"`
890
- : 'for this field';
891
- diagnostics.push({
892
- code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
893
- message: `Field "${model.name}.${field.name}" cannot be optional when using execution default ${generatorDescription}. Remove "?" or use a storage default.`,
894
- sourceId,
895
- span: defaultAttribute?.span ?? field.span,
896
- });
897
- continue;
898
- }
899
- if (loweredDefault.executionDefault) {
900
- const generatorDescriptor = generatorDescriptorById.get(loweredDefault.executionDefault.id);
901
- const generatedDescriptor = generatorDescriptor?.resolveGeneratedColumnDescriptor?.({
902
- generated: loweredDefault.executionDefault,
903
- });
904
- if (generatedDescriptor) {
905
- descriptor = generatedDescriptor;
906
- }
907
- }
908
- const mappedColumnName = mapping.fieldColumns.get(field.name) ?? field.name;
909
- resolvedFields.push({
910
- field,
911
- columnName: mappedColumnName,
912
- descriptor,
913
- ...ifDefined('defaultValue', loweredDefault.defaultValue),
914
- ...ifDefined('executionDefault', loweredDefault.executionDefault),
915
- isId: Boolean(getAttribute(field.attributes, 'id')),
916
- isUnique: Boolean(getAttribute(field.attributes, 'unique')),
917
- });
918
- }
919
-
920
- return resolvedFields;
921
- }
922
-
923
- function hasSameSpan(a: PslSpan, b: ContractSourceDiagnosticSpan): boolean {
924
- return (
925
- a.start.offset === b.start.offset &&
926
- a.end.offset === b.end.offset &&
927
- a.start.line === b.start.line &&
928
- a.end.line === b.end.line
929
- );
930
- }
931
-
932
- function compareStrings(left: string, right: string): -1 | 0 | 1 {
933
- if (left < right) {
934
- return -1;
935
- }
936
- if (left > right) {
937
- return 1;
938
- }
939
- return 0;
940
- }
941
-
942
- function indexFkRelations(input: { readonly fkRelationMetadata: readonly FkRelationMetadata[] }): {
943
- readonly modelRelations: Map<string, ModelRelationMetadata[]>;
944
- readonly fkRelationsByPair: Map<string, FkRelationMetadata[]>;
945
- } {
946
- const modelRelations = new Map<string, ModelRelationMetadata[]>();
947
- const fkRelationsByPair = new Map<string, FkRelationMetadata[]>();
948
-
949
- for (const relation of input.fkRelationMetadata) {
950
- const existing = modelRelations.get(relation.declaringModelName);
951
- const current = existing ?? [];
952
- if (!existing) {
953
- modelRelations.set(relation.declaringModelName, current);
954
- }
955
- current.push({
956
- fieldName: relation.declaringFieldName,
957
- toModel: relation.targetModelName,
958
- toTable: relation.targetTableName,
959
- cardinality: 'N:1',
960
- parentTable: relation.declaringTableName,
961
- parentColumns: relation.localColumns,
962
- childTable: relation.targetTableName,
963
- childColumns: relation.referencedColumns,
964
- });
965
-
966
- const pairKey = fkRelationPairKey(relation.declaringModelName, relation.targetModelName);
967
- const pairRelations = fkRelationsByPair.get(pairKey);
968
- if (!pairRelations) {
969
- fkRelationsByPair.set(pairKey, [relation]);
970
- continue;
971
- }
972
- pairRelations.push(relation);
973
- }
974
-
975
- return { modelRelations, fkRelationsByPair };
976
- }
977
-
978
- function applyBackrelationCandidates(input: {
979
- readonly backrelationCandidates: readonly ModelBackrelationCandidate[];
980
- readonly fkRelationsByPair: Map<string, readonly FkRelationMetadata[]>;
981
- readonly modelRelations: Map<string, ModelRelationMetadata[]>;
982
- readonly diagnostics: ContractSourceDiagnostic[];
983
- readonly sourceId: string;
984
- }): void {
985
- for (const candidate of input.backrelationCandidates) {
986
- const pairKey = fkRelationPairKey(candidate.targetModelName, candidate.modelName);
987
- const pairMatches = input.fkRelationsByPair.get(pairKey) ?? [];
988
- const matches = candidate.relationName
989
- ? pairMatches.filter((relation) => relation.relationName === candidate.relationName)
990
- : [...pairMatches];
991
-
992
- if (matches.length === 0) {
993
- input.diagnostics.push({
994
- code: 'PSL_ORPHANED_BACKRELATION_LIST',
995
- 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.`,
996
- sourceId: input.sourceId,
997
- span: candidate.field.span,
998
- });
999
- continue;
1000
- }
1001
- if (matches.length > 1) {
1002
- input.diagnostics.push({
1003
- code: 'PSL_AMBIGUOUS_BACKRELATION_LIST',
1004
- message: `Backrelation list field "${candidate.modelName}.${candidate.field.name}" matches multiple FK-side relations on model "${candidate.targetModelName}". Add @relation(name: "...") (or @relation("...")) to both sides to disambiguate.`,
1005
- sourceId: input.sourceId,
1006
- span: candidate.field.span,
1007
- });
1008
- continue;
1009
- }
1010
-
1011
- invariant(matches.length === 1, 'Backrelation matching requires exactly one match');
1012
- const matched = matches[0];
1013
- assertDefined(matched, 'Backrelation matching requires a defined relation match');
1014
-
1015
- const existing = input.modelRelations.get(candidate.modelName);
1016
- const current = existing ?? [];
1017
- if (!existing) {
1018
- input.modelRelations.set(candidate.modelName, current);
1019
- }
1020
- current.push({
1021
- fieldName: candidate.field.name,
1022
- toModel: matched.declaringModelName,
1023
- toTable: matched.declaringTableName,
1024
- cardinality: '1:N',
1025
- parentTable: candidate.tableName,
1026
- parentColumns: matched.referencedColumns,
1027
- childTable: matched.declaringTableName,
1028
- childColumns: matched.localColumns,
1029
- });
1030
- }
1031
- }
1032
-
1033
- function emitModelsWithRelations(input: {
1034
- readonly builder: DynamicContractBuilder;
1035
- readonly resolvedModels: ResolvedModelEntry[];
1036
- readonly modelRelations: Map<string, readonly ModelRelationMetadata[]>;
1037
- }): DynamicContractBuilder {
1038
- let nextBuilder = input.builder;
1039
-
1040
- const sortedModels = input.resolvedModels.sort((left, right) => {
1041
- const tableComparison = compareStrings(left.mapping.tableName, right.mapping.tableName);
1042
- if (tableComparison === 0) {
1043
- return compareStrings(left.model.name, right.model.name);
1044
- }
1045
- return tableComparison;
1046
- });
1047
-
1048
- for (const entry of sortedModels) {
1049
- const relationEntries = [...(input.modelRelations.get(entry.model.name) ?? [])].sort(
1050
- (left, right) => compareStrings(left.fieldName, right.fieldName),
1051
- );
1052
- nextBuilder = nextBuilder.model(
1053
- entry.model.name,
1054
- entry.mapping.tableName,
1055
- (modelBuilder: DynamicModelBuilder) => {
1056
- let next = modelBuilder;
1057
- for (const resolvedField of entry.resolvedFields) {
1058
- next = next.field(resolvedField.field.name, resolvedField.columnName);
1059
- }
1060
- for (const relation of relationEntries) {
1061
- next = next.relation(relation.fieldName, {
1062
- toModel: relation.toModel,
1063
- toTable: relation.toTable,
1064
- cardinality: relation.cardinality,
1065
- on: {
1066
- parentTable: relation.parentTable,
1067
- parentColumns: relation.parentColumns,
1068
- childTable: relation.childTable,
1069
- childColumns: relation.childColumns,
1070
- },
1071
- });
1072
- }
1073
- return next;
1074
- },
1075
- );
1076
- }
1077
-
1078
- return nextBuilder;
1079
- }
1080
-
1081
- function mapParserDiagnostics(document: ParsePslDocumentResult): ContractSourceDiagnostic[] {
1082
- return document.diagnostics.map((diagnostic) => ({
1083
- code: diagnostic.code,
1084
- message: diagnostic.message,
1085
- sourceId: diagnostic.sourceId,
1086
- span: diagnostic.span,
1087
- }));
1088
- }
1089
-
1090
- function normalizeReferentialAction(input: {
1091
- readonly modelName: string;
1092
- readonly fieldName: string;
1093
- readonly actionName: 'onDelete' | 'onUpdate';
1094
- readonly actionToken: string;
1095
- readonly sourceId: string;
1096
- readonly span: PslSpan;
1097
- readonly diagnostics: ContractSourceDiagnostic[];
1098
- }): string | undefined {
1099
- const normalized =
1100
- REFERENTIAL_ACTION_MAP[input.actionToken as keyof typeof REFERENTIAL_ACTION_MAP];
1101
- if (normalized) {
1102
- return normalized;
1103
- }
1104
-
1105
- input.diagnostics.push({
1106
- code: 'PSL_UNSUPPORTED_REFERENTIAL_ACTION',
1107
- message: `Relation field "${input.modelName}.${input.fieldName}" has unsupported ${input.actionName} action "${input.actionToken}"`,
1108
- sourceId: input.sourceId,
1109
- span: input.span,
1110
- });
1111
- return undefined;
1112
- }
1113
-
1114
- function parseAttributeFieldList(input: {
1115
- readonly attribute: PslAttribute;
1116
- readonly sourceId: string;
1117
- readonly diagnostics: ContractSourceDiagnostic[];
1118
- readonly code: string;
1119
- readonly messagePrefix: string;
1120
- }): readonly string[] | undefined {
1121
- const raw = getNamedArgument(input.attribute, 'fields') ?? getPositionalArgument(input.attribute);
1122
- if (!raw) {
1123
- input.diagnostics.push({
1124
- code: input.code,
1125
- message: `${input.messagePrefix} requires fields list argument`,
1126
- sourceId: input.sourceId,
1127
- span: input.attribute.span,
1128
- });
1129
- return undefined;
1130
- }
1131
- const fields = parseFieldList(raw);
1132
- if (!fields || fields.length === 0) {
1133
- input.diagnostics.push({
1134
- code: input.code,
1135
- message: `${input.messagePrefix} requires bracketed field list argument`,
1136
- sourceId: input.sourceId,
1137
- span: input.attribute.span,
1138
- });
1139
- return undefined;
1140
- }
1141
- return fields;
1142
- }
1143
-
1144
- function mapFieldNamesToColumns(input: {
1145
- readonly modelName: string;
1146
- readonly fieldNames: readonly string[];
1147
- readonly mapping: ModelNameMapping;
1148
- readonly sourceId: string;
1149
- readonly diagnostics: ContractSourceDiagnostic[];
1150
- readonly span: PslSpan;
1151
- readonly contextLabel: string;
1152
- }): readonly string[] | undefined {
1153
- const columns: string[] = [];
1154
- for (const fieldName of input.fieldNames) {
1155
- const columnName = input.mapping.fieldColumns.get(fieldName);
1156
- if (!columnName) {
1157
- input.diagnostics.push({
1158
- code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
1159
- message: `${input.contextLabel} references unknown field "${input.modelName}.${fieldName}"`,
1160
- sourceId: input.sourceId,
1161
- span: input.span,
1162
- });
1163
- return undefined;
1164
- }
1165
- columns.push(columnName);
1166
- }
1167
- return columns;
1168
- }
1169
-
1170
- function buildModelMappings(
1171
- models: readonly PslModel[],
1172
- diagnostics: ContractSourceDiagnostic[],
1173
- sourceId: string,
1174
- ): Map<string, ModelNameMapping> {
1175
- const result = new Map<string, ModelNameMapping>();
1176
- for (const model of models) {
1177
- const mapAttribute = getAttribute(model.attributes, 'map');
1178
- const tableName = parseMapName({
1179
- attribute: mapAttribute,
1180
- defaultValue: lowerFirst(model.name),
1181
- sourceId,
1182
- diagnostics,
1183
- entityLabel: `Model "${model.name}"`,
1184
- span: model.span,
1185
- });
1186
- const fieldColumns = new Map<string, string>();
1187
- for (const field of model.fields) {
1188
- const fieldMapAttribute = getAttribute(field.attributes, 'map');
1189
- const columnName = parseMapName({
1190
- attribute: fieldMapAttribute,
1191
- defaultValue: field.name,
1192
- sourceId,
1193
- diagnostics,
1194
- entityLabel: `Field "${model.name}.${field.name}"`,
1195
- span: field.span,
1196
- });
1197
- fieldColumns.set(field.name, columnName);
1198
- }
1199
- result.set(model.name, {
1200
- model,
1201
- tableName,
1202
- fieldColumns,
1203
- });
1204
- }
1205
- return result;
1206
- }
1207
-
1208
- function validateNavigationListFieldAttributes(input: {
1209
- readonly modelName: string;
1210
- readonly field: PslField;
1211
- readonly sourceId: string;
1212
- readonly composedExtensions: Set<string>;
1213
- readonly diagnostics: ContractSourceDiagnostic[];
1214
- }): boolean {
1215
- let valid = true;
1216
- for (const attribute of input.field.attributes) {
1217
- if (attribute.name === 'relation') {
1218
- continue;
1219
- }
1220
- if (attribute.name.startsWith('pgvector.') && !input.composedExtensions.has('pgvector')) {
1221
- input.diagnostics.push({
1222
- code: 'PSL_EXTENSION_NAMESPACE_NOT_COMPOSED',
1223
- message: `Attribute "@${attribute.name}" uses unrecognized namespace "pgvector". Add extension pack "pgvector" to extensionPacks in prisma-next.config.ts.`,
1224
- sourceId: input.sourceId,
1225
- span: attribute.span,
1226
- });
1227
- valid = false;
1228
- continue;
1229
- }
1230
- input.diagnostics.push({
1231
- code: 'PSL_UNSUPPORTED_FIELD_ATTRIBUTE',
1232
- message: `Field "${input.modelName}.${input.field.name}" uses unsupported attribute "@${attribute.name}"`,
1233
- sourceId: input.sourceId,
1234
- span: attribute.span,
1235
- });
1236
- valid = false;
1237
- }
1238
- return valid;
1239
- }
1240
-
1241
- function parseRelationAttribute(input: {
1242
- readonly attribute: PslAttribute;
1243
- readonly modelName: string;
1244
- readonly fieldName: string;
1245
- readonly sourceId: string;
1246
- readonly diagnostics: ContractSourceDiagnostic[];
1247
- }): ParsedRelationAttribute | undefined {
1248
- const positionalEntries = input.attribute.args.filter((arg) => arg.kind === 'positional');
1249
- if (positionalEntries.length > 1) {
1250
- input.diagnostics.push({
1251
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1252
- message: `Relation field "${input.modelName}.${input.fieldName}" has too many positional arguments`,
1253
- sourceId: input.sourceId,
1254
- span: input.attribute.span,
1255
- });
1256
- return undefined;
1257
- }
1258
-
1259
- let relationNameFromPositional: string | undefined;
1260
- const positionalNameEntry = getPositionalArgumentEntry(input.attribute);
1261
- if (positionalNameEntry) {
1262
- const parsedName = parseQuotedStringLiteral(positionalNameEntry.value);
1263
- if (!parsedName) {
1264
- input.diagnostics.push({
1265
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1266
- message: `Relation field "${input.modelName}.${input.fieldName}" positional relation name must be a quoted string literal`,
1267
- sourceId: input.sourceId,
1268
- span: positionalNameEntry.span,
1269
- });
1270
- return undefined;
1271
- }
1272
- relationNameFromPositional = parsedName;
1273
- }
1274
-
1275
- for (const arg of input.attribute.args) {
1276
- if (arg.kind === 'positional') {
1277
- continue;
1278
- }
1279
- if (
1280
- arg.name !== 'name' &&
1281
- arg.name !== 'fields' &&
1282
- arg.name !== 'references' &&
1283
- arg.name !== 'map' &&
1284
- arg.name !== 'onDelete' &&
1285
- arg.name !== 'onUpdate'
1286
- ) {
1287
- input.diagnostics.push({
1288
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1289
- message: `Relation field "${input.modelName}.${input.fieldName}" has unsupported argument "${arg.name}"`,
1290
- sourceId: input.sourceId,
1291
- span: arg.span,
1292
- });
1293
- return undefined;
1294
- }
1295
- }
1296
-
1297
- const namedRelationNameRaw = getNamedArgument(input.attribute, 'name');
1298
- const namedRelationName = namedRelationNameRaw
1299
- ? parseQuotedStringLiteral(namedRelationNameRaw)
1300
- : undefined;
1301
- if (namedRelationNameRaw && !namedRelationName) {
1302
- input.diagnostics.push({
1303
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1304
- message: `Relation field "${input.modelName}.${input.fieldName}" named relation name must be a quoted string literal`,
1305
- sourceId: input.sourceId,
1306
- span: input.attribute.span,
1307
- });
1308
- return undefined;
1309
- }
1310
-
1311
- if (
1312
- relationNameFromPositional &&
1313
- namedRelationName &&
1314
- relationNameFromPositional !== namedRelationName
1315
- ) {
1316
- input.diagnostics.push({
1317
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1318
- message: `Relation field "${input.modelName}.${input.fieldName}" has conflicting positional and named relation names`,
1319
- sourceId: input.sourceId,
1320
- span: input.attribute.span,
1321
- });
1322
- return undefined;
1323
- }
1324
- const relationName = namedRelationName ?? relationNameFromPositional;
1325
-
1326
- const constraintNameRaw = getNamedArgument(input.attribute, 'map');
1327
- const constraintName = constraintNameRaw
1328
- ? parseQuotedStringLiteral(constraintNameRaw)
1329
- : undefined;
1330
- if (constraintNameRaw && !constraintName) {
1331
- input.diagnostics.push({
1332
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1333
- message: `Relation field "${input.modelName}.${input.fieldName}" map argument must be a quoted string literal`,
1334
- sourceId: input.sourceId,
1335
- span: input.attribute.span,
1336
- });
1337
- return undefined;
1338
- }
1339
-
1340
- const fieldsRaw = getNamedArgument(input.attribute, 'fields');
1341
- const referencesRaw = getNamedArgument(input.attribute, 'references');
1342
- if ((fieldsRaw && !referencesRaw) || (!fieldsRaw && referencesRaw)) {
1343
- input.diagnostics.push({
1344
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1345
- message: `Relation field "${input.modelName}.${input.fieldName}" requires fields and references arguments`,
1346
- sourceId: input.sourceId,
1347
- span: input.attribute.span,
1348
- });
1349
- return undefined;
1350
- }
1351
-
1352
- let fields: readonly string[] | undefined;
1353
- let references: readonly string[] | undefined;
1354
- if (fieldsRaw && referencesRaw) {
1355
- const parsedFields = parseFieldList(fieldsRaw);
1356
- const parsedReferences = parseFieldList(referencesRaw);
1357
- if (
1358
- !parsedFields ||
1359
- !parsedReferences ||
1360
- parsedFields.length === 0 ||
1361
- parsedReferences.length === 0
1362
- ) {
1363
- input.diagnostics.push({
1364
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1365
- message: `Relation field "${input.modelName}.${input.fieldName}" requires bracketed fields and references lists`,
1366
- sourceId: input.sourceId,
1367
- span: input.attribute.span,
1368
- });
1369
- return undefined;
1370
- }
1371
- fields = parsedFields;
1372
- references = parsedReferences;
1373
- }
1374
-
1375
- const onDeleteArgument = getNamedArgument(input.attribute, 'onDelete');
1376
- const onUpdateArgument = getNamedArgument(input.attribute, 'onUpdate');
1377
-
1378
- return {
1379
- ...ifDefined('relationName', relationName),
1380
- ...ifDefined('fields', fields),
1381
- ...ifDefined('references', references),
1382
- ...ifDefined('constraintName', constraintName),
1383
- ...ifDefined('onDelete', onDeleteArgument ? unquoteStringLiteral(onDeleteArgument) : undefined),
1384
- ...ifDefined('onUpdate', onUpdateArgument ? unquoteStringLiteral(onUpdateArgument) : undefined),
1385
- };
1386
- }
1387
-
1388
- export function interpretPslDocumentToSqlContractIR(
1389
- input: InterpretPslDocumentToSqlContractIRInput,
1390
- ): Result<ContractIR, ContractSourceDiagnostics> {
1391
- const sourceId = input.document.ast.sourceId;
1392
- if (!input.target) {
1393
- return notOk({
1394
- summary: 'PSL to SQL Contract IR normalization failed',
1395
- diagnostics: [
1396
- {
1397
- code: 'PSL_TARGET_CONTEXT_REQUIRED',
1398
- message: 'PSL interpretation requires an explicit target context from composition.',
1399
- sourceId,
1400
- },
1401
- ],
1402
- });
1403
- }
1404
- if (!input.scalarTypeDescriptors) {
1405
- return notOk({
1406
- summary: 'PSL to SQL Contract IR normalization failed',
1407
- diagnostics: [
1408
- {
1409
- code: 'PSL_SCALAR_TYPE_CONTEXT_REQUIRED',
1410
- message: 'PSL interpretation requires composed scalar type descriptors.',
1411
- sourceId,
1412
- },
1413
- ],
1414
- });
83
+ readonly authoringContributions?: AuthoringContributions;
84
+ }
85
+
86
+ function buildComposedExtensionPackRefs(
87
+ target: TargetPackRef<'sql', 'postgres'>,
88
+ extensionIds: readonly string[],
89
+ extensionPackRefs: readonly ExtensionPackRef<'sql', 'postgres'>[] = [],
90
+ ): Record<string, ExtensionPackRef<'sql', 'postgres'>> | undefined {
91
+ if (extensionIds.length === 0) {
92
+ return undefined;
1415
93
  }
1416
94
 
1417
- const diagnostics: ContractSourceDiagnostic[] = mapParserDiagnostics(input.document);
1418
- const modelNames = new Set(input.document.ast.models.map((model) => model.name));
1419
- const composedExtensions = new Set(input.composedExtensionPacks ?? []);
1420
- const defaultFunctionRegistry =
1421
- input.controlMutationDefaults?.defaultFunctionRegistry ?? new Map<string, never>();
1422
- const generatorDescriptors = input.controlMutationDefaults?.generatorDescriptors ?? [];
1423
- const generatorDescriptorById = new Map<string, MutationDefaultGeneratorDescriptor>();
1424
- for (const descriptor of generatorDescriptors) {
1425
- generatorDescriptorById.set(descriptor.id, descriptor);
95
+ const extensionPackRefById = new Map(extensionPackRefs.map((packRef) => [packRef.id, packRef]));
96
+
97
+ return Object.fromEntries(
98
+ extensionIds.map((extensionId) => [
99
+ extensionId,
100
+ extensionPackRefById.get(extensionId) ??
101
+ ({
102
+ kind: 'extension',
103
+ id: extensionId,
104
+ familyId: target.familyId,
105
+ targetId: target.targetId,
106
+ version: '0.0.1',
107
+ } satisfies ExtensionPackRef<'sql', 'postgres'>),
108
+ ]),
109
+ );
110
+ }
111
+
112
+ function hasSameSpan(a: PslSpan, b: ContractSourceDiagnosticSpan): boolean {
113
+ return (
114
+ a.start.offset === b.start.offset &&
115
+ a.end.offset === b.end.offset &&
116
+ a.start.line === b.start.line &&
117
+ a.end.line === b.end.line
118
+ );
119
+ }
120
+
121
+ function compareStrings(left: string, right: string): -1 | 0 | 1 {
122
+ if (left < right) {
123
+ return -1;
124
+ }
125
+ if (left > right) {
126
+ return 1;
1426
127
  }
128
+ return 0;
129
+ }
130
+
131
+ function mapParserDiagnostics(document: ParsePslDocumentResult): ContractSourceDiagnostic[] {
132
+ return document.diagnostics.map((diagnostic) => ({
133
+ code: diagnostic.code,
134
+ message: diagnostic.message,
135
+ sourceId: diagnostic.sourceId,
136
+ span: diagnostic.span,
137
+ }));
138
+ }
1427
139
 
1428
- let builder = defineContract().target(input.target) as DynamicContractBuilder;
140
+ interface ProcessEnumDeclarationsInput {
141
+ readonly enums: readonly PslEnum[];
142
+ readonly sourceId: string;
143
+ readonly enumTypeConstructor: AuthoringTypeConstructorDescriptor | undefined;
144
+ readonly diagnostics: ContractSourceDiagnostic[];
145
+ }
146
+
147
+ function processEnumDeclarations(input: ProcessEnumDeclarationsInput): {
148
+ readonly storageTypes: Record<string, StorageTypeInstance>;
149
+ readonly enumTypeDescriptors: Map<string, ColumnDescriptor>;
150
+ } {
151
+ const storageTypes: Record<string, StorageTypeInstance> = {};
1429
152
  const enumTypeDescriptors = new Map<string, ColumnDescriptor>();
1430
- const namedTypeDescriptors = new Map<string, ColumnDescriptor>();
1431
- const namedTypeBaseTypes = new Map<string, string>();
1432
153
 
1433
- for (const enumDeclaration of input.document.ast.enums) {
154
+ for (const enumDeclaration of input.enums) {
1434
155
  const nativeType = parseMapName({
1435
156
  attribute: getAttribute(enumDeclaration.attributes, 'map'),
1436
157
  defaultValue: enumDeclaration.name,
1437
- sourceId,
1438
- diagnostics,
158
+ sourceId: input.sourceId,
159
+ diagnostics: input.diagnostics,
1439
160
  entityLabel: `Enum "${enumDeclaration.name}"`,
1440
161
  span: enumDeclaration.span,
1441
162
  });
163
+ const enumStorageType = input.enumTypeConstructor
164
+ ? instantiateAuthoringTypeConstructor(input.enumTypeConstructor, [
165
+ nativeType,
166
+ enumDeclaration.values.map((value) => value.name),
167
+ ])
168
+ : {
169
+ codecId: 'pg/enum@1',
170
+ nativeType,
171
+ typeParams: { values: enumDeclaration.values.map((value) => value.name) },
172
+ };
1442
173
  const descriptor: ColumnDescriptor = {
1443
- codecId: 'pg/enum@1',
1444
- nativeType,
174
+ codecId: enumStorageType.codecId,
175
+ nativeType: enumStorageType.nativeType,
1445
176
  typeRef: enumDeclaration.name,
1446
177
  };
1447
178
  enumTypeDescriptors.set(enumDeclaration.name, descriptor);
1448
- builder = builder.storageType(enumDeclaration.name, {
1449
- codecId: 'pg/enum@1',
1450
- nativeType,
1451
- typeParams: { values: enumDeclaration.values.map((value) => value.name) },
1452
- });
179
+ storageTypes[enumDeclaration.name] = {
180
+ codecId: enumStorageType.codecId,
181
+ nativeType: enumStorageType.nativeType,
182
+ typeParams: enumStorageType.typeParams ?? {
183
+ values: enumDeclaration.values.map((value) => value.name),
184
+ },
185
+ };
1453
186
  }
1454
187
 
1455
- for (const declaration of input.document.ast.types?.declarations ?? []) {
188
+ return { storageTypes, enumTypeDescriptors };
189
+ }
190
+
191
+ interface ResolveNamedTypeDeclarationsInput {
192
+ readonly declarations: readonly PslNamedTypeDeclaration[];
193
+ readonly sourceId: string;
194
+ readonly enumTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
195
+ readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
196
+ readonly composedExtensions: ReadonlySet<string>;
197
+ readonly pgvectorVectorConstructor: AuthoringTypeConstructorDescriptor | undefined;
198
+ readonly diagnostics: ContractSourceDiagnostic[];
199
+ }
200
+
201
+ function resolveNamedTypeDeclarations(input: ResolveNamedTypeDeclarationsInput): {
202
+ readonly storageTypes: Record<string, StorageTypeInstance>;
203
+ readonly namedTypeDescriptors: Map<string, ColumnDescriptor>;
204
+ readonly namedTypeBaseTypes: Map<string, string>;
205
+ } {
206
+ const storageTypes: Record<string, StorageTypeInstance> = {};
207
+ const namedTypeDescriptors = new Map<string, ColumnDescriptor>();
208
+ const namedTypeBaseTypes = new Map<string, string>();
209
+
210
+ for (const declaration of input.declarations) {
1456
211
  const baseDescriptor =
1457
- enumTypeDescriptors.get(declaration.baseType) ??
212
+ input.enumTypeDescriptors.get(declaration.baseType) ??
1458
213
  input.scalarTypeDescriptors.get(declaration.baseType);
1459
214
  if (!baseDescriptor) {
1460
- diagnostics.push({
215
+ input.diagnostics.push({
1461
216
  code: 'PSL_UNSUPPORTED_NAMED_TYPE_BASE',
1462
217
  message: `Named type "${declaration.name}" references unsupported base type "${declaration.baseType}"`,
1463
- sourceId,
218
+ sourceId: input.sourceId,
1464
219
  span: declaration.span,
1465
220
  });
1466
221
  continue;
@@ -1475,63 +230,69 @@ export function interpretPslDocumentToSqlContractIR(
1475
230
  (attribute) => attribute.name !== 'pgvector.column' && !attribute.name.startsWith('db.'),
1476
231
  );
1477
232
  if (unsupportedNamedTypeAttribute) {
1478
- diagnostics.push({
233
+ input.diagnostics.push({
1479
234
  code: 'PSL_UNSUPPORTED_NAMED_TYPE_ATTRIBUTE',
1480
235
  message: `Named type "${declaration.name}" uses unsupported attribute "${unsupportedNamedTypeAttribute.name}"`,
1481
- sourceId,
236
+ sourceId: input.sourceId,
1482
237
  span: unsupportedNamedTypeAttribute.span,
1483
238
  });
1484
239
  continue;
1485
240
  }
1486
241
 
1487
242
  if (pgvectorAttribute && dbNativeTypeAttribute) {
1488
- diagnostics.push({
243
+ input.diagnostics.push({
1489
244
  code: 'PSL_UNSUPPORTED_NAMED_TYPE_ATTRIBUTE',
1490
245
  message: `Named type "${declaration.name}" cannot combine @pgvector.column with @${dbNativeTypeAttribute.name}.`,
1491
- sourceId,
246
+ sourceId: input.sourceId,
1492
247
  span: dbNativeTypeAttribute.span,
1493
248
  });
1494
249
  continue;
1495
250
  }
1496
251
 
1497
252
  if (pgvectorAttribute) {
1498
- if (!composedExtensions.has('pgvector')) {
1499
- diagnostics.push({
253
+ if (!input.composedExtensions.has('pgvector')) {
254
+ input.diagnostics.push({
1500
255
  code: 'PSL_EXTENSION_NAMESPACE_NOT_COMPOSED',
1501
256
  message:
1502
257
  'Attribute "@pgvector.column" uses unrecognized namespace "pgvector". Add extension pack "pgvector" to extensionPacks in prisma-next.config.ts.',
1503
- sourceId,
258
+ sourceId: input.sourceId,
1504
259
  span: pgvectorAttribute.span,
1505
260
  });
1506
261
  continue;
1507
262
  }
1508
263
  if (declaration.baseType !== 'Bytes') {
1509
- diagnostics.push({
264
+ input.diagnostics.push({
1510
265
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
1511
266
  message: `Named type "${declaration.name}" uses @pgvector.column on unsupported base type "${declaration.baseType}"`,
1512
- sourceId,
267
+ sourceId: input.sourceId,
1513
268
  span: pgvectorAttribute.span,
1514
269
  });
1515
270
  continue;
1516
271
  }
1517
272
  const length = parsePgvectorLength({
1518
273
  attribute: pgvectorAttribute,
1519
- diagnostics,
1520
- sourceId,
274
+ diagnostics: input.diagnostics,
275
+ sourceId: input.sourceId,
1521
276
  });
1522
277
  if (length === undefined) {
1523
278
  continue;
1524
279
  }
1525
- namedTypeDescriptors.set(declaration.name, {
1526
- codecId: 'pg/vector@1',
1527
- nativeType: 'vector',
1528
- typeParams: { length },
1529
- });
1530
- builder = builder.storageType(declaration.name, {
1531
- codecId: 'pg/vector@1',
1532
- nativeType: 'vector',
1533
- typeParams: { length },
1534
- });
280
+ const pgvectorStorageType = input.pgvectorVectorConstructor
281
+ ? instantiateAuthoringTypeConstructor(input.pgvectorVectorConstructor, [length])
282
+ : {
283
+ codecId: 'pg/vector@1',
284
+ nativeType: 'vector',
285
+ typeParams: { length },
286
+ };
287
+ namedTypeDescriptors.set(
288
+ declaration.name,
289
+ toNamedTypeFieldDescriptor(declaration.name, pgvectorStorageType),
290
+ );
291
+ storageTypes[declaration.name] = {
292
+ codecId: pgvectorStorageType.codecId,
293
+ nativeType: pgvectorStorageType.nativeType,
294
+ typeParams: pgvectorStorageType.typeParams ?? { length },
295
+ };
1535
296
  continue;
1536
297
  }
1537
298
 
@@ -1540,359 +301,778 @@ export function interpretPslDocumentToSqlContractIR(
1540
301
  attribute: dbNativeTypeAttribute,
1541
302
  baseType: declaration.baseType,
1542
303
  baseDescriptor,
1543
- diagnostics,
1544
- sourceId,
304
+ diagnostics: input.diagnostics,
305
+ sourceId: input.sourceId,
1545
306
  entityLabel: `Named type "${declaration.name}"`,
1546
307
  });
1547
308
  if (!descriptor) {
1548
309
  continue;
1549
310
  }
1550
- namedTypeDescriptors.set(declaration.name, {
1551
- ...descriptor,
1552
- typeRef: declaration.name,
1553
- });
1554
- builder = builder.storageType(declaration.name, {
311
+ namedTypeDescriptors.set(
312
+ declaration.name,
313
+ toNamedTypeFieldDescriptor(declaration.name, descriptor),
314
+ );
315
+ storageTypes[declaration.name] = {
1555
316
  codecId: descriptor.codecId,
1556
317
  nativeType: descriptor.nativeType,
1557
318
  typeParams: descriptor.typeParams ?? {},
1558
- });
319
+ };
1559
320
  continue;
1560
321
  }
1561
322
 
1562
- const descriptor: ColumnDescriptor = {
1563
- codecId: baseDescriptor.codecId,
1564
- nativeType: baseDescriptor.nativeType,
1565
- typeRef: declaration.name,
1566
- };
323
+ const descriptor = toNamedTypeFieldDescriptor(declaration.name, baseDescriptor);
1567
324
  namedTypeDescriptors.set(declaration.name, descriptor);
1568
- builder = builder.storageType(declaration.name, {
325
+ storageTypes[declaration.name] = {
1569
326
  codecId: baseDescriptor.codecId,
1570
327
  nativeType: baseDescriptor.nativeType,
1571
328
  typeParams: {},
329
+ };
330
+ }
331
+
332
+ return { storageTypes, namedTypeDescriptors, namedTypeBaseTypes };
333
+ }
334
+
335
+ interface BuildModelNodeInput {
336
+ readonly model: PslModel;
337
+ readonly mapping: ModelNameMapping;
338
+ readonly modelMappings: ReadonlyMap<string, ModelNameMapping>;
339
+ readonly modelNames: Set<string>;
340
+ readonly compositeTypeNames: ReadonlySet<string>;
341
+ readonly enumTypeDescriptors: Map<string, ColumnDescriptor>;
342
+ readonly namedTypeDescriptors: Map<string, ColumnDescriptor>;
343
+ readonly namedTypeBaseTypes: Map<string, string>;
344
+ readonly composedExtensions: Set<string>;
345
+ readonly authoringContributions: AuthoringContributions | undefined;
346
+ readonly defaultFunctionRegistry: ControlMutationDefaultRegistry;
347
+ readonly generatorDescriptorById: ReadonlyMap<string, MutationDefaultGeneratorDescriptor>;
348
+ readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
349
+ readonly sourceId: string;
350
+ readonly diagnostics: ContractSourceDiagnostic[];
351
+ }
352
+
353
+ interface BuildModelNodeResult {
354
+ readonly modelNode: ModelNode;
355
+ readonly fkRelationMetadata: FkRelationMetadata[];
356
+ readonly backrelationCandidates: ModelBackrelationCandidate[];
357
+ readonly resolvedFields: readonly ResolvedField[];
358
+ }
359
+
360
+ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult {
361
+ const { model, mapping, sourceId, diagnostics } = input;
362
+ const tableName = mapping.tableName;
363
+
364
+ const resolvedFields = collectResolvedFields(
365
+ model,
366
+ mapping,
367
+ input.enumTypeDescriptors,
368
+ input.namedTypeDescriptors,
369
+ input.namedTypeBaseTypes,
370
+ input.modelNames,
371
+ input.compositeTypeNames,
372
+ input.composedExtensions,
373
+ input.authoringContributions,
374
+ input.defaultFunctionRegistry,
375
+ input.generatorDescriptorById,
376
+ diagnostics,
377
+ sourceId,
378
+ input.scalarTypeDescriptors,
379
+ );
380
+
381
+ const primaryKeyFields = resolvedFields.filter((field) => field.isId);
382
+ const primaryKeyColumns = primaryKeyFields.map((field) => field.columnName);
383
+ const primaryKeyName = primaryKeyFields.length === 1 ? primaryKeyFields[0]?.idName : undefined;
384
+ const isVariantModel = model.attributes.some((attr) => attr.name === 'base');
385
+ if (primaryKeyColumns.length === 0 && !isVariantModel) {
386
+ diagnostics.push({
387
+ code: 'PSL_MISSING_PRIMARY_KEY',
388
+ message: `Model "${model.name}" must declare at least one @id field for SQL provider`,
389
+ sourceId,
390
+ span: model.span,
1572
391
  });
1573
392
  }
1574
393
 
1575
- const modelMappings = buildModelMappings(input.document.ast.models, diagnostics, sourceId);
1576
- const resolvedModels: Array<{
1577
- model: PslModel;
1578
- mapping: ModelNameMapping;
1579
- resolvedFields: ResolvedField[];
1580
- }> = [];
1581
- const fkRelationMetadata: FkRelationMetadata[] = [];
1582
- const backrelationCandidates: ModelBackrelationCandidate[] = [];
394
+ const resultBackrelationCandidates: ModelBackrelationCandidate[] = [];
395
+ for (const field of model.fields) {
396
+ if (!field.list || !input.modelNames.has(field.typeName)) {
397
+ continue;
398
+ }
399
+ const attributesValid = validateNavigationListFieldAttributes({
400
+ modelName: model.name,
401
+ field,
402
+ sourceId,
403
+ composedExtensions: input.composedExtensions,
404
+ diagnostics,
405
+ });
406
+ const relationAttribute = getAttribute(field.attributes, 'relation');
407
+ let relationName: string | undefined;
408
+ if (relationAttribute) {
409
+ const parsedRelation = parseRelationAttribute({
410
+ attribute: relationAttribute,
411
+ modelName: model.name,
412
+ fieldName: field.name,
413
+ sourceId,
414
+ diagnostics,
415
+ });
416
+ if (!parsedRelation) {
417
+ continue;
418
+ }
419
+ if (parsedRelation.fields || parsedRelation.references) {
420
+ diagnostics.push({
421
+ code: 'PSL_INVALID_RELATION_ATTRIBUTE',
422
+ message: `Backrelation list field "${model.name}.${field.name}" cannot declare fields/references; define them on the FK-side relation field`,
423
+ sourceId,
424
+ span: relationAttribute.span,
425
+ });
426
+ continue;
427
+ }
428
+ if (parsedRelation.onDelete || parsedRelation.onUpdate) {
429
+ diagnostics.push({
430
+ code: 'PSL_INVALID_RELATION_ATTRIBUTE',
431
+ message: `Backrelation list field "${model.name}.${field.name}" cannot declare onDelete/onUpdate; define referential actions on the FK-side relation field`,
432
+ sourceId,
433
+ span: relationAttribute.span,
434
+ });
435
+ continue;
436
+ }
437
+ relationName = parsedRelation.relationName;
438
+ }
439
+ if (!attributesValid) {
440
+ continue;
441
+ }
1583
442
 
1584
- for (const model of input.document.ast.models) {
1585
- const mapping = modelMappings.get(model.name);
1586
- if (!mapping) {
443
+ resultBackrelationCandidates.push({
444
+ modelName: model.name,
445
+ tableName,
446
+ field,
447
+ targetModelName: field.typeName,
448
+ ...ifDefined('relationName', relationName),
449
+ });
450
+ }
451
+
452
+ const relationAttributes = model.fields
453
+ .map((field) => ({
454
+ field,
455
+ relation: getAttribute(field.attributes, 'relation'),
456
+ }))
457
+ .filter((entry): entry is { field: PslField; relation: PslAttribute } =>
458
+ Boolean(entry.relation),
459
+ );
460
+ const uniqueConstraints: UniqueConstraintNode[] = resolvedFields
461
+ .filter((field) => field.isUnique)
462
+ .map((field) => ({
463
+ columns: [field.columnName],
464
+ ...ifDefined('name', field.uniqueName),
465
+ }));
466
+ const indexNodes: IndexNode[] = [];
467
+ const foreignKeyNodes: ForeignKeyNode[] = [];
468
+
469
+ for (const modelAttribute of model.attributes) {
470
+ if (modelAttribute.name === 'map') {
1587
471
  continue;
1588
472
  }
1589
- const tableName = mapping.tableName;
1590
- const resolvedFields = collectResolvedFields(
1591
- model,
473
+ if (modelAttribute.name === 'discriminator' || modelAttribute.name === 'base') {
474
+ continue;
475
+ }
476
+ if (modelAttribute.name === 'unique' || modelAttribute.name === 'index') {
477
+ const fieldNames = parseAttributeFieldList({
478
+ attribute: modelAttribute,
479
+ sourceId,
480
+ diagnostics,
481
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
482
+ messagePrefix: `Model "${model.name}" @@${modelAttribute.name}`,
483
+ });
484
+ if (!fieldNames) {
485
+ continue;
486
+ }
487
+ const columnNames = mapFieldNamesToColumns({
488
+ modelName: model.name,
489
+ fieldNames,
490
+ mapping,
491
+ sourceId,
492
+ diagnostics,
493
+ span: modelAttribute.span,
494
+ contextLabel: `Model "${model.name}" @@${modelAttribute.name}`,
495
+ });
496
+ if (!columnNames) {
497
+ continue;
498
+ }
499
+ const constraintName = parseConstraintMapArgument({
500
+ attribute: modelAttribute,
501
+ sourceId,
502
+ diagnostics,
503
+ entityLabel: `Model "${model.name}" @@${modelAttribute.name}`,
504
+ span: modelAttribute.span,
505
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
506
+ });
507
+ if (modelAttribute.name === 'unique') {
508
+ uniqueConstraints.push({
509
+ columns: columnNames,
510
+ ...ifDefined('name', constraintName),
511
+ });
512
+ } else {
513
+ indexNodes.push({
514
+ columns: columnNames,
515
+ ...ifDefined('name', constraintName),
516
+ });
517
+ }
518
+ continue;
519
+ }
520
+ if (modelAttribute.name.startsWith('pgvector.') && !input.composedExtensions.has('pgvector')) {
521
+ diagnostics.push({
522
+ code: 'PSL_EXTENSION_NAMESPACE_NOT_COMPOSED',
523
+ message: `Attribute "@@${modelAttribute.name}" uses unrecognized namespace "pgvector". Add extension pack "pgvector" to extensionPacks in prisma-next.config.ts.`,
524
+ sourceId,
525
+ span: modelAttribute.span,
526
+ });
527
+ continue;
528
+ }
529
+ diagnostics.push({
530
+ code: 'PSL_UNSUPPORTED_MODEL_ATTRIBUTE',
531
+ message: `Model "${model.name}" uses unsupported attribute "@@${modelAttribute.name}"`,
532
+ sourceId,
533
+ span: modelAttribute.span,
534
+ });
535
+ }
536
+
537
+ const resultFkRelationMetadata: FkRelationMetadata[] = [];
538
+ for (const relationAttribute of relationAttributes) {
539
+ if (relationAttribute.field.list) {
540
+ continue;
541
+ }
542
+
543
+ if (!input.modelNames.has(relationAttribute.field.typeName)) {
544
+ diagnostics.push({
545
+ code: 'PSL_INVALID_RELATION_TARGET',
546
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${relationAttribute.field.typeName}"`,
547
+ sourceId,
548
+ span: relationAttribute.field.span,
549
+ });
550
+ continue;
551
+ }
552
+
553
+ const parsedRelation = parseRelationAttribute({
554
+ attribute: relationAttribute.relation,
555
+ modelName: model.name,
556
+ fieldName: relationAttribute.field.name,
557
+ sourceId,
558
+ diagnostics,
559
+ });
560
+ if (!parsedRelation) {
561
+ continue;
562
+ }
563
+ if (!parsedRelation.fields || !parsedRelation.references) {
564
+ diagnostics.push({
565
+ code: 'PSL_INVALID_RELATION_ATTRIBUTE',
566
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" requires fields and references arguments`,
567
+ sourceId,
568
+ span: relationAttribute.relation.span,
569
+ });
570
+ continue;
571
+ }
572
+
573
+ const targetMapping = input.modelMappings.get(relationAttribute.field.typeName);
574
+ if (!targetMapping) {
575
+ diagnostics.push({
576
+ code: 'PSL_INVALID_RELATION_TARGET',
577
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${relationAttribute.field.typeName}"`,
578
+ sourceId,
579
+ span: relationAttribute.field.span,
580
+ });
581
+ continue;
582
+ }
583
+
584
+ const localColumns = mapFieldNamesToColumns({
585
+ modelName: model.name,
586
+ fieldNames: parsedRelation.fields,
1592
587
  mapping,
1593
- enumTypeDescriptors,
1594
- namedTypeDescriptors,
1595
- namedTypeBaseTypes,
1596
- modelNames,
1597
- composedExtensions,
1598
- defaultFunctionRegistry,
1599
- generatorDescriptorById,
588
+ sourceId,
1600
589
  diagnostics,
590
+ span: relationAttribute.relation.span,
591
+ contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
592
+ });
593
+ if (!localColumns) {
594
+ continue;
595
+ }
596
+ const referencedColumns = mapFieldNamesToColumns({
597
+ modelName: targetMapping.model.name,
598
+ fieldNames: parsedRelation.references,
599
+ mapping: targetMapping,
1601
600
  sourceId,
1602
- input.scalarTypeDescriptors,
1603
- );
1604
- resolvedModels.push({ model, mapping, resolvedFields });
601
+ diagnostics,
602
+ span: relationAttribute.relation.span,
603
+ contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
604
+ });
605
+ if (!referencedColumns) {
606
+ continue;
607
+ }
608
+ if (localColumns.length !== referencedColumns.length) {
609
+ diagnostics.push({
610
+ code: 'PSL_INVALID_RELATION_ATTRIBUTE',
611
+ message: `Relation field "${model.name}.${relationAttribute.field.name}" must provide the same number of fields and references`,
612
+ sourceId,
613
+ span: relationAttribute.relation.span,
614
+ });
615
+ continue;
616
+ }
617
+
618
+ const onDelete = parsedRelation.onDelete
619
+ ? normalizeReferentialAction({
620
+ modelName: model.name,
621
+ fieldName: relationAttribute.field.name,
622
+ actionName: 'onDelete',
623
+ actionToken: parsedRelation.onDelete,
624
+ sourceId,
625
+ span: relationAttribute.field.span,
626
+ diagnostics,
627
+ })
628
+ : undefined;
629
+ const onUpdate = parsedRelation.onUpdate
630
+ ? normalizeReferentialAction({
631
+ modelName: model.name,
632
+ fieldName: relationAttribute.field.name,
633
+ actionName: 'onUpdate',
634
+ actionToken: parsedRelation.onUpdate,
635
+ sourceId,
636
+ span: relationAttribute.field.span,
637
+ diagnostics,
638
+ })
639
+ : undefined;
640
+
641
+ foreignKeyNodes.push({
642
+ columns: localColumns,
643
+ references: {
644
+ model: targetMapping.model.name,
645
+ table: targetMapping.tableName,
646
+ columns: referencedColumns,
647
+ },
648
+ ...ifDefined('name', parsedRelation.constraintName),
649
+ ...ifDefined('onDelete', onDelete),
650
+ ...ifDefined('onUpdate', onUpdate),
651
+ });
1605
652
 
1606
- const primaryKeyColumns = resolvedFields
1607
- .filter((field) => field.isId)
1608
- .map((field) => field.columnName);
1609
- if (primaryKeyColumns.length === 0) {
1610
- diagnostics.push({
1611
- code: 'PSL_MISSING_PRIMARY_KEY',
1612
- message: `Model "${model.name}" must declare at least one @id field for SQL provider`,
1613
- sourceId,
1614
- span: model.span,
1615
- });
1616
- }
653
+ resultFkRelationMetadata.push({
654
+ declaringModelName: model.name,
655
+ declaringFieldName: relationAttribute.field.name,
656
+ declaringTableName: tableName,
657
+ targetModelName: targetMapping.model.name,
658
+ targetTableName: targetMapping.tableName,
659
+ ...ifDefined('relationName', parsedRelation.relationName),
660
+ localColumns,
661
+ referencedColumns,
662
+ });
663
+ }
664
+
665
+ return {
666
+ modelNode: {
667
+ modelName: model.name,
668
+ tableName,
669
+ fields: resolvedFields.map((resolvedField) => ({
670
+ fieldName: resolvedField.field.name,
671
+ columnName: resolvedField.columnName,
672
+ descriptor: resolvedField.descriptor,
673
+ nullable: resolvedField.field.optional,
674
+ ...ifDefined('default', resolvedField.defaultValue),
675
+ ...ifDefined('executionDefault', resolvedField.executionDefault),
676
+ })),
677
+ ...(primaryKeyColumns.length > 0
678
+ ? {
679
+ id: {
680
+ columns: primaryKeyColumns,
681
+ ...ifDefined('name', primaryKeyName),
682
+ },
683
+ }
684
+ : {}),
685
+ ...(uniqueConstraints.length > 0 ? { uniques: uniqueConstraints } : {}),
686
+ ...(indexNodes.length > 0 ? { indexes: indexNodes } : {}),
687
+ ...(foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {}),
688
+ },
689
+ fkRelationMetadata: resultFkRelationMetadata,
690
+ backrelationCandidates: resultBackrelationCandidates,
691
+ resolvedFields,
692
+ };
693
+ }
1617
694
 
1618
- for (const field of model.fields) {
1619
- if (!field.list || !modelNames.has(field.typeName)) {
695
+ function buildValueObjects(
696
+ compositeTypes: readonly PslCompositeType[],
697
+ enumTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
698
+ namedTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
699
+ scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
700
+ diagnostics: ContractSourceDiagnostic[],
701
+ sourceId: string,
702
+ ): Record<string, ContractValueObject> {
703
+ const valueObjects: Record<string, ContractValueObject> = {};
704
+ const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
705
+
706
+ for (const compositeType of compositeTypes) {
707
+ const fields: Record<string, ContractField> = {};
708
+ for (const field of compositeType.fields) {
709
+ if (compositeTypeNames.has(field.typeName)) {
710
+ const result: ContractField = {
711
+ type: { kind: 'valueObject', name: field.typeName },
712
+ nullable: field.optional,
713
+ };
714
+ fields[field.name] = field.list ? { ...result, many: true } : result;
1620
715
  continue;
1621
716
  }
1622
- const attributesValid = validateNavigationListFieldAttributes({
1623
- modelName: model.name,
717
+ const descriptor = resolveColumnDescriptor(
1624
718
  field,
1625
- sourceId,
1626
- composedExtensions,
1627
- diagnostics,
1628
- });
1629
- const relationAttribute = getAttribute(field.attributes, 'relation');
1630
- let relationName: string | undefined;
1631
- if (relationAttribute) {
1632
- const parsedRelation = parseRelationAttribute({
1633
- attribute: relationAttribute,
1634
- modelName: model.name,
1635
- fieldName: field.name,
719
+ enumTypeDescriptors as Map<string, ColumnDescriptor>,
720
+ namedTypeDescriptors as Map<string, ColumnDescriptor>,
721
+ scalarTypeDescriptors,
722
+ );
723
+ if (!descriptor) {
724
+ diagnostics.push({
725
+ code: 'PSL_UNSUPPORTED_FIELD_TYPE',
726
+ message: `Field "${compositeType.name}.${field.name}" type "${field.typeName}" is not supported`,
1636
727
  sourceId,
1637
- diagnostics,
728
+ span: field.span,
1638
729
  });
1639
- if (!parsedRelation) {
1640
- continue;
1641
- }
1642
- if (parsedRelation.fields || parsedRelation.references) {
1643
- diagnostics.push({
1644
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1645
- message: `Backrelation list field "${model.name}.${field.name}" cannot declare fields/references; define them on the FK-side relation field`,
1646
- sourceId,
1647
- span: relationAttribute.span,
1648
- });
1649
- continue;
1650
- }
1651
- if (parsedRelation.onDelete || parsedRelation.onUpdate) {
1652
- diagnostics.push({
1653
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1654
- message: `Backrelation list field "${model.name}.${field.name}" cannot declare onDelete/onUpdate; define referential actions on the FK-side relation field`,
1655
- sourceId,
1656
- span: relationAttribute.span,
1657
- });
1658
- continue;
1659
- }
1660
- relationName = parsedRelation.relationName;
1661
- }
1662
- if (!attributesValid) {
1663
730
  continue;
1664
731
  }
732
+ fields[field.name] = {
733
+ nullable: field.optional,
734
+ type: { kind: 'scalar', codecId: descriptor.codecId },
735
+ };
736
+ }
737
+ valueObjects[compositeType.name] = { fields };
738
+ }
1665
739
 
1666
- backrelationCandidates.push({
1667
- modelName: model.name,
1668
- tableName,
1669
- field,
1670
- targetModelName: field.typeName,
1671
- ...ifDefined('relationName', relationName),
1672
- });
740
+ return valueObjects;
741
+ }
742
+
743
+ function patchModelDomainFields(
744
+ models: Record<string, ContractModel>,
745
+ modelResolvedFields: ReadonlyMap<string, readonly ResolvedField[]>,
746
+ ): Record<string, ContractModel> {
747
+ let patched = models;
748
+
749
+ for (const [modelName, resolvedFields] of modelResolvedFields) {
750
+ const model = patched[modelName];
751
+ if (!model) continue;
752
+
753
+ let needsPatch = false;
754
+ const patchedFields: Record<string, ContractField> = { ...model.fields };
755
+
756
+ for (const rf of resolvedFields) {
757
+ if (rf.valueObjectTypeName) {
758
+ needsPatch = true;
759
+ patchedFields[rf.field.name] = {
760
+ nullable: rf.field.optional,
761
+ type: { kind: 'valueObject', name: rf.valueObjectTypeName },
762
+ ...(rf.many ? { many: true as const } : {}),
763
+ };
764
+ } else if (rf.many && rf.scalarCodecId) {
765
+ needsPatch = true;
766
+ patchedFields[rf.field.name] = {
767
+ nullable: rf.field.optional,
768
+ type: { kind: 'scalar', codecId: rf.scalarCodecId },
769
+ many: true as const,
770
+ };
771
+ }
1673
772
  }
1674
773
 
1675
- const relationAttributes = model.fields
1676
- .map((field) => ({
1677
- field,
1678
- relation: getAttribute(field.attributes, 'relation'),
1679
- }))
1680
- .filter((entry): entry is { field: PslField; relation: PslAttribute } =>
1681
- Boolean(entry.relation),
1682
- );
774
+ if (needsPatch) {
775
+ patched = { ...patched, [modelName]: { ...model, fields: patchedFields } };
776
+ }
777
+ }
1683
778
 
1684
- builder = builder.table(tableName, (tableBuilder: DynamicTableBuilder) => {
1685
- let table = tableBuilder;
779
+ return patched;
780
+ }
1686
781
 
1687
- for (const resolvedField of resolvedFields) {
1688
- if (resolvedField.executionDefault) {
1689
- table = table.generated(resolvedField.columnName, {
1690
- type: resolvedField.descriptor,
1691
- generated: resolvedField.executionDefault,
1692
- });
1693
- } else {
1694
- const options: {
1695
- type: ColumnDescriptor;
1696
- nullable?: true;
1697
- default?: ColumnDefault;
1698
- } = {
1699
- type: resolvedField.descriptor,
1700
- ...ifDefined('nullable', resolvedField.field.optional ? (true as const) : undefined),
1701
- ...ifDefined('default', resolvedField.defaultValue),
1702
- };
1703
- table = table.column(resolvedField.columnName, options);
1704
- }
782
+ type DiscriminatorDeclaration = {
783
+ readonly fieldName: string;
784
+ readonly span: ContractSourceDiagnosticSpan;
785
+ };
1705
786
 
1706
- if (resolvedField.isUnique) {
1707
- table = table.unique([resolvedField.columnName]);
1708
- }
1709
- }
787
+ type BaseDeclaration = {
788
+ readonly baseName: string;
789
+ readonly value: string;
790
+ readonly span: ContractSourceDiagnosticSpan;
791
+ };
1710
792
 
1711
- if (primaryKeyColumns.length > 0) {
1712
- table = table.primaryKey(primaryKeyColumns);
1713
- }
793
+ function collectPolymorphismDeclarations(
794
+ models: readonly PslModel[],
795
+ sourceId: string,
796
+ diagnostics: ContractSourceDiagnostic[],
797
+ ): {
798
+ discriminatorDeclarations: Map<string, DiscriminatorDeclaration>;
799
+ baseDeclarations: Map<string, BaseDeclaration>;
800
+ } {
801
+ const discriminatorDeclarations = new Map<string, DiscriminatorDeclaration>();
802
+ const baseDeclarations = new Map<string, BaseDeclaration>();
1714
803
 
1715
- for (const modelAttribute of model.attributes) {
1716
- if (modelAttribute.name === 'map') {
1717
- continue;
1718
- }
1719
- if (modelAttribute.name === 'unique' || modelAttribute.name === 'index') {
1720
- const fieldNames = parseAttributeFieldList({
1721
- attribute: modelAttribute,
1722
- sourceId,
1723
- diagnostics,
804
+ for (const model of models) {
805
+ for (const attr of model.attributes) {
806
+ if (attr.name === 'discriminator') {
807
+ const fieldName = getPositionalArgument(attr);
808
+ if (!fieldName) {
809
+ diagnostics.push({
1724
810
  code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
1725
- messagePrefix: `Model "${model.name}" @@${modelAttribute.name}`,
1726
- });
1727
- if (!fieldNames) {
1728
- continue;
1729
- }
1730
- const columnNames = mapFieldNamesToColumns({
1731
- modelName: model.name,
1732
- fieldNames,
1733
- mapping,
811
+ message: `Model "${model.name}" @@discriminator requires a field name argument`,
1734
812
  sourceId,
1735
- diagnostics,
1736
- span: modelAttribute.span,
1737
- contextLabel: `Model "${model.name}" @@${modelAttribute.name}`,
813
+ span: attr.span,
1738
814
  });
1739
- if (!columnNames) {
1740
- continue;
1741
- }
1742
- if (modelAttribute.name === 'unique') {
1743
- table = table.unique(columnNames);
1744
- } else {
1745
- table = table.index(columnNames);
1746
- }
1747
815
  continue;
1748
816
  }
1749
- if (modelAttribute.name.startsWith('pgvector.') && !composedExtensions.has('pgvector')) {
817
+ const discField = model.fields.find((f) => f.name === fieldName);
818
+ if (discField && discField.typeName !== 'String') {
1750
819
  diagnostics.push({
1751
- code: 'PSL_EXTENSION_NAMESPACE_NOT_COMPOSED',
1752
- message: `Attribute "@@${modelAttribute.name}" uses unrecognized namespace "pgvector". Add extension pack "pgvector" to extensionPacks in prisma-next.config.ts.`,
820
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
821
+ message: `Discriminator field "${fieldName}" on model "${model.name}" must be of type String, but is "${discField.typeName}"`,
1753
822
  sourceId,
1754
- span: modelAttribute.span,
823
+ span: attr.span,
1755
824
  });
1756
825
  continue;
1757
826
  }
1758
- diagnostics.push({
1759
- code: 'PSL_UNSUPPORTED_MODEL_ATTRIBUTE',
1760
- message: `Model "${model.name}" uses unsupported attribute "@@${modelAttribute.name}"`,
1761
- sourceId,
1762
- span: modelAttribute.span,
1763
- });
827
+ discriminatorDeclarations.set(model.name, { fieldName, span: attr.span });
1764
828
  }
1765
829
 
1766
- for (const relationAttribute of relationAttributes) {
1767
- if (relationAttribute.field.list) {
1768
- continue;
1769
- }
1770
-
1771
- if (!modelNames.has(relationAttribute.field.typeName)) {
830
+ if (attr.name === 'base') {
831
+ const baseName = getPositionalArgument(attr, 0);
832
+ const rawValue = getPositionalArgument(attr, 1);
833
+ if (!baseName || !rawValue) {
1772
834
  diagnostics.push({
1773
- code: 'PSL_INVALID_RELATION_TARGET',
1774
- message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${relationAttribute.field.typeName}"`,
835
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
836
+ message: `Model "${model.name}" @@base requires two arguments: base model name and discriminator value`,
1775
837
  sourceId,
1776
- span: relationAttribute.field.span,
838
+ span: attr.span,
1777
839
  });
1778
840
  continue;
1779
841
  }
1780
-
1781
- const parsedRelation = parseRelationAttribute({
1782
- attribute: relationAttribute.relation,
1783
- modelName: model.name,
1784
- fieldName: relationAttribute.field.name,
1785
- sourceId,
1786
- diagnostics,
1787
- });
1788
- if (!parsedRelation) {
1789
- continue;
1790
- }
1791
- if (!parsedRelation.fields || !parsedRelation.references) {
842
+ const value = parseQuotedStringLiteral(rawValue);
843
+ if (value === undefined) {
1792
844
  diagnostics.push({
1793
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1794
- message: `Relation field "${model.name}.${relationAttribute.field.name}" requires fields and references arguments`,
845
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
846
+ message: `Model "${model.name}" @@base discriminator value must be a quoted string literal`,
1795
847
  sourceId,
1796
- span: relationAttribute.relation.span,
848
+ span: attr.span,
1797
849
  });
1798
850
  continue;
1799
851
  }
852
+ baseDeclarations.set(model.name, { baseName, value, span: attr.span });
853
+ }
854
+ }
855
+ }
1800
856
 
1801
- const targetMapping = modelMappings.get(relationAttribute.field.typeName);
1802
- if (!targetMapping) {
1803
- diagnostics.push({
1804
- code: 'PSL_INVALID_RELATION_TARGET',
1805
- message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${relationAttribute.field.typeName}"`,
1806
- sourceId,
1807
- span: relationAttribute.field.span,
1808
- });
1809
- continue;
1810
- }
857
+ return { discriminatorDeclarations, baseDeclarations };
858
+ }
1811
859
 
1812
- const localColumns = mapFieldNamesToColumns({
1813
- modelName: model.name,
1814
- fieldNames: parsedRelation.fields,
1815
- mapping,
860
+ function resolvePolymorphism(
861
+ models: Record<string, ContractModel>,
862
+ discriminatorDeclarations: Map<string, DiscriminatorDeclaration>,
863
+ baseDeclarations: Map<string, BaseDeclaration>,
864
+ modelNames: Set<string>,
865
+ modelMappings: ReadonlyMap<string, ModelNameMapping>,
866
+ sourceId: string,
867
+ diagnostics: ContractSourceDiagnostic[],
868
+ ): Record<string, ContractModel> {
869
+ let patched = models;
870
+
871
+ for (const [modelName, decl] of discriminatorDeclarations) {
872
+ if (baseDeclarations.has(modelName)) {
873
+ diagnostics.push({
874
+ code: 'PSL_DISCRIMINATOR_AND_BASE',
875
+ message: `Model "${modelName}" cannot have both @@discriminator and @@base`,
876
+ sourceId,
877
+ span: decl.span,
878
+ });
879
+ continue;
880
+ }
881
+
882
+ const model = patched[modelName];
883
+ if (!model) continue;
884
+
885
+ if (!Object.hasOwn(model.fields, decl.fieldName)) {
886
+ diagnostics.push({
887
+ code: 'PSL_DISCRIMINATOR_FIELD_NOT_FOUND',
888
+ message: `Discriminator field "${decl.fieldName}" is not a field on model "${modelName}"`,
889
+ sourceId,
890
+ span: decl.span,
891
+ });
892
+ continue;
893
+ }
894
+
895
+ const variants: Record<string, { readonly value: string }> = {};
896
+ const seenValues = new Map<string, string>();
897
+
898
+ for (const [variantName, baseDecl] of baseDeclarations) {
899
+ if (baseDecl.baseName !== modelName) continue;
900
+
901
+ const existingVariant = seenValues.get(baseDecl.value);
902
+ if (existingVariant) {
903
+ diagnostics.push({
904
+ code: 'PSL_DUPLICATE_DISCRIMINATOR_VALUE',
905
+ message: `Discriminator value "${baseDecl.value}" is used by both "${existingVariant}" and "${variantName}" on base model "${modelName}"`,
1816
906
  sourceId,
1817
- diagnostics,
1818
- span: relationAttribute.relation.span,
1819
- contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
907
+ span: baseDecl.span,
1820
908
  });
1821
- if (!localColumns) {
1822
- continue;
1823
- }
1824
- const referencedColumns = mapFieldNamesToColumns({
1825
- modelName: targetMapping.model.name,
1826
- fieldNames: parsedRelation.references,
1827
- mapping: targetMapping,
909
+ continue;
910
+ }
911
+ seenValues.set(baseDecl.value, variantName);
912
+ variants[variantName] = { value: baseDecl.value };
913
+ }
914
+
915
+ if (Object.keys(variants).length === 0) {
916
+ diagnostics.push({
917
+ code: 'PSL_ORPHANED_DISCRIMINATOR',
918
+ message: `Model "${modelName}" has @@discriminator but no variant models declare @@base(${modelName}, ...)`,
919
+ sourceId,
920
+ span: decl.span,
921
+ });
922
+ continue;
923
+ }
924
+
925
+ patched = {
926
+ ...patched,
927
+ [modelName]: { ...model, discriminator: { field: decl.fieldName }, variants },
928
+ };
929
+ }
930
+
931
+ for (const [variantName, baseDecl] of baseDeclarations) {
932
+ if (!modelNames.has(baseDecl.baseName)) {
933
+ diagnostics.push({
934
+ code: 'PSL_BASE_TARGET_NOT_FOUND',
935
+ message: `Model "${variantName}" @@base references non-existent model "${baseDecl.baseName}"`,
936
+ sourceId,
937
+ span: baseDecl.span,
938
+ });
939
+ continue;
940
+ }
941
+
942
+ if (!discriminatorDeclarations.has(baseDecl.baseName)) {
943
+ diagnostics.push({
944
+ code: 'PSL_ORPHANED_BASE',
945
+ message: `Model "${variantName}" declares @@base(${baseDecl.baseName}, ...) but "${baseDecl.baseName}" has no @@discriminator`,
946
+ sourceId,
947
+ span: baseDecl.span,
948
+ });
949
+ continue;
950
+ }
951
+
952
+ if (discriminatorDeclarations.has(variantName)) {
953
+ continue;
954
+ }
955
+
956
+ const variantModel = patched[variantName];
957
+ if (!variantModel) continue;
958
+
959
+ const baseMapping = modelMappings.get(baseDecl.baseName);
960
+ const variantMapping = modelMappings.get(variantName);
961
+ const hasExplicitMap =
962
+ variantMapping?.model.attributes.some((attr) => attr.name === 'map') ?? false;
963
+ const resolvedTable = hasExplicitMap ? variantMapping?.tableName : baseMapping?.tableName;
964
+
965
+ patched = {
966
+ ...patched,
967
+ [variantName]: {
968
+ ...variantModel,
969
+ base: baseDecl.baseName,
970
+ ...(resolvedTable ? { storage: { ...variantModel.storage, table: resolvedTable } } : {}),
971
+ },
972
+ };
973
+ }
974
+
975
+ return patched;
976
+ }
977
+
978
+ export function interpretPslDocumentToSqlContract(
979
+ input: InterpretPslDocumentToSqlContractInput,
980
+ ): Result<Contract, ContractSourceDiagnostics> {
981
+ const sourceId = input.document.ast.sourceId;
982
+ if (!input.target) {
983
+ return notOk({
984
+ summary: 'PSL to SQL contract interpretation failed',
985
+ diagnostics: [
986
+ {
987
+ code: 'PSL_TARGET_CONTEXT_REQUIRED',
988
+ message: 'PSL interpretation requires an explicit target context from composition.',
1828
989
  sourceId,
1829
- diagnostics,
1830
- span: relationAttribute.relation.span,
1831
- contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
1832
- });
1833
- if (!referencedColumns) {
1834
- continue;
1835
- }
1836
- if (localColumns.length !== referencedColumns.length) {
1837
- diagnostics.push({
1838
- code: 'PSL_INVALID_RELATION_ATTRIBUTE',
1839
- message: `Relation field "${model.name}.${relationAttribute.field.name}" must provide the same number of fields and references`,
1840
- sourceId,
1841
- span: relationAttribute.relation.span,
1842
- });
1843
- continue;
1844
- }
990
+ },
991
+ ],
992
+ });
993
+ }
994
+ if (!input.scalarTypeDescriptors) {
995
+ return notOk({
996
+ summary: 'PSL to SQL contract interpretation failed',
997
+ diagnostics: [
998
+ {
999
+ code: 'PSL_SCALAR_TYPE_CONTEXT_REQUIRED',
1000
+ message: 'PSL interpretation requires composed scalar type descriptors.',
1001
+ sourceId,
1002
+ },
1003
+ ],
1004
+ });
1005
+ }
1845
1006
 
1846
- const onDelete = parsedRelation.onDelete
1847
- ? normalizeReferentialAction({
1848
- modelName: model.name,
1849
- fieldName: relationAttribute.field.name,
1850
- actionName: 'onDelete',
1851
- actionToken: parsedRelation.onDelete,
1852
- sourceId,
1853
- span: relationAttribute.field.span,
1854
- diagnostics,
1855
- })
1856
- : undefined;
1857
- const onUpdate = parsedRelation.onUpdate
1858
- ? normalizeReferentialAction({
1859
- modelName: model.name,
1860
- fieldName: relationAttribute.field.name,
1861
- actionName: 'onUpdate',
1862
- actionToken: parsedRelation.onUpdate,
1863
- sourceId,
1864
- span: relationAttribute.field.span,
1865
- diagnostics,
1866
- })
1867
- : undefined;
1868
-
1869
- table = table.foreignKey(
1870
- localColumns,
1871
- {
1872
- table: targetMapping.tableName,
1873
- columns: referencedColumns,
1874
- },
1875
- {
1876
- ...ifDefined('name', parsedRelation.constraintName),
1877
- ...ifDefined('onDelete', onDelete),
1878
- ...ifDefined('onUpdate', onUpdate),
1879
- },
1880
- );
1881
-
1882
- fkRelationMetadata.push({
1883
- declaringModelName: model.name,
1884
- declaringFieldName: relationAttribute.field.name,
1885
- declaringTableName: tableName,
1886
- targetModelName: targetMapping.model.name,
1887
- targetTableName: targetMapping.tableName,
1888
- ...ifDefined('relationName', parsedRelation.relationName),
1889
- localColumns,
1890
- referencedColumns,
1891
- });
1892
- }
1007
+ const diagnostics: ContractSourceDiagnostic[] = mapParserDiagnostics(input.document);
1008
+ const models = input.document.ast.models ?? [];
1009
+ const enums = input.document.ast.enums ?? [];
1010
+ const compositeTypes = input.document.ast.compositeTypes ?? [];
1011
+ const modelNames = new Set(models.map((model) => model.name));
1012
+ const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
1013
+ const composedExtensions = new Set(input.composedExtensionPacks ?? []);
1014
+ const defaultFunctionRegistry =
1015
+ input.controlMutationDefaults?.defaultFunctionRegistry ?? new Map<string, never>();
1016
+ const generatorDescriptors = input.controlMutationDefaults?.generatorDescriptors ?? [];
1017
+ const generatorDescriptorById = new Map<string, MutationDefaultGeneratorDescriptor>();
1018
+ for (const descriptor of generatorDescriptors) {
1019
+ generatorDescriptorById.set(descriptor.id, descriptor);
1020
+ }
1021
+
1022
+ const enumResult = processEnumDeclarations({
1023
+ enums,
1024
+ sourceId,
1025
+ enumTypeConstructor: getAuthoringTypeConstructor(input.authoringContributions, ['enum']),
1026
+ diagnostics,
1027
+ });
1028
+
1029
+ const namedTypeResult = resolveNamedTypeDeclarations({
1030
+ declarations: input.document.ast.types?.declarations ?? [],
1031
+ sourceId,
1032
+ enumTypeDescriptors: enumResult.enumTypeDescriptors,
1033
+ scalarTypeDescriptors: input.scalarTypeDescriptors,
1034
+ composedExtensions,
1035
+ pgvectorVectorConstructor: getAuthoringTypeConstructor(input.authoringContributions, [
1036
+ 'pgvector',
1037
+ 'vector',
1038
+ ]),
1039
+ diagnostics,
1040
+ });
1041
+
1042
+ const storageTypes = { ...enumResult.storageTypes, ...namedTypeResult.storageTypes };
1043
+
1044
+ const modelMappings = buildModelMappings(models, diagnostics, sourceId);
1045
+ const modelNodes: ModelNode[] = [];
1046
+ const fkRelationMetadata: FkRelationMetadata[] = [];
1047
+ const backrelationCandidates: ModelBackrelationCandidate[] = [];
1048
+ const modelResolvedFields = new Map<string, readonly ResolvedField[]>();
1893
1049
 
1894
- return table;
1050
+ for (const model of models) {
1051
+ const mapping = modelMappings.get(model.name);
1052
+ if (!mapping) {
1053
+ continue;
1054
+ }
1055
+ const result = buildModelNodeFromPsl({
1056
+ model,
1057
+ mapping,
1058
+ modelMappings,
1059
+ modelNames,
1060
+ compositeTypeNames,
1061
+ enumTypeDescriptors: enumResult.enumTypeDescriptors,
1062
+ namedTypeDescriptors: namedTypeResult.namedTypeDescriptors,
1063
+ namedTypeBaseTypes: namedTypeResult.namedTypeBaseTypes,
1064
+ composedExtensions,
1065
+ authoringContributions: input.authoringContributions,
1066
+ defaultFunctionRegistry,
1067
+ generatorDescriptorById,
1068
+ scalarTypeDescriptors: input.scalarTypeDescriptors,
1069
+ sourceId,
1070
+ diagnostics,
1895
1071
  });
1072
+ modelNodes.push(result.modelNode);
1073
+ fkRelationMetadata.push(...result.fkRelationMetadata);
1074
+ backrelationCandidates.push(...result.backrelationCandidates);
1075
+ modelResolvedFields.set(model.name, result.resolvedFields);
1896
1076
  }
1897
1077
 
1898
1078
  const { modelRelations, fkRelationsByPair } = indexFkRelations({ fkRelationMetadata });
@@ -1903,11 +1083,12 @@ export function interpretPslDocumentToSqlContractIR(
1903
1083
  diagnostics,
1904
1084
  sourceId,
1905
1085
  });
1906
- builder = emitModelsWithRelations({
1907
- builder,
1908
- resolvedModels,
1909
- modelRelations,
1910
- });
1086
+
1087
+ const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations(
1088
+ models,
1089
+ sourceId,
1090
+ diagnostics,
1091
+ );
1911
1092
 
1912
1093
  if (diagnostics.length > 0) {
1913
1094
  const dedupedDiagnostics = diagnostics.filter(
@@ -1923,11 +1104,77 @@ export function interpretPslDocumentToSqlContractIR(
1923
1104
  );
1924
1105
 
1925
1106
  return notOk({
1926
- summary: 'PSL to SQL Contract IR normalization failed',
1107
+ summary: 'PSL to SQL contract interpretation failed',
1927
1108
  diagnostics: dedupedDiagnostics,
1928
1109
  });
1929
1110
  }
1930
1111
 
1931
- const contract = builder.build() as ContractIR;
1932
- return ok(contract);
1112
+ const contract = buildSqlContractFromDefinition({
1113
+ target: input.target,
1114
+ ...ifDefined(
1115
+ 'extensionPacks',
1116
+ buildComposedExtensionPackRefs(
1117
+ input.target,
1118
+ [...composedExtensions].sort(compareStrings),
1119
+ input.composedExtensionPackRefs,
1120
+ ),
1121
+ ),
1122
+ ...(Object.keys(storageTypes).length > 0 ? { storageTypes } : {}),
1123
+ models: modelNodes.map((model) => ({
1124
+ ...model,
1125
+ ...(modelRelations.has(model.modelName)
1126
+ ? {
1127
+ relations: [...(modelRelations.get(model.modelName) ?? [])].sort((left, right) =>
1128
+ compareStrings(left.fieldName, right.fieldName),
1129
+ ),
1130
+ }
1131
+ : {}),
1132
+ })),
1133
+ });
1134
+
1135
+ const valueObjects = buildValueObjects(
1136
+ compositeTypes,
1137
+ enumResult.enumTypeDescriptors,
1138
+ namedTypeResult.namedTypeDescriptors,
1139
+ input.scalarTypeDescriptors,
1140
+ diagnostics,
1141
+ sourceId,
1142
+ );
1143
+
1144
+ let patchedModels = patchModelDomainFields(
1145
+ contract.models as Record<string, ContractModel>,
1146
+ modelResolvedFields,
1147
+ );
1148
+
1149
+ const polyDiagnostics: ContractSourceDiagnostic[] = [];
1150
+ patchedModels = resolvePolymorphism(
1151
+ patchedModels,
1152
+ discriminatorDeclarations,
1153
+ baseDeclarations,
1154
+ modelNames,
1155
+ modelMappings,
1156
+ sourceId,
1157
+ polyDiagnostics,
1158
+ );
1159
+
1160
+ if (polyDiagnostics.length > 0) {
1161
+ return notOk({
1162
+ summary: 'PSL to SQL contract interpretation failed',
1163
+ diagnostics: polyDiagnostics,
1164
+ });
1165
+ }
1166
+
1167
+ const variantModelNames = new Set(baseDeclarations.keys());
1168
+ const filteredRoots = Object.fromEntries(
1169
+ Object.entries(contract.roots).filter(([, modelName]) => !variantModelNames.has(modelName)),
1170
+ );
1171
+
1172
+ const patchedContract: Contract = {
1173
+ ...contract,
1174
+ roots: filteredRoots,
1175
+ models: patchedModels,
1176
+ ...(Object.keys(valueObjects).length > 0 ? { valueObjects } : {}),
1177
+ };
1178
+
1179
+ return ok(patchedContract);
1933
1180
  }