@prisma-next/sql-contract-psl 0.3.0-dev.55 → 0.3.0-dev.63
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.
- package/LICENSE +201 -0
- package/README.md +24 -0
- package/dist/index.d.mts +40 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/interpreter-IXr5c7s7.mjs +1376 -0
- package/dist/interpreter-IXr5c7s7.mjs.map +1 -0
- package/dist/provider.d.mts +1 -1
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +4 -3
- package/dist/provider.mjs.map +1 -1
- package/package.json +6 -6
- package/src/default-function-registry.ts +510 -0
- package/src/interpreter.ts +629 -67
- package/src/provider.ts +3 -1
- package/dist/interpreter-_6-Xk1_m.mjs +0 -661
- package/dist/interpreter-_6-Xk1_m.mjs.map +0 -1
package/src/interpreter.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { TargetPackRef } from '@prisma-next/contract/framework-components';
|
|
2
|
-
import type { ContractIR } from '@prisma-next/contract/ir';
|
|
3
|
-
import type { ColumnDefault } from '@prisma-next/contract/types';
|
|
4
1
|
import type {
|
|
5
2
|
ContractSourceDiagnostic,
|
|
6
3
|
ContractSourceDiagnosticSpan,
|
|
7
4
|
ContractSourceDiagnostics,
|
|
8
|
-
} from '@prisma-next/
|
|
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';
|
|
9
9
|
import type {
|
|
10
10
|
ParsePslDocumentResult,
|
|
11
11
|
PslAttribute,
|
|
@@ -14,7 +14,15 @@ import type {
|
|
|
14
14
|
PslSpan,
|
|
15
15
|
} from '@prisma-next/psl-parser';
|
|
16
16
|
import { defineContract } from '@prisma-next/sql-contract-ts/contract-builder';
|
|
17
|
+
import { assertDefined, invariant } from '@prisma-next/utils/assertions';
|
|
18
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
17
19
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
20
|
+
import {
|
|
21
|
+
createBuiltinDefaultFunctionRegistry,
|
|
22
|
+
type DefaultFunctionRegistry,
|
|
23
|
+
lowerDefaultFunctionWithRegistry,
|
|
24
|
+
parseDefaultFunctionCall,
|
|
25
|
+
} from './default-function-registry';
|
|
18
26
|
|
|
19
27
|
type ColumnDescriptor = {
|
|
20
28
|
readonly codecId: string;
|
|
@@ -27,6 +35,7 @@ export interface InterpretPslDocumentToSqlContractIRInput {
|
|
|
27
35
|
readonly document: ParsePslDocumentResult;
|
|
28
36
|
readonly target?: TargetPackRef<'sql', 'postgres'>;
|
|
29
37
|
readonly composedExtensionPacks?: readonly string[];
|
|
38
|
+
readonly defaultFunctionRegistry?: DefaultFunctionRegistry;
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
const DEFAULT_POSTGRES_TARGET: TargetPackRef<'sql', 'postgres'> = {
|
|
@@ -50,6 +59,32 @@ const SCALAR_COLUMN_MAP: Record<string, ColumnDescriptor> = {
|
|
|
50
59
|
Bytes: { codecId: 'pg/bytea@1', nativeType: 'bytea' },
|
|
51
60
|
};
|
|
52
61
|
|
|
62
|
+
const GENERATED_ID_COLUMN_MAP: Partial<Record<string, ColumnDescriptor>> = {
|
|
63
|
+
ulid: { codecId: 'sql/char@1', nativeType: 'character', typeParams: { length: 26 } },
|
|
64
|
+
uuidv7: { codecId: 'sql/char@1', nativeType: 'character', typeParams: { length: 36 } },
|
|
65
|
+
uuidv4: { codecId: 'sql/char@1', nativeType: 'character', typeParams: { length: 36 } },
|
|
66
|
+
cuid2: { codecId: 'sql/char@1', nativeType: 'character', typeParams: { length: 24 } },
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
function resolveGeneratedColumnDescriptor(
|
|
70
|
+
executionDefault: ExecutionMutationDefaultValue,
|
|
71
|
+
): ColumnDescriptor | undefined {
|
|
72
|
+
if (executionDefault.kind !== 'generator') {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (executionDefault.id === 'nanoid') {
|
|
77
|
+
const rawSize = executionDefault.params?.['size'];
|
|
78
|
+
const length =
|
|
79
|
+
typeof rawSize === 'number' && Number.isInteger(rawSize) && rawSize >= 2 && rawSize <= 255
|
|
80
|
+
? rawSize
|
|
81
|
+
: 21;
|
|
82
|
+
return { codecId: 'sql/char@1', nativeType: 'character', typeParams: { length } };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return GENERATED_ID_COLUMN_MAP[executionDefault.id];
|
|
86
|
+
}
|
|
87
|
+
|
|
53
88
|
const REFERENTIAL_ACTION_MAP = {
|
|
54
89
|
NoAction: 'noAction',
|
|
55
90
|
Restrict: 'restrict',
|
|
@@ -68,10 +103,60 @@ type ResolvedField = {
|
|
|
68
103
|
readonly columnName: string;
|
|
69
104
|
readonly descriptor: ColumnDescriptor;
|
|
70
105
|
readonly defaultValue?: ColumnDefault;
|
|
106
|
+
readonly executionDefault?: ExecutionMutationDefaultValue;
|
|
71
107
|
readonly isId: boolean;
|
|
72
108
|
readonly isUnique: boolean;
|
|
73
109
|
};
|
|
74
110
|
|
|
111
|
+
type ParsedRelationAttribute = {
|
|
112
|
+
readonly relationName?: string;
|
|
113
|
+
readonly fields?: readonly string[];
|
|
114
|
+
readonly references?: readonly string[];
|
|
115
|
+
readonly onDelete?: string;
|
|
116
|
+
readonly onUpdate?: string;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
type FkRelationMetadata = {
|
|
120
|
+
readonly declaringModelName: string;
|
|
121
|
+
readonly declaringFieldName: string;
|
|
122
|
+
readonly declaringTableName: string;
|
|
123
|
+
readonly targetModelName: string;
|
|
124
|
+
readonly targetTableName: string;
|
|
125
|
+
readonly relationName?: string;
|
|
126
|
+
readonly localColumns: readonly string[];
|
|
127
|
+
readonly referencedColumns: readonly string[];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
type ModelBackrelationCandidate = {
|
|
131
|
+
readonly modelName: string;
|
|
132
|
+
readonly tableName: string;
|
|
133
|
+
readonly field: PslField;
|
|
134
|
+
readonly targetModelName: string;
|
|
135
|
+
readonly relationName?: string;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
type ModelRelationMetadata = {
|
|
139
|
+
readonly fieldName: string;
|
|
140
|
+
readonly toModel: string;
|
|
141
|
+
readonly toTable: string;
|
|
142
|
+
readonly cardinality: '1:N' | 'N:1';
|
|
143
|
+
readonly parentTable: string;
|
|
144
|
+
readonly parentColumns: readonly string[];
|
|
145
|
+
readonly childTable: string;
|
|
146
|
+
readonly childColumns: readonly string[];
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
type ResolvedModelEntry = {
|
|
150
|
+
readonly model: PslModel;
|
|
151
|
+
readonly mapping: ModelNameMapping;
|
|
152
|
+
readonly resolvedFields: readonly ResolvedField[];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function fkRelationPairKey(declaringModelName: string, targetModelName: string): string {
|
|
156
|
+
// NOTE: We assume PSL model identifiers do not contain the `::` separator.
|
|
157
|
+
return `${declaringModelName}::${targetModelName}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
75
160
|
type ModelNameMapping = {
|
|
76
161
|
readonly model: PslModel;
|
|
77
162
|
readonly tableName: string;
|
|
@@ -83,6 +168,10 @@ type DynamicTableBuilder = {
|
|
|
83
168
|
name: string,
|
|
84
169
|
options: { type: ColumnDescriptor; nullable?: true; default?: ColumnDefault },
|
|
85
170
|
): DynamicTableBuilder;
|
|
171
|
+
generated(
|
|
172
|
+
name: string,
|
|
173
|
+
options: { type: ColumnDescriptor; generated: ExecutionMutationDefaultValue },
|
|
174
|
+
): DynamicTableBuilder;
|
|
86
175
|
unique(columns: readonly string[]): DynamicTableBuilder;
|
|
87
176
|
primaryKey(columns: readonly string[]): DynamicTableBuilder;
|
|
88
177
|
index(columns: readonly string[]): DynamicTableBuilder;
|
|
@@ -95,6 +184,20 @@ type DynamicTableBuilder = {
|
|
|
95
184
|
|
|
96
185
|
type DynamicModelBuilder = {
|
|
97
186
|
field(name: string, column: string): DynamicModelBuilder;
|
|
187
|
+
relation(
|
|
188
|
+
name: string,
|
|
189
|
+
options: {
|
|
190
|
+
toModel: string;
|
|
191
|
+
toTable: string;
|
|
192
|
+
cardinality: '1:1' | '1:N' | 'N:1';
|
|
193
|
+
on: {
|
|
194
|
+
parentTable: string;
|
|
195
|
+
parentColumns: readonly string[];
|
|
196
|
+
childTable: string;
|
|
197
|
+
childColumns: readonly string[];
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
): DynamicModelBuilder;
|
|
98
201
|
};
|
|
99
202
|
|
|
100
203
|
type DynamicContractBuilder = {
|
|
@@ -145,6 +248,21 @@ function getPositionalArgument(attribute: PslAttribute, index = 0): string | und
|
|
|
145
248
|
return entry.value;
|
|
146
249
|
}
|
|
147
250
|
|
|
251
|
+
function getPositionalArgumentEntry(
|
|
252
|
+
attribute: PslAttribute,
|
|
253
|
+
index = 0,
|
|
254
|
+
): { value: string; span: PslSpan } | undefined {
|
|
255
|
+
const entries = attribute.args.filter((arg) => arg.kind === 'positional');
|
|
256
|
+
const entry = entries[index];
|
|
257
|
+
if (!entry || entry.kind !== 'positional') {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
value: entry.value,
|
|
262
|
+
span: entry.span,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
148
266
|
function unquoteStringLiteral(value: string): string {
|
|
149
267
|
const trimmed = value.trim();
|
|
150
268
|
const match = trimmed.match(/^(['"])(.*)\1$/);
|
|
@@ -156,6 +274,8 @@ function unquoteStringLiteral(value: string): string {
|
|
|
156
274
|
|
|
157
275
|
function parseQuotedStringLiteral(value: string): string | undefined {
|
|
158
276
|
const trimmed = value.trim();
|
|
277
|
+
// This intentionally accepts either '...' or "..." and relies on PSL's
|
|
278
|
+
// own string literal rules to disallow unescaped interior delimiters.
|
|
159
279
|
const match = trimmed.match(/^(['"])(.*)\1$/);
|
|
160
280
|
if (!match) {
|
|
161
281
|
return undefined;
|
|
@@ -176,14 +296,8 @@ function parseFieldList(value: string): readonly string[] | undefined {
|
|
|
176
296
|
return parts;
|
|
177
297
|
}
|
|
178
298
|
|
|
179
|
-
function
|
|
299
|
+
function parseDefaultLiteralValue(expression: string): ColumnDefault | undefined {
|
|
180
300
|
const trimmed = expression.trim();
|
|
181
|
-
if (trimmed === 'autoincrement()') {
|
|
182
|
-
return { kind: 'function', expression: 'autoincrement()' };
|
|
183
|
-
}
|
|
184
|
-
if (trimmed === 'now()') {
|
|
185
|
-
return { kind: 'function', expression: 'now()' };
|
|
186
|
-
}
|
|
187
301
|
if (trimmed === 'true' || trimmed === 'false') {
|
|
188
302
|
return { kind: 'literal', value: trimmed === 'true' };
|
|
189
303
|
}
|
|
@@ -197,6 +311,78 @@ function parseDefaultValueExpression(expression: string): ColumnDefault | undefi
|
|
|
197
311
|
return undefined;
|
|
198
312
|
}
|
|
199
313
|
|
|
314
|
+
function lowerDefaultForField(input: {
|
|
315
|
+
readonly modelName: string;
|
|
316
|
+
readonly fieldName: string;
|
|
317
|
+
readonly defaultAttribute: PslAttribute;
|
|
318
|
+
readonly sourceId: string;
|
|
319
|
+
readonly defaultFunctionRegistry: DefaultFunctionRegistry;
|
|
320
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
321
|
+
}): {
|
|
322
|
+
readonly defaultValue?: ColumnDefault;
|
|
323
|
+
readonly executionDefault?: ExecutionMutationDefaultValue;
|
|
324
|
+
} {
|
|
325
|
+
const positionalEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'positional');
|
|
326
|
+
const namedEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'named');
|
|
327
|
+
|
|
328
|
+
if (namedEntries.length > 0 || positionalEntries.length !== 1) {
|
|
329
|
+
input.diagnostics.push({
|
|
330
|
+
code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
|
|
331
|
+
message: `Field "${input.modelName}.${input.fieldName}" requires exactly one positional @default(...) expression.`,
|
|
332
|
+
sourceId: input.sourceId,
|
|
333
|
+
span: input.defaultAttribute.span,
|
|
334
|
+
});
|
|
335
|
+
return {};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const expressionEntry = getPositionalArgumentEntry(input.defaultAttribute);
|
|
339
|
+
if (!expressionEntry) {
|
|
340
|
+
input.diagnostics.push({
|
|
341
|
+
code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
|
|
342
|
+
message: `Field "${input.modelName}.${input.fieldName}" requires a positional @default(...) expression.`,
|
|
343
|
+
sourceId: input.sourceId,
|
|
344
|
+
span: input.defaultAttribute.span,
|
|
345
|
+
});
|
|
346
|
+
return {};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const literalDefault = parseDefaultLiteralValue(expressionEntry.value);
|
|
350
|
+
if (literalDefault) {
|
|
351
|
+
return { defaultValue: literalDefault };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const defaultFunctionCall = parseDefaultFunctionCall(expressionEntry.value, expressionEntry.span);
|
|
355
|
+
if (!defaultFunctionCall) {
|
|
356
|
+
input.diagnostics.push({
|
|
357
|
+
code: 'PSL_INVALID_DEFAULT_VALUE',
|
|
358
|
+
message: `Unsupported default value "${expressionEntry.value}"`,
|
|
359
|
+
sourceId: input.sourceId,
|
|
360
|
+
span: input.defaultAttribute.span,
|
|
361
|
+
});
|
|
362
|
+
return {};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const lowered = lowerDefaultFunctionWithRegistry({
|
|
366
|
+
call: defaultFunctionCall,
|
|
367
|
+
registry: input.defaultFunctionRegistry,
|
|
368
|
+
context: {
|
|
369
|
+
sourceId: input.sourceId,
|
|
370
|
+
modelName: input.modelName,
|
|
371
|
+
fieldName: input.fieldName,
|
|
372
|
+
},
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (!lowered.ok) {
|
|
376
|
+
input.diagnostics.push(lowered.diagnostic);
|
|
377
|
+
return {};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (lowered.value.kind === 'storage') {
|
|
381
|
+
return { defaultValue: lowered.value.defaultValue };
|
|
382
|
+
}
|
|
383
|
+
return { executionDefault: lowered.value.generated };
|
|
384
|
+
}
|
|
385
|
+
|
|
200
386
|
function parseMapName(input: {
|
|
201
387
|
readonly attribute: PslAttribute | undefined;
|
|
202
388
|
readonly defaultValue: string;
|
|
@@ -288,6 +474,7 @@ function collectResolvedFields(
|
|
|
288
474
|
namedTypeBaseTypes: Map<string, string>,
|
|
289
475
|
modelNames: Set<string>,
|
|
290
476
|
composedExtensions: Set<string>,
|
|
477
|
+
defaultFunctionRegistry: DefaultFunctionRegistry,
|
|
291
478
|
diagnostics: ContractSourceDiagnostic[],
|
|
292
479
|
sourceId: string,
|
|
293
480
|
): ResolvedField[] {
|
|
@@ -295,9 +482,12 @@ function collectResolvedFields(
|
|
|
295
482
|
|
|
296
483
|
for (const field of model.fields) {
|
|
297
484
|
if (field.list) {
|
|
485
|
+
if (modelNames.has(field.typeName)) {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
298
488
|
diagnostics.push({
|
|
299
489
|
code: 'PSL_UNSUPPORTED_FIELD_LIST',
|
|
300
|
-
message: `Field "${model.name}.${field.name}" uses list
|
|
490
|
+
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.`,
|
|
301
491
|
sourceId,
|
|
302
492
|
span: field.span,
|
|
303
493
|
});
|
|
@@ -387,22 +577,42 @@ function collectResolvedFields(
|
|
|
387
577
|
}
|
|
388
578
|
|
|
389
579
|
const defaultAttribute = getAttribute(field.attributes, 'default');
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
580
|
+
const loweredDefault = defaultAttribute
|
|
581
|
+
? lowerDefaultForField({
|
|
582
|
+
modelName: model.name,
|
|
583
|
+
fieldName: field.name,
|
|
584
|
+
defaultAttribute,
|
|
585
|
+
sourceId,
|
|
586
|
+
defaultFunctionRegistry,
|
|
587
|
+
diagnostics,
|
|
588
|
+
})
|
|
589
|
+
: {};
|
|
590
|
+
if (field.optional && loweredDefault.executionDefault) {
|
|
591
|
+
const generatorDescription =
|
|
592
|
+
loweredDefault.executionDefault.kind === 'generator'
|
|
593
|
+
? `"${loweredDefault.executionDefault.id}"`
|
|
594
|
+
: 'for this field';
|
|
393
595
|
diagnostics.push({
|
|
394
|
-
code: '
|
|
395
|
-
message: `
|
|
596
|
+
code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
|
|
597
|
+
message: `Field "${model.name}.${field.name}" cannot be optional when using execution default ${generatorDescription}. Remove "?" or use a storage default.`,
|
|
396
598
|
sourceId,
|
|
397
|
-
span: defaultAttribute.span,
|
|
599
|
+
span: defaultAttribute?.span ?? field.span,
|
|
398
600
|
});
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
if (loweredDefault.executionDefault) {
|
|
604
|
+
const generatedDescriptor = resolveGeneratedColumnDescriptor(loweredDefault.executionDefault);
|
|
605
|
+
if (generatedDescriptor) {
|
|
606
|
+
descriptor = generatedDescriptor;
|
|
607
|
+
}
|
|
399
608
|
}
|
|
400
609
|
const mappedColumnName = mapping.fieldColumns.get(field.name) ?? field.name;
|
|
401
610
|
resolvedFields.push({
|
|
402
611
|
field,
|
|
403
612
|
columnName: mappedColumnName,
|
|
404
613
|
descriptor,
|
|
405
|
-
...(defaultValue
|
|
614
|
+
...ifDefined('defaultValue', loweredDefault.defaultValue),
|
|
615
|
+
...ifDefined('executionDefault', loweredDefault.executionDefault),
|
|
406
616
|
isId: Boolean(getAttribute(field.attributes, 'id')),
|
|
407
617
|
isUnique: Boolean(getAttribute(field.attributes, 'unique')),
|
|
408
618
|
});
|
|
@@ -420,6 +630,155 @@ function hasSameSpan(a: PslSpan, b: ContractSourceDiagnosticSpan): boolean {
|
|
|
420
630
|
);
|
|
421
631
|
}
|
|
422
632
|
|
|
633
|
+
function compareStrings(left: string, right: string): -1 | 0 | 1 {
|
|
634
|
+
if (left < right) {
|
|
635
|
+
return -1;
|
|
636
|
+
}
|
|
637
|
+
if (left > right) {
|
|
638
|
+
return 1;
|
|
639
|
+
}
|
|
640
|
+
return 0;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function indexFkRelations(input: { readonly fkRelationMetadata: readonly FkRelationMetadata[] }): {
|
|
644
|
+
readonly modelRelations: Map<string, ModelRelationMetadata[]>;
|
|
645
|
+
readonly fkRelationsByPair: Map<string, FkRelationMetadata[]>;
|
|
646
|
+
} {
|
|
647
|
+
const modelRelations = new Map<string, ModelRelationMetadata[]>();
|
|
648
|
+
const fkRelationsByPair = new Map<string, FkRelationMetadata[]>();
|
|
649
|
+
|
|
650
|
+
for (const relation of input.fkRelationMetadata) {
|
|
651
|
+
const existing = modelRelations.get(relation.declaringModelName);
|
|
652
|
+
const current = existing ?? [];
|
|
653
|
+
if (!existing) {
|
|
654
|
+
modelRelations.set(relation.declaringModelName, current);
|
|
655
|
+
}
|
|
656
|
+
current.push({
|
|
657
|
+
fieldName: relation.declaringFieldName,
|
|
658
|
+
toModel: relation.targetModelName,
|
|
659
|
+
toTable: relation.targetTableName,
|
|
660
|
+
cardinality: 'N:1',
|
|
661
|
+
parentTable: relation.declaringTableName,
|
|
662
|
+
parentColumns: relation.localColumns,
|
|
663
|
+
childTable: relation.targetTableName,
|
|
664
|
+
childColumns: relation.referencedColumns,
|
|
665
|
+
});
|
|
666
|
+
|
|
667
|
+
const pairKey = fkRelationPairKey(relation.declaringModelName, relation.targetModelName);
|
|
668
|
+
const pairRelations = fkRelationsByPair.get(pairKey);
|
|
669
|
+
if (!pairRelations) {
|
|
670
|
+
fkRelationsByPair.set(pairKey, [relation]);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
pairRelations.push(relation);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return { modelRelations, fkRelationsByPair };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function applyBackrelationCandidates(input: {
|
|
680
|
+
readonly backrelationCandidates: readonly ModelBackrelationCandidate[];
|
|
681
|
+
readonly fkRelationsByPair: Map<string, readonly FkRelationMetadata[]>;
|
|
682
|
+
readonly modelRelations: Map<string, ModelRelationMetadata[]>;
|
|
683
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
684
|
+
readonly sourceId: string;
|
|
685
|
+
}): void {
|
|
686
|
+
for (const candidate of input.backrelationCandidates) {
|
|
687
|
+
const pairKey = fkRelationPairKey(candidate.targetModelName, candidate.modelName);
|
|
688
|
+
const pairMatches = input.fkRelationsByPair.get(pairKey) ?? [];
|
|
689
|
+
const matches = candidate.relationName
|
|
690
|
+
? pairMatches.filter((relation) => relation.relationName === candidate.relationName)
|
|
691
|
+
: [...pairMatches];
|
|
692
|
+
|
|
693
|
+
if (matches.length === 0) {
|
|
694
|
+
input.diagnostics.push({
|
|
695
|
+
code: 'PSL_ORPHANED_BACKRELATION_LIST',
|
|
696
|
+
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.`,
|
|
697
|
+
sourceId: input.sourceId,
|
|
698
|
+
span: candidate.field.span,
|
|
699
|
+
});
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (matches.length > 1) {
|
|
703
|
+
input.diagnostics.push({
|
|
704
|
+
code: 'PSL_AMBIGUOUS_BACKRELATION_LIST',
|
|
705
|
+
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.`,
|
|
706
|
+
sourceId: input.sourceId,
|
|
707
|
+
span: candidate.field.span,
|
|
708
|
+
});
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
invariant(matches.length === 1, 'Backrelation matching requires exactly one match');
|
|
713
|
+
const matched = matches[0];
|
|
714
|
+
assertDefined(matched, 'Backrelation matching requires a defined relation match');
|
|
715
|
+
|
|
716
|
+
const existing = input.modelRelations.get(candidate.modelName);
|
|
717
|
+
const current = existing ?? [];
|
|
718
|
+
if (!existing) {
|
|
719
|
+
input.modelRelations.set(candidate.modelName, current);
|
|
720
|
+
}
|
|
721
|
+
current.push({
|
|
722
|
+
fieldName: candidate.field.name,
|
|
723
|
+
toModel: matched.declaringModelName,
|
|
724
|
+
toTable: matched.declaringTableName,
|
|
725
|
+
cardinality: '1:N',
|
|
726
|
+
parentTable: candidate.tableName,
|
|
727
|
+
parentColumns: matched.referencedColumns,
|
|
728
|
+
childTable: matched.declaringTableName,
|
|
729
|
+
childColumns: matched.localColumns,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function emitModelsWithRelations(input: {
|
|
735
|
+
readonly builder: DynamicContractBuilder;
|
|
736
|
+
readonly resolvedModels: ResolvedModelEntry[];
|
|
737
|
+
readonly modelRelations: Map<string, readonly ModelRelationMetadata[]>;
|
|
738
|
+
}): DynamicContractBuilder {
|
|
739
|
+
let nextBuilder = input.builder;
|
|
740
|
+
|
|
741
|
+
const sortedModels = input.resolvedModels.sort((left, right) => {
|
|
742
|
+
const tableComparison = compareStrings(left.mapping.tableName, right.mapping.tableName);
|
|
743
|
+
if (tableComparison === 0) {
|
|
744
|
+
return compareStrings(left.model.name, right.model.name);
|
|
745
|
+
}
|
|
746
|
+
return tableComparison;
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
for (const entry of sortedModels) {
|
|
750
|
+
const relationEntries = [...(input.modelRelations.get(entry.model.name) ?? [])].sort(
|
|
751
|
+
(left, right) => compareStrings(left.fieldName, right.fieldName),
|
|
752
|
+
);
|
|
753
|
+
nextBuilder = nextBuilder.model(
|
|
754
|
+
entry.model.name,
|
|
755
|
+
entry.mapping.tableName,
|
|
756
|
+
(modelBuilder: DynamicModelBuilder) => {
|
|
757
|
+
let next = modelBuilder;
|
|
758
|
+
for (const resolvedField of entry.resolvedFields) {
|
|
759
|
+
next = next.field(resolvedField.field.name, resolvedField.columnName);
|
|
760
|
+
}
|
|
761
|
+
for (const relation of relationEntries) {
|
|
762
|
+
next = next.relation(relation.fieldName, {
|
|
763
|
+
toModel: relation.toModel,
|
|
764
|
+
toTable: relation.toTable,
|
|
765
|
+
cardinality: relation.cardinality,
|
|
766
|
+
on: {
|
|
767
|
+
parentTable: relation.parentTable,
|
|
768
|
+
parentColumns: relation.parentColumns,
|
|
769
|
+
childTable: relation.childTable,
|
|
770
|
+
childColumns: relation.childColumns,
|
|
771
|
+
},
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
return next;
|
|
775
|
+
},
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return nextBuilder;
|
|
780
|
+
}
|
|
781
|
+
|
|
423
782
|
function mapParserDiagnostics(document: ParsePslDocumentResult): ContractSourceDiagnostic[] {
|
|
424
783
|
return document.diagnostics.map((diagnostic) => ({
|
|
425
784
|
code: diagnostic.code,
|
|
@@ -547,31 +906,79 @@ function buildModelMappings(
|
|
|
547
906
|
return result;
|
|
548
907
|
}
|
|
549
908
|
|
|
909
|
+
function validateNavigationListFieldAttributes(input: {
|
|
910
|
+
readonly modelName: string;
|
|
911
|
+
readonly field: PslField;
|
|
912
|
+
readonly sourceId: string;
|
|
913
|
+
readonly composedExtensions: Set<string>;
|
|
914
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
915
|
+
}): boolean {
|
|
916
|
+
let valid = true;
|
|
917
|
+
for (const attribute of input.field.attributes) {
|
|
918
|
+
if (attribute.name === 'relation') {
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (attribute.name.startsWith('pgvector.') && !input.composedExtensions.has('pgvector')) {
|
|
922
|
+
input.diagnostics.push({
|
|
923
|
+
code: 'PSL_EXTENSION_NAMESPACE_NOT_COMPOSED',
|
|
924
|
+
message: `Attribute "@${attribute.name}" uses unrecognized namespace "pgvector". Add extension pack "pgvector" to extensionPacks in prisma-next.config.ts.`,
|
|
925
|
+
sourceId: input.sourceId,
|
|
926
|
+
span: attribute.span,
|
|
927
|
+
});
|
|
928
|
+
valid = false;
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
input.diagnostics.push({
|
|
932
|
+
code: 'PSL_UNSUPPORTED_FIELD_ATTRIBUTE',
|
|
933
|
+
message: `Field "${input.modelName}.${input.field.name}" uses unsupported attribute "@${attribute.name}"`,
|
|
934
|
+
sourceId: input.sourceId,
|
|
935
|
+
span: attribute.span,
|
|
936
|
+
});
|
|
937
|
+
valid = false;
|
|
938
|
+
}
|
|
939
|
+
return valid;
|
|
940
|
+
}
|
|
941
|
+
|
|
550
942
|
function parseRelationAttribute(input: {
|
|
551
943
|
readonly attribute: PslAttribute;
|
|
552
944
|
readonly modelName: string;
|
|
553
945
|
readonly fieldName: string;
|
|
554
946
|
readonly sourceId: string;
|
|
555
947
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
556
|
-
}):
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
948
|
+
}): ParsedRelationAttribute | undefined {
|
|
949
|
+
const positionalEntries = input.attribute.args.filter((arg) => arg.kind === 'positional');
|
|
950
|
+
if (positionalEntries.length > 1) {
|
|
951
|
+
input.diagnostics.push({
|
|
952
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
953
|
+
message: `Relation field "${input.modelName}.${input.fieldName}" has too many positional arguments`,
|
|
954
|
+
sourceId: input.sourceId,
|
|
955
|
+
span: input.attribute.span,
|
|
956
|
+
});
|
|
957
|
+
return undefined;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
let relationNameFromPositional: string | undefined;
|
|
961
|
+
const positionalNameEntry = getPositionalArgumentEntry(input.attribute);
|
|
962
|
+
if (positionalNameEntry) {
|
|
963
|
+
const parsedName = parseQuotedStringLiteral(positionalNameEntry.value);
|
|
964
|
+
if (!parsedName) {
|
|
566
965
|
input.diagnostics.push({
|
|
567
966
|
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
568
|
-
message: `Relation field "${input.modelName}.${input.fieldName}" must
|
|
967
|
+
message: `Relation field "${input.modelName}.${input.fieldName}" positional relation name must be a quoted string literal`,
|
|
569
968
|
sourceId: input.sourceId,
|
|
570
|
-
span:
|
|
969
|
+
span: positionalNameEntry.span,
|
|
571
970
|
});
|
|
572
971
|
return undefined;
|
|
573
972
|
}
|
|
973
|
+
relationNameFromPositional = parsedName;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
for (const arg of input.attribute.args) {
|
|
977
|
+
if (arg.kind === 'positional') {
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
574
980
|
if (
|
|
981
|
+
arg.name !== 'name' &&
|
|
575
982
|
arg.name !== 'fields' &&
|
|
576
983
|
arg.name !== 'references' &&
|
|
577
984
|
arg.name !== 'onDelete' &&
|
|
@@ -587,38 +994,79 @@ function parseRelationAttribute(input: {
|
|
|
587
994
|
}
|
|
588
995
|
}
|
|
589
996
|
|
|
590
|
-
const
|
|
591
|
-
const
|
|
592
|
-
|
|
997
|
+
const namedRelationNameRaw = getNamedArgument(input.attribute, 'name');
|
|
998
|
+
const namedRelationName = namedRelationNameRaw
|
|
999
|
+
? parseQuotedStringLiteral(namedRelationNameRaw)
|
|
1000
|
+
: undefined;
|
|
1001
|
+
if (namedRelationNameRaw && !namedRelationName) {
|
|
593
1002
|
input.diagnostics.push({
|
|
594
1003
|
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
595
|
-
message: `Relation field "${input.modelName}.${input.fieldName}"
|
|
1004
|
+
message: `Relation field "${input.modelName}.${input.fieldName}" named relation name must be a quoted string literal`,
|
|
596
1005
|
sourceId: input.sourceId,
|
|
597
1006
|
span: input.attribute.span,
|
|
598
1007
|
});
|
|
599
1008
|
return undefined;
|
|
600
1009
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
1010
|
+
|
|
1011
|
+
if (
|
|
1012
|
+
relationNameFromPositional &&
|
|
1013
|
+
namedRelationName &&
|
|
1014
|
+
relationNameFromPositional !== namedRelationName
|
|
1015
|
+
) {
|
|
604
1016
|
input.diagnostics.push({
|
|
605
1017
|
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
606
|
-
message: `Relation field "${input.modelName}.${input.fieldName}"
|
|
1018
|
+
message: `Relation field "${input.modelName}.${input.fieldName}" has conflicting positional and named relation names`,
|
|
607
1019
|
sourceId: input.sourceId,
|
|
608
1020
|
span: input.attribute.span,
|
|
609
1021
|
});
|
|
610
1022
|
return undefined;
|
|
611
1023
|
}
|
|
1024
|
+
const relationName = namedRelationName ?? relationNameFromPositional;
|
|
1025
|
+
|
|
1026
|
+
const fieldsRaw = getNamedArgument(input.attribute, 'fields');
|
|
1027
|
+
const referencesRaw = getNamedArgument(input.attribute, 'references');
|
|
1028
|
+
if ((fieldsRaw && !referencesRaw) || (!fieldsRaw && referencesRaw)) {
|
|
1029
|
+
input.diagnostics.push({
|
|
1030
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1031
|
+
message: `Relation field "${input.modelName}.${input.fieldName}" requires fields and references arguments`,
|
|
1032
|
+
sourceId: input.sourceId,
|
|
1033
|
+
span: input.attribute.span,
|
|
1034
|
+
});
|
|
1035
|
+
return undefined;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
let fields: readonly string[] | undefined;
|
|
1039
|
+
let references: readonly string[] | undefined;
|
|
1040
|
+
if (fieldsRaw && referencesRaw) {
|
|
1041
|
+
const parsedFields = parseFieldList(fieldsRaw);
|
|
1042
|
+
const parsedReferences = parseFieldList(referencesRaw);
|
|
1043
|
+
if (
|
|
1044
|
+
!parsedFields ||
|
|
1045
|
+
!parsedReferences ||
|
|
1046
|
+
parsedFields.length === 0 ||
|
|
1047
|
+
parsedReferences.length === 0
|
|
1048
|
+
) {
|
|
1049
|
+
input.diagnostics.push({
|
|
1050
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1051
|
+
message: `Relation field "${input.modelName}.${input.fieldName}" requires bracketed fields and references lists`,
|
|
1052
|
+
sourceId: input.sourceId,
|
|
1053
|
+
span: input.attribute.span,
|
|
1054
|
+
});
|
|
1055
|
+
return undefined;
|
|
1056
|
+
}
|
|
1057
|
+
fields = parsedFields;
|
|
1058
|
+
references = parsedReferences;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const onDeleteArgument = getNamedArgument(input.attribute, 'onDelete');
|
|
1062
|
+
const onUpdateArgument = getNamedArgument(input.attribute, 'onUpdate');
|
|
612
1063
|
|
|
613
1064
|
return {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
...(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
...(getNamedArgument(input.attribute, 'onUpdate')
|
|
620
|
-
? { onUpdate: unquoteStringLiteral(getNamedArgument(input.attribute, 'onUpdate') ?? '') }
|
|
621
|
-
: {}),
|
|
1065
|
+
...ifDefined('relationName', relationName),
|
|
1066
|
+
...ifDefined('fields', fields),
|
|
1067
|
+
...ifDefined('references', references),
|
|
1068
|
+
...ifDefined('onDelete', onDeleteArgument ? unquoteStringLiteral(onDeleteArgument) : undefined),
|
|
1069
|
+
...ifDefined('onUpdate', onUpdateArgument ? unquoteStringLiteral(onUpdateArgument) : undefined),
|
|
622
1070
|
};
|
|
623
1071
|
}
|
|
624
1072
|
|
|
@@ -629,6 +1077,8 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
629
1077
|
const modelNames = new Set(input.document.ast.models.map((model) => model.name));
|
|
630
1078
|
const sourceId = input.document.ast.sourceId;
|
|
631
1079
|
const composedExtensions = new Set(input.composedExtensionPacks ?? []);
|
|
1080
|
+
const defaultFunctionRegistry =
|
|
1081
|
+
input.defaultFunctionRegistry ?? createBuiltinDefaultFunctionRegistry();
|
|
632
1082
|
|
|
633
1083
|
let builder = defineContract().target(
|
|
634
1084
|
input.target ?? DEFAULT_POSTGRES_TARGET,
|
|
@@ -735,6 +1185,13 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
735
1185
|
}
|
|
736
1186
|
|
|
737
1187
|
const modelMappings = buildModelMappings(input.document.ast.models, diagnostics, sourceId);
|
|
1188
|
+
const resolvedModels: Array<{
|
|
1189
|
+
model: PslModel;
|
|
1190
|
+
mapping: ModelNameMapping;
|
|
1191
|
+
resolvedFields: ResolvedField[];
|
|
1192
|
+
}> = [];
|
|
1193
|
+
const fkRelationMetadata: FkRelationMetadata[] = [];
|
|
1194
|
+
const backrelationCandidates: ModelBackrelationCandidate[] = [];
|
|
738
1195
|
|
|
739
1196
|
for (const model of input.document.ast.models) {
|
|
740
1197
|
const mapping = modelMappings.get(model.name);
|
|
@@ -750,9 +1207,11 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
750
1207
|
namedTypeBaseTypes,
|
|
751
1208
|
modelNames,
|
|
752
1209
|
composedExtensions,
|
|
1210
|
+
defaultFunctionRegistry,
|
|
753
1211
|
diagnostics,
|
|
754
1212
|
sourceId,
|
|
755
1213
|
);
|
|
1214
|
+
resolvedModels.push({ model, mapping, resolvedFields });
|
|
756
1215
|
|
|
757
1216
|
const primaryKeyColumns = resolvedFields
|
|
758
1217
|
.filter((field) => field.isId)
|
|
@@ -766,6 +1225,63 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
766
1225
|
});
|
|
767
1226
|
}
|
|
768
1227
|
|
|
1228
|
+
for (const field of model.fields) {
|
|
1229
|
+
if (!field.list || !modelNames.has(field.typeName)) {
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
const attributesValid = validateNavigationListFieldAttributes({
|
|
1233
|
+
modelName: model.name,
|
|
1234
|
+
field,
|
|
1235
|
+
sourceId,
|
|
1236
|
+
composedExtensions,
|
|
1237
|
+
diagnostics,
|
|
1238
|
+
});
|
|
1239
|
+
const relationAttribute = getAttribute(field.attributes, 'relation');
|
|
1240
|
+
let relationName: string | undefined;
|
|
1241
|
+
if (relationAttribute) {
|
|
1242
|
+
const parsedRelation = parseRelationAttribute({
|
|
1243
|
+
attribute: relationAttribute,
|
|
1244
|
+
modelName: model.name,
|
|
1245
|
+
fieldName: field.name,
|
|
1246
|
+
sourceId,
|
|
1247
|
+
diagnostics,
|
|
1248
|
+
});
|
|
1249
|
+
if (!parsedRelation) {
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
if (parsedRelation.fields || parsedRelation.references) {
|
|
1253
|
+
diagnostics.push({
|
|
1254
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1255
|
+
message: `Backrelation list field "${model.name}.${field.name}" cannot declare fields/references; define them on the FK-side relation field`,
|
|
1256
|
+
sourceId,
|
|
1257
|
+
span: relationAttribute.span,
|
|
1258
|
+
});
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
if (parsedRelation.onDelete || parsedRelation.onUpdate) {
|
|
1262
|
+
diagnostics.push({
|
|
1263
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1264
|
+
message: `Backrelation list field "${model.name}.${field.name}" cannot declare onDelete/onUpdate; define referential actions on the FK-side relation field`,
|
|
1265
|
+
sourceId,
|
|
1266
|
+
span: relationAttribute.span,
|
|
1267
|
+
});
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
relationName = parsedRelation.relationName;
|
|
1271
|
+
}
|
|
1272
|
+
if (!attributesValid) {
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
backrelationCandidates.push({
|
|
1277
|
+
modelName: model.name,
|
|
1278
|
+
tableName,
|
|
1279
|
+
field,
|
|
1280
|
+
targetModelName: field.typeName,
|
|
1281
|
+
...ifDefined('relationName', relationName),
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
|
|
769
1285
|
const relationAttributes = model.fields
|
|
770
1286
|
.map((field) => ({
|
|
771
1287
|
field,
|
|
@@ -779,16 +1295,23 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
779
1295
|
let table = tableBuilder;
|
|
780
1296
|
|
|
781
1297
|
for (const resolvedField of resolvedFields) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1298
|
+
if (resolvedField.executionDefault) {
|
|
1299
|
+
table = table.generated(resolvedField.columnName, {
|
|
1300
|
+
type: resolvedField.descriptor,
|
|
1301
|
+
generated: resolvedField.executionDefault,
|
|
1302
|
+
});
|
|
1303
|
+
} else {
|
|
1304
|
+
const options: {
|
|
1305
|
+
type: ColumnDescriptor;
|
|
1306
|
+
nullable?: true;
|
|
1307
|
+
default?: ColumnDefault;
|
|
1308
|
+
} = {
|
|
1309
|
+
type: resolvedField.descriptor,
|
|
1310
|
+
...ifDefined('nullable', resolvedField.field.optional ? (true as const) : undefined),
|
|
1311
|
+
...ifDefined('default', resolvedField.defaultValue),
|
|
1312
|
+
};
|
|
1313
|
+
table = table.column(resolvedField.columnName, options);
|
|
1314
|
+
}
|
|
792
1315
|
|
|
793
1316
|
if (resolvedField.isUnique) {
|
|
794
1317
|
table = table.unique([resolvedField.columnName]);
|
|
@@ -851,6 +1374,10 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
851
1374
|
}
|
|
852
1375
|
|
|
853
1376
|
for (const relationAttribute of relationAttributes) {
|
|
1377
|
+
if (relationAttribute.field.list) {
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
854
1381
|
if (!modelNames.has(relationAttribute.field.typeName)) {
|
|
855
1382
|
diagnostics.push({
|
|
856
1383
|
code: 'PSL_INVALID_RELATION_TARGET',
|
|
@@ -871,6 +1398,15 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
871
1398
|
if (!parsedRelation) {
|
|
872
1399
|
continue;
|
|
873
1400
|
}
|
|
1401
|
+
if (!parsedRelation.fields || !parsedRelation.references) {
|
|
1402
|
+
diagnostics.push({
|
|
1403
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1404
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" requires fields and references arguments`,
|
|
1405
|
+
sourceId,
|
|
1406
|
+
span: relationAttribute.relation.span,
|
|
1407
|
+
});
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
874
1410
|
|
|
875
1411
|
const targetMapping = modelMappings.get(relationAttribute.field.typeName);
|
|
876
1412
|
if (!targetMapping) {
|
|
@@ -907,6 +1443,15 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
907
1443
|
if (!referencedColumns) {
|
|
908
1444
|
continue;
|
|
909
1445
|
}
|
|
1446
|
+
if (localColumns.length !== referencedColumns.length) {
|
|
1447
|
+
diagnostics.push({
|
|
1448
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1449
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" must provide the same number of fields and references`,
|
|
1450
|
+
sourceId,
|
|
1451
|
+
span: relationAttribute.relation.span,
|
|
1452
|
+
});
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
910
1455
|
|
|
911
1456
|
const onDelete = parsedRelation.onDelete
|
|
912
1457
|
? normalizeReferentialAction({
|
|
@@ -938,24 +1483,41 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
938
1483
|
columns: referencedColumns,
|
|
939
1484
|
},
|
|
940
1485
|
{
|
|
941
|
-
...(onDelete
|
|
942
|
-
...(onUpdate
|
|
1486
|
+
...ifDefined('onDelete', onDelete),
|
|
1487
|
+
...ifDefined('onUpdate', onUpdate),
|
|
943
1488
|
},
|
|
944
1489
|
);
|
|
1490
|
+
|
|
1491
|
+
fkRelationMetadata.push({
|
|
1492
|
+
declaringModelName: model.name,
|
|
1493
|
+
declaringFieldName: relationAttribute.field.name,
|
|
1494
|
+
declaringTableName: tableName,
|
|
1495
|
+
targetModelName: targetMapping.model.name,
|
|
1496
|
+
targetTableName: targetMapping.tableName,
|
|
1497
|
+
...ifDefined('relationName', parsedRelation.relationName),
|
|
1498
|
+
localColumns,
|
|
1499
|
+
referencedColumns,
|
|
1500
|
+
});
|
|
945
1501
|
}
|
|
946
1502
|
|
|
947
1503
|
return table;
|
|
948
1504
|
});
|
|
949
|
-
|
|
950
|
-
builder = builder.model(model.name, tableName, (modelBuilder: DynamicModelBuilder) => {
|
|
951
|
-
let next = modelBuilder;
|
|
952
|
-
for (const resolvedField of resolvedFields) {
|
|
953
|
-
next = next.field(resolvedField.field.name, resolvedField.columnName);
|
|
954
|
-
}
|
|
955
|
-
return next;
|
|
956
|
-
});
|
|
957
1505
|
}
|
|
958
1506
|
|
|
1507
|
+
const { modelRelations, fkRelationsByPair } = indexFkRelations({ fkRelationMetadata });
|
|
1508
|
+
applyBackrelationCandidates({
|
|
1509
|
+
backrelationCandidates,
|
|
1510
|
+
fkRelationsByPair,
|
|
1511
|
+
modelRelations,
|
|
1512
|
+
diagnostics,
|
|
1513
|
+
sourceId,
|
|
1514
|
+
});
|
|
1515
|
+
builder = emitModelsWithRelations({
|
|
1516
|
+
builder,
|
|
1517
|
+
resolvedModels,
|
|
1518
|
+
modelRelations,
|
|
1519
|
+
});
|
|
1520
|
+
|
|
959
1521
|
if (diagnostics.length > 0) {
|
|
960
1522
|
const dedupedDiagnostics = diagnostics.filter(
|
|
961
1523
|
(diagnostic, index, allDiagnostics) =>
|