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