@prisma-next/sql-contract-psl 0.0.1 → 0.3.0-dev.114
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 +34 -8
- package/dist/default-function-registry-DUMRIhJH.d.mts +71 -0
- package/dist/default-function-registry-DUMRIhJH.d.mts.map +1 -0
- package/dist/index.d.mts +11 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/interpreter-D7gLmaHz.mjs +1412 -0
- package/dist/interpreter-D7gLmaHz.mjs.map +1 -0
- package/dist/provider.d.mts +10 -11
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +18 -12
- package/dist/provider.mjs.map +1 -1
- package/package.json +23 -23
- package/src/default-function-registry.ts +262 -0
- package/src/exports/index.ts +8 -0
- package/src/interpreter.ts +1055 -102
- package/src/provider.ts +34 -21
- package/dist/interpreter-_6-Xk1_m.mjs +0 -661
- package/dist/interpreter-_6-Xk1_m.mjs.map +0 -1
package/src/interpreter.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
} from '@prisma-next/config/config-types';
|
|
6
6
|
import type { TargetPackRef } from '@prisma-next/contract/framework-components';
|
|
7
7
|
import type { ContractIR } from '@prisma-next/contract/ir';
|
|
8
|
-
import type { ColumnDefault } from '@prisma-next/contract/types';
|
|
8
|
+
import type { ColumnDefault, ExecutionMutationDefaultValue } from '@prisma-next/contract/types';
|
|
9
9
|
import type {
|
|
10
10
|
ParsePslDocumentResult,
|
|
11
11
|
PslAttribute,
|
|
@@ -14,7 +14,16 @@ 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
|
+
type ControlMutationDefaultRegistry,
|
|
22
|
+
type ControlMutationDefaults,
|
|
23
|
+
lowerDefaultFunctionWithRegistry,
|
|
24
|
+
type MutationDefaultGeneratorDescriptor,
|
|
25
|
+
parseDefaultFunctionCall,
|
|
26
|
+
} from './default-function-registry';
|
|
18
27
|
|
|
19
28
|
type ColumnDescriptor = {
|
|
20
29
|
readonly codecId: string;
|
|
@@ -25,31 +34,12 @@ type ColumnDescriptor = {
|
|
|
25
34
|
|
|
26
35
|
export interface InterpretPslDocumentToSqlContractIRInput {
|
|
27
36
|
readonly document: ParsePslDocumentResult;
|
|
28
|
-
readonly target
|
|
37
|
+
readonly target: TargetPackRef<'sql', 'postgres'>;
|
|
38
|
+
readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
|
|
29
39
|
readonly composedExtensionPacks?: readonly string[];
|
|
40
|
+
readonly controlMutationDefaults?: ControlMutationDefaults;
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
const DEFAULT_POSTGRES_TARGET: TargetPackRef<'sql', 'postgres'> = {
|
|
33
|
-
kind: 'target',
|
|
34
|
-
familyId: 'sql',
|
|
35
|
-
targetId: 'postgres',
|
|
36
|
-
id: 'postgres',
|
|
37
|
-
version: '0.0.1',
|
|
38
|
-
capabilities: {},
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const SCALAR_COLUMN_MAP: Record<string, ColumnDescriptor> = {
|
|
42
|
-
String: { codecId: 'pg/text@1', nativeType: 'text' },
|
|
43
|
-
Boolean: { codecId: 'pg/bool@1', nativeType: 'bool' },
|
|
44
|
-
Int: { codecId: 'pg/int4@1', nativeType: 'int4' },
|
|
45
|
-
BigInt: { codecId: 'pg/int8@1', nativeType: 'int8' },
|
|
46
|
-
Float: { codecId: 'pg/float8@1', nativeType: 'float8' },
|
|
47
|
-
Decimal: { codecId: 'pg/numeric@1', nativeType: 'numeric' },
|
|
48
|
-
DateTime: { codecId: 'pg/timestamptz@1', nativeType: 'timestamptz' },
|
|
49
|
-
Json: { codecId: 'pg/jsonb@1', nativeType: 'jsonb' },
|
|
50
|
-
Bytes: { codecId: 'pg/bytea@1', nativeType: 'bytea' },
|
|
51
|
-
};
|
|
52
|
-
|
|
53
43
|
const REFERENTIAL_ACTION_MAP = {
|
|
54
44
|
NoAction: 'noAction',
|
|
55
45
|
Restrict: 'restrict',
|
|
@@ -68,10 +58,61 @@ type ResolvedField = {
|
|
|
68
58
|
readonly columnName: string;
|
|
69
59
|
readonly descriptor: ColumnDescriptor;
|
|
70
60
|
readonly defaultValue?: ColumnDefault;
|
|
61
|
+
readonly executionDefault?: ExecutionMutationDefaultValue;
|
|
71
62
|
readonly isId: boolean;
|
|
72
63
|
readonly isUnique: boolean;
|
|
73
64
|
};
|
|
74
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
|
+
|
|
75
116
|
type ModelNameMapping = {
|
|
76
117
|
readonly model: PslModel;
|
|
77
118
|
readonly tableName: string;
|
|
@@ -83,18 +124,36 @@ type DynamicTableBuilder = {
|
|
|
83
124
|
name: string,
|
|
84
125
|
options: { type: ColumnDescriptor; nullable?: true; default?: ColumnDefault },
|
|
85
126
|
): DynamicTableBuilder;
|
|
127
|
+
generated(
|
|
128
|
+
name: string,
|
|
129
|
+
options: { type: ColumnDescriptor; generated: ExecutionMutationDefaultValue },
|
|
130
|
+
): DynamicTableBuilder;
|
|
86
131
|
unique(columns: readonly string[]): DynamicTableBuilder;
|
|
87
132
|
primaryKey(columns: readonly string[]): DynamicTableBuilder;
|
|
88
133
|
index(columns: readonly string[]): DynamicTableBuilder;
|
|
89
134
|
foreignKey(
|
|
90
135
|
columns: readonly string[],
|
|
91
136
|
references: { table: string; columns: readonly string[] },
|
|
92
|
-
options?: { onDelete?: string; onUpdate?: string },
|
|
137
|
+
options?: { name?: string; onDelete?: string; onUpdate?: string },
|
|
93
138
|
): DynamicTableBuilder;
|
|
94
139
|
};
|
|
95
140
|
|
|
96
141
|
type DynamicModelBuilder = {
|
|
97
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;
|
|
98
157
|
};
|
|
99
158
|
|
|
100
159
|
type DynamicContractBuilder = {
|
|
@@ -124,8 +183,11 @@ function lowerFirst(value: string): string {
|
|
|
124
183
|
return value[0]?.toLowerCase() + value.slice(1);
|
|
125
184
|
}
|
|
126
185
|
|
|
127
|
-
function getAttribute(
|
|
128
|
-
|
|
186
|
+
function getAttribute(
|
|
187
|
+
attributes: readonly PslAttribute[] | undefined,
|
|
188
|
+
name: string,
|
|
189
|
+
): PslAttribute | undefined {
|
|
190
|
+
return attributes?.find((attribute) => attribute.name === name);
|
|
129
191
|
}
|
|
130
192
|
|
|
131
193
|
function getNamedArgument(attribute: PslAttribute, name: string): string | undefined {
|
|
@@ -145,6 +207,21 @@ function getPositionalArgument(attribute: PslAttribute, index = 0): string | und
|
|
|
145
207
|
return entry.value;
|
|
146
208
|
}
|
|
147
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
|
+
|
|
148
225
|
function unquoteStringLiteral(value: string): string {
|
|
149
226
|
const trimmed = value.trim();
|
|
150
227
|
const match = trimmed.match(/^(['"])(.*)\1$/);
|
|
@@ -156,6 +233,8 @@ function unquoteStringLiteral(value: string): string {
|
|
|
156
233
|
|
|
157
234
|
function parseQuotedStringLiteral(value: string): string | undefined {
|
|
158
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.
|
|
159
238
|
const match = trimmed.match(/^(['"])(.*)\1$/);
|
|
160
239
|
if (!match) {
|
|
161
240
|
return undefined;
|
|
@@ -176,14 +255,8 @@ function parseFieldList(value: string): readonly string[] | undefined {
|
|
|
176
255
|
return parts;
|
|
177
256
|
}
|
|
178
257
|
|
|
179
|
-
function
|
|
258
|
+
function parseDefaultLiteralValue(expression: string): ColumnDefault | undefined {
|
|
180
259
|
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
260
|
if (trimmed === 'true' || trimmed === 'false') {
|
|
188
261
|
return { kind: 'literal', value: trimmed === 'true' };
|
|
189
262
|
}
|
|
@@ -197,6 +270,103 @@ function parseDefaultValueExpression(expression: string): ColumnDefault | undefi
|
|
|
197
270
|
return undefined;
|
|
198
271
|
}
|
|
199
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
|
+
|
|
200
370
|
function parseMapName(input: {
|
|
201
371
|
readonly attribute: PslAttribute | undefined;
|
|
202
372
|
readonly defaultValue: string;
|
|
@@ -263,10 +433,313 @@ function parsePgvectorLength(input: {
|
|
|
263
433
|
return parsed;
|
|
264
434
|
}
|
|
265
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
|
+
|
|
266
738
|
function resolveColumnDescriptor(
|
|
267
739
|
field: PslField,
|
|
268
740
|
enumTypeDescriptors: Map<string, ColumnDescriptor>,
|
|
269
741
|
namedTypeDescriptors: Map<string, ColumnDescriptor>,
|
|
742
|
+
scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
|
|
270
743
|
): ColumnDescriptor | undefined {
|
|
271
744
|
if (field.typeRef && namedTypeDescriptors.has(field.typeRef)) {
|
|
272
745
|
return namedTypeDescriptors.get(field.typeRef);
|
|
@@ -277,7 +750,7 @@ function resolveColumnDescriptor(
|
|
|
277
750
|
if (enumTypeDescriptors.has(field.typeName)) {
|
|
278
751
|
return enumTypeDescriptors.get(field.typeName);
|
|
279
752
|
}
|
|
280
|
-
return
|
|
753
|
+
return scalarTypeDescriptors.get(field.typeName);
|
|
281
754
|
}
|
|
282
755
|
|
|
283
756
|
function collectResolvedFields(
|
|
@@ -288,16 +761,22 @@ function collectResolvedFields(
|
|
|
288
761
|
namedTypeBaseTypes: Map<string, string>,
|
|
289
762
|
modelNames: Set<string>,
|
|
290
763
|
composedExtensions: Set<string>,
|
|
764
|
+
defaultFunctionRegistry: ControlMutationDefaultRegistry,
|
|
765
|
+
generatorDescriptorById: ReadonlyMap<string, MutationDefaultGeneratorDescriptor>,
|
|
291
766
|
diagnostics: ContractSourceDiagnostic[],
|
|
292
767
|
sourceId: string,
|
|
768
|
+
scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
|
|
293
769
|
): ResolvedField[] {
|
|
294
770
|
const resolvedFields: ResolvedField[] = [];
|
|
295
771
|
|
|
296
772
|
for (const field of model.fields) {
|
|
297
773
|
if (field.list) {
|
|
774
|
+
if (modelNames.has(field.typeName)) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
298
777
|
diagnostics.push({
|
|
299
778
|
code: 'PSL_UNSUPPORTED_FIELD_LIST',
|
|
300
|
-
message: `Field "${model.name}.${field.name}" uses 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.`,
|
|
301
780
|
sourceId,
|
|
302
781
|
span: field.span,
|
|
303
782
|
});
|
|
@@ -337,7 +816,12 @@ function collectResolvedFields(
|
|
|
337
816
|
continue;
|
|
338
817
|
}
|
|
339
818
|
|
|
340
|
-
let descriptor = resolveColumnDescriptor(
|
|
819
|
+
let descriptor = resolveColumnDescriptor(
|
|
820
|
+
field,
|
|
821
|
+
enumTypeDescriptors,
|
|
822
|
+
namedTypeDescriptors,
|
|
823
|
+
scalarTypeDescriptors,
|
|
824
|
+
);
|
|
341
825
|
const pgvectorColumnAttribute = getAttribute(field.attributes, 'pgvector.column');
|
|
342
826
|
if (pgvectorColumnAttribute) {
|
|
343
827
|
if (!composedExtensions.has('pgvector')) {
|
|
@@ -368,7 +852,7 @@ function collectResolvedFields(
|
|
|
368
852
|
if (length !== undefined) {
|
|
369
853
|
descriptor = {
|
|
370
854
|
codecId: 'pg/vector@1',
|
|
371
|
-
nativeType:
|
|
855
|
+
nativeType: 'vector',
|
|
372
856
|
typeParams: { length },
|
|
373
857
|
};
|
|
374
858
|
}
|
|
@@ -387,22 +871,47 @@ function collectResolvedFields(
|
|
|
387
871
|
}
|
|
388
872
|
|
|
389
873
|
const defaultAttribute = getAttribute(field.attributes, 'default');
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
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';
|
|
393
891
|
diagnostics.push({
|
|
394
|
-
code: '
|
|
395
|
-
message: `
|
|
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.`,
|
|
396
894
|
sourceId,
|
|
397
|
-
span: defaultAttribute.span,
|
|
895
|
+
span: defaultAttribute?.span ?? field.span,
|
|
398
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
|
+
}
|
|
399
907
|
}
|
|
400
908
|
const mappedColumnName = mapping.fieldColumns.get(field.name) ?? field.name;
|
|
401
909
|
resolvedFields.push({
|
|
402
910
|
field,
|
|
403
911
|
columnName: mappedColumnName,
|
|
404
912
|
descriptor,
|
|
405
|
-
...(defaultValue
|
|
913
|
+
...ifDefined('defaultValue', loweredDefault.defaultValue),
|
|
914
|
+
...ifDefined('executionDefault', loweredDefault.executionDefault),
|
|
406
915
|
isId: Boolean(getAttribute(field.attributes, 'id')),
|
|
407
916
|
isUnique: Boolean(getAttribute(field.attributes, 'unique')),
|
|
408
917
|
});
|
|
@@ -420,6 +929,155 @@ function hasSameSpan(a: PslSpan, b: ContractSourceDiagnosticSpan): boolean {
|
|
|
420
929
|
);
|
|
421
930
|
}
|
|
422
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
|
+
|
|
423
1081
|
function mapParserDiagnostics(document: ParsePslDocumentResult): ContractSourceDiagnostic[] {
|
|
424
1082
|
return document.diagnostics.map((diagnostic) => ({
|
|
425
1083
|
code: diagnostic.code,
|
|
@@ -547,33 +1205,82 @@ function buildModelMappings(
|
|
|
547
1205
|
return result;
|
|
548
1206
|
}
|
|
549
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
|
+
|
|
550
1241
|
function parseRelationAttribute(input: {
|
|
551
1242
|
readonly attribute: PslAttribute;
|
|
552
1243
|
readonly modelName: string;
|
|
553
1244
|
readonly fieldName: string;
|
|
554
1245
|
readonly sourceId: string;
|
|
555
1246
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
556
|
-
}):
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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) {
|
|
566
1264
|
input.diagnostics.push({
|
|
567
1265
|
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
568
|
-
message: `Relation field "${input.modelName}.${input.fieldName}" must
|
|
1266
|
+
message: `Relation field "${input.modelName}.${input.fieldName}" positional relation name must be a quoted string literal`,
|
|
569
1267
|
sourceId: input.sourceId,
|
|
570
|
-
span:
|
|
1268
|
+
span: positionalNameEntry.span,
|
|
571
1269
|
});
|
|
572
1270
|
return undefined;
|
|
573
1271
|
}
|
|
1272
|
+
relationNameFromPositional = parsedName;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
for (const arg of input.attribute.args) {
|
|
1276
|
+
if (arg.kind === 'positional') {
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
574
1279
|
if (
|
|
1280
|
+
arg.name !== 'name' &&
|
|
575
1281
|
arg.name !== 'fields' &&
|
|
576
1282
|
arg.name !== 'references' &&
|
|
1283
|
+
arg.name !== 'map' &&
|
|
577
1284
|
arg.name !== 'onDelete' &&
|
|
578
1285
|
arg.name !== 'onUpdate'
|
|
579
1286
|
) {
|
|
@@ -587,58 +1294,151 @@ function parseRelationAttribute(input: {
|
|
|
587
1294
|
}
|
|
588
1295
|
}
|
|
589
1296
|
|
|
590
|
-
const
|
|
591
|
-
const
|
|
592
|
-
|
|
1297
|
+
const namedRelationNameRaw = getNamedArgument(input.attribute, 'name');
|
|
1298
|
+
const namedRelationName = namedRelationNameRaw
|
|
1299
|
+
? parseQuotedStringLiteral(namedRelationNameRaw)
|
|
1300
|
+
: undefined;
|
|
1301
|
+
if (namedRelationNameRaw && !namedRelationName) {
|
|
593
1302
|
input.diagnostics.push({
|
|
594
1303
|
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
595
|
-
message: `Relation field "${input.modelName}.${input.fieldName}"
|
|
1304
|
+
message: `Relation field "${input.modelName}.${input.fieldName}" named relation name must be a quoted string literal`,
|
|
596
1305
|
sourceId: input.sourceId,
|
|
597
1306
|
span: input.attribute.span,
|
|
598
1307
|
});
|
|
599
1308
|
return undefined;
|
|
600
1309
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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)) {
|
|
604
1343
|
input.diagnostics.push({
|
|
605
1344
|
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
606
|
-
message: `Relation field "${input.modelName}.${input.fieldName}" requires
|
|
1345
|
+
message: `Relation field "${input.modelName}.${input.fieldName}" requires fields and references arguments`,
|
|
607
1346
|
sourceId: input.sourceId,
|
|
608
1347
|
span: input.attribute.span,
|
|
609
1348
|
});
|
|
610
1349
|
return undefined;
|
|
611
1350
|
}
|
|
612
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
|
+
|
|
613
1378
|
return {
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
...(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
...(
|
|
620
|
-
? { onUpdate: unquoteStringLiteral(getNamedArgument(input.attribute, 'onUpdate') ?? '') }
|
|
621
|
-
: {}),
|
|
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),
|
|
622
1385
|
};
|
|
623
1386
|
}
|
|
624
1387
|
|
|
625
1388
|
export function interpretPslDocumentToSqlContractIR(
|
|
626
1389
|
input: InterpretPslDocumentToSqlContractIRInput,
|
|
627
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
|
+
});
|
|
1415
|
+
}
|
|
1416
|
+
|
|
628
1417
|
const diagnostics: ContractSourceDiagnostic[] = mapParserDiagnostics(input.document);
|
|
629
1418
|
const modelNames = new Set(input.document.ast.models.map((model) => model.name));
|
|
630
|
-
const sourceId = input.document.ast.sourceId;
|
|
631
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);
|
|
1426
|
+
}
|
|
632
1427
|
|
|
633
|
-
let builder = defineContract().target(
|
|
634
|
-
input.target ?? DEFAULT_POSTGRES_TARGET,
|
|
635
|
-
) as unknown as DynamicContractBuilder;
|
|
1428
|
+
let builder = defineContract().target(input.target) as DynamicContractBuilder;
|
|
636
1429
|
const enumTypeDescriptors = new Map<string, ColumnDescriptor>();
|
|
637
1430
|
const namedTypeDescriptors = new Map<string, ColumnDescriptor>();
|
|
638
1431
|
const namedTypeBaseTypes = new Map<string, string>();
|
|
639
1432
|
|
|
640
1433
|
for (const enumDeclaration of input.document.ast.enums) {
|
|
641
|
-
const nativeType =
|
|
1434
|
+
const nativeType = parseMapName({
|
|
1435
|
+
attribute: getAttribute(enumDeclaration.attributes, 'map'),
|
|
1436
|
+
defaultValue: enumDeclaration.name,
|
|
1437
|
+
sourceId,
|
|
1438
|
+
diagnostics,
|
|
1439
|
+
entityLabel: `Enum "${enumDeclaration.name}"`,
|
|
1440
|
+
span: enumDeclaration.span,
|
|
1441
|
+
});
|
|
642
1442
|
const descriptor: ColumnDescriptor = {
|
|
643
1443
|
codecId: 'pg/enum@1',
|
|
644
1444
|
nativeType,
|
|
@@ -654,7 +1454,8 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
654
1454
|
|
|
655
1455
|
for (const declaration of input.document.ast.types?.declarations ?? []) {
|
|
656
1456
|
const baseDescriptor =
|
|
657
|
-
enumTypeDescriptors.get(declaration.baseType) ??
|
|
1457
|
+
enumTypeDescriptors.get(declaration.baseType) ??
|
|
1458
|
+
input.scalarTypeDescriptors.get(declaration.baseType);
|
|
658
1459
|
if (!baseDescriptor) {
|
|
659
1460
|
diagnostics.push({
|
|
660
1461
|
code: 'PSL_UNSUPPORTED_NAMED_TYPE_BASE',
|
|
@@ -667,8 +1468,11 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
667
1468
|
namedTypeBaseTypes.set(declaration.name, declaration.baseType);
|
|
668
1469
|
|
|
669
1470
|
const pgvectorAttribute = getAttribute(declaration.attributes, 'pgvector.column');
|
|
1471
|
+
const dbNativeTypeAttribute = declaration.attributes.find((attribute) =>
|
|
1472
|
+
attribute.name.startsWith('db.'),
|
|
1473
|
+
);
|
|
670
1474
|
const unsupportedNamedTypeAttribute = declaration.attributes.find(
|
|
671
|
-
(attribute) => attribute.name !== 'pgvector.column',
|
|
1475
|
+
(attribute) => attribute.name !== 'pgvector.column' && !attribute.name.startsWith('db.'),
|
|
672
1476
|
);
|
|
673
1477
|
if (unsupportedNamedTypeAttribute) {
|
|
674
1478
|
diagnostics.push({
|
|
@@ -680,6 +1484,16 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
680
1484
|
continue;
|
|
681
1485
|
}
|
|
682
1486
|
|
|
1487
|
+
if (pgvectorAttribute && dbNativeTypeAttribute) {
|
|
1488
|
+
diagnostics.push({
|
|
1489
|
+
code: 'PSL_UNSUPPORTED_NAMED_TYPE_ATTRIBUTE',
|
|
1490
|
+
message: `Named type "${declaration.name}" cannot combine @pgvector.column with @${dbNativeTypeAttribute.name}.`,
|
|
1491
|
+
sourceId,
|
|
1492
|
+
span: dbNativeTypeAttribute.span,
|
|
1493
|
+
});
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
683
1497
|
if (pgvectorAttribute) {
|
|
684
1498
|
if (!composedExtensions.has('pgvector')) {
|
|
685
1499
|
diagnostics.push({
|
|
@@ -710,17 +1524,41 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
710
1524
|
}
|
|
711
1525
|
namedTypeDescriptors.set(declaration.name, {
|
|
712
1526
|
codecId: 'pg/vector@1',
|
|
713
|
-
nativeType:
|
|
714
|
-
|
|
1527
|
+
nativeType: 'vector',
|
|
1528
|
+
typeParams: { length },
|
|
715
1529
|
});
|
|
716
1530
|
builder = builder.storageType(declaration.name, {
|
|
717
1531
|
codecId: 'pg/vector@1',
|
|
718
|
-
nativeType:
|
|
1532
|
+
nativeType: 'vector',
|
|
719
1533
|
typeParams: { length },
|
|
720
1534
|
});
|
|
721
1535
|
continue;
|
|
722
1536
|
}
|
|
723
1537
|
|
|
1538
|
+
if (dbNativeTypeAttribute) {
|
|
1539
|
+
const descriptor = resolveDbNativeTypeAttribute({
|
|
1540
|
+
attribute: dbNativeTypeAttribute,
|
|
1541
|
+
baseType: declaration.baseType,
|
|
1542
|
+
baseDescriptor,
|
|
1543
|
+
diagnostics,
|
|
1544
|
+
sourceId,
|
|
1545
|
+
entityLabel: `Named type "${declaration.name}"`,
|
|
1546
|
+
});
|
|
1547
|
+
if (!descriptor) {
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
namedTypeDescriptors.set(declaration.name, {
|
|
1551
|
+
...descriptor,
|
|
1552
|
+
typeRef: declaration.name,
|
|
1553
|
+
});
|
|
1554
|
+
builder = builder.storageType(declaration.name, {
|
|
1555
|
+
codecId: descriptor.codecId,
|
|
1556
|
+
nativeType: descriptor.nativeType,
|
|
1557
|
+
typeParams: descriptor.typeParams ?? {},
|
|
1558
|
+
});
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
724
1562
|
const descriptor: ColumnDescriptor = {
|
|
725
1563
|
codecId: baseDescriptor.codecId,
|
|
726
1564
|
nativeType: baseDescriptor.nativeType,
|
|
@@ -735,6 +1573,13 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
735
1573
|
}
|
|
736
1574
|
|
|
737
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[] = [];
|
|
738
1583
|
|
|
739
1584
|
for (const model of input.document.ast.models) {
|
|
740
1585
|
const mapping = modelMappings.get(model.name);
|
|
@@ -750,9 +1595,13 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
750
1595
|
namedTypeBaseTypes,
|
|
751
1596
|
modelNames,
|
|
752
1597
|
composedExtensions,
|
|
1598
|
+
defaultFunctionRegistry,
|
|
1599
|
+
generatorDescriptorById,
|
|
753
1600
|
diagnostics,
|
|
754
1601
|
sourceId,
|
|
1602
|
+
input.scalarTypeDescriptors,
|
|
755
1603
|
);
|
|
1604
|
+
resolvedModels.push({ model, mapping, resolvedFields });
|
|
756
1605
|
|
|
757
1606
|
const primaryKeyColumns = resolvedFields
|
|
758
1607
|
.filter((field) => field.isId)
|
|
@@ -766,6 +1615,63 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
766
1615
|
});
|
|
767
1616
|
}
|
|
768
1617
|
|
|
1618
|
+
for (const field of model.fields) {
|
|
1619
|
+
if (!field.list || !modelNames.has(field.typeName)) {
|
|
1620
|
+
continue;
|
|
1621
|
+
}
|
|
1622
|
+
const attributesValid = validateNavigationListFieldAttributes({
|
|
1623
|
+
modelName: model.name,
|
|
1624
|
+
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,
|
|
1636
|
+
sourceId,
|
|
1637
|
+
diagnostics,
|
|
1638
|
+
});
|
|
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
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
backrelationCandidates.push({
|
|
1667
|
+
modelName: model.name,
|
|
1668
|
+
tableName,
|
|
1669
|
+
field,
|
|
1670
|
+
targetModelName: field.typeName,
|
|
1671
|
+
...ifDefined('relationName', relationName),
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
|
|
769
1675
|
const relationAttributes = model.fields
|
|
770
1676
|
.map((field) => ({
|
|
771
1677
|
field,
|
|
@@ -779,16 +1685,23 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
779
1685
|
let table = tableBuilder;
|
|
780
1686
|
|
|
781
1687
|
for (const resolvedField of resolvedFields) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
+
}
|
|
792
1705
|
|
|
793
1706
|
if (resolvedField.isUnique) {
|
|
794
1707
|
table = table.unique([resolvedField.columnName]);
|
|
@@ -851,6 +1764,10 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
851
1764
|
}
|
|
852
1765
|
|
|
853
1766
|
for (const relationAttribute of relationAttributes) {
|
|
1767
|
+
if (relationAttribute.field.list) {
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
854
1771
|
if (!modelNames.has(relationAttribute.field.typeName)) {
|
|
855
1772
|
diagnostics.push({
|
|
856
1773
|
code: 'PSL_INVALID_RELATION_TARGET',
|
|
@@ -871,6 +1788,15 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
871
1788
|
if (!parsedRelation) {
|
|
872
1789
|
continue;
|
|
873
1790
|
}
|
|
1791
|
+
if (!parsedRelation.fields || !parsedRelation.references) {
|
|
1792
|
+
diagnostics.push({
|
|
1793
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1794
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" requires fields and references arguments`,
|
|
1795
|
+
sourceId,
|
|
1796
|
+
span: relationAttribute.relation.span,
|
|
1797
|
+
});
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
874
1800
|
|
|
875
1801
|
const targetMapping = modelMappings.get(relationAttribute.field.typeName);
|
|
876
1802
|
if (!targetMapping) {
|
|
@@ -907,6 +1833,15 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
907
1833
|
if (!referencedColumns) {
|
|
908
1834
|
continue;
|
|
909
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
|
+
}
|
|
910
1845
|
|
|
911
1846
|
const onDelete = parsedRelation.onDelete
|
|
912
1847
|
? normalizeReferentialAction({
|
|
@@ -938,24 +1873,42 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
938
1873
|
columns: referencedColumns,
|
|
939
1874
|
},
|
|
940
1875
|
{
|
|
941
|
-
...(
|
|
942
|
-
...(
|
|
1876
|
+
...ifDefined('name', parsedRelation.constraintName),
|
|
1877
|
+
...ifDefined('onDelete', onDelete),
|
|
1878
|
+
...ifDefined('onUpdate', onUpdate),
|
|
943
1879
|
},
|
|
944
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
|
+
});
|
|
945
1892
|
}
|
|
946
1893
|
|
|
947
1894
|
return table;
|
|
948
1895
|
});
|
|
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
1896
|
}
|
|
958
1897
|
|
|
1898
|
+
const { modelRelations, fkRelationsByPair } = indexFkRelations({ fkRelationMetadata });
|
|
1899
|
+
applyBackrelationCandidates({
|
|
1900
|
+
backrelationCandidates,
|
|
1901
|
+
fkRelationsByPair,
|
|
1902
|
+
modelRelations,
|
|
1903
|
+
diagnostics,
|
|
1904
|
+
sourceId,
|
|
1905
|
+
});
|
|
1906
|
+
builder = emitModelsWithRelations({
|
|
1907
|
+
builder,
|
|
1908
|
+
resolvedModels,
|
|
1909
|
+
modelRelations,
|
|
1910
|
+
});
|
|
1911
|
+
|
|
959
1912
|
if (diagnostics.length > 0) {
|
|
960
1913
|
const dedupedDiagnostics = diagnostics.filter(
|
|
961
1914
|
(diagnostic, index, allDiagnostics) =>
|