@prisma-next/sql-contract-psl 0.3.0-dev.70 → 0.3.0
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 +18 -17
- package/dist/default-function-registry-DUMRIhJH.d.mts +71 -0
- package/dist/default-function-registry-DUMRIhJH.d.mts.map +1 -0
- package/dist/index.d.mts +18 -42
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/interpreter-iFCRN9nb.mjs +2343 -0
- package/dist/interpreter-iFCRN9nb.mjs.map +1 -0
- package/dist/provider.d.mts +14 -12
- package/dist/provider.d.mts.map +1 -1
- package/dist/provider.mjs +20 -13
- package/dist/provider.mjs.map +1 -1
- package/package.json +11 -8
- package/src/default-function-registry.ts +52 -300
- package/src/exports/index.ts +10 -2
- package/src/interpreter.ts +1074 -1326
- package/src/provider.ts +44 -25
- package/src/psl-attribute-parsing.ts +303 -0
- package/src/psl-authoring-arguments.ts +454 -0
- package/src/psl-column-resolution.ts +600 -0
- package/src/psl-field-resolution.ts +335 -0
- package/src/psl-relation-resolution.ts +371 -0
- package/dist/interpreter-IXr5c7s7.mjs +0 -1376
- package/dist/interpreter-IXr5c7s7.mjs.map +0 -1
package/src/interpreter.ts
CHANGED
|
@@ -3,1505 +3,1194 @@ 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,
|
|
14
|
-
|
|
25
|
+
PslNamedTypeDeclaration,
|
|
15
26
|
} from '@prisma-next/psl-parser';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
27
|
+
import type { StorageTypeInstance } from '@prisma-next/sql-contract/types';
|
|
28
|
+
import {
|
|
29
|
+
buildSqlContractFromDefinition,
|
|
30
|
+
type ForeignKeyNode,
|
|
31
|
+
type IndexNode,
|
|
32
|
+
type ModelNode,
|
|
33
|
+
type UniqueConstraintNode,
|
|
34
|
+
} from '@prisma-next/sql-contract-ts/contract-builder';
|
|
18
35
|
import { ifDefined } from '@prisma-next/utils/defined';
|
|
19
36
|
import { notOk, ok, type Result } from '@prisma-next/utils/result';
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
parseDefaultFunctionCall,
|
|
37
|
+
import type {
|
|
38
|
+
ControlMutationDefaultRegistry,
|
|
39
|
+
ControlMutationDefaults,
|
|
40
|
+
MutationDefaultGeneratorDescriptor,
|
|
25
41
|
} from './default-function-registry';
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
import {
|
|
43
|
+
getAttribute,
|
|
44
|
+
getPositionalArgument,
|
|
45
|
+
mapFieldNamesToColumns,
|
|
46
|
+
parseAttributeFieldList,
|
|
47
|
+
parseConstraintMapArgument,
|
|
48
|
+
parseMapName,
|
|
49
|
+
parseQuotedStringLiteral,
|
|
50
|
+
} from './psl-attribute-parsing';
|
|
51
|
+
import type { ColumnDescriptor } from './psl-column-resolution';
|
|
52
|
+
import {
|
|
53
|
+
checkUncomposedNamespace,
|
|
54
|
+
getAuthoringTypeConstructor,
|
|
55
|
+
instantiatePslTypeConstructor,
|
|
56
|
+
reportUncomposedNamespace,
|
|
57
|
+
resolveDbNativeTypeAttribute,
|
|
58
|
+
resolveFieldTypeDescriptor,
|
|
59
|
+
resolvePslTypeConstructorDescriptor,
|
|
60
|
+
toNamedTypeFieldDescriptor,
|
|
61
|
+
} from './psl-column-resolution';
|
|
62
|
+
import {
|
|
63
|
+
buildModelMappings,
|
|
64
|
+
collectResolvedFields,
|
|
65
|
+
type ModelNameMapping,
|
|
66
|
+
type ResolvedField,
|
|
67
|
+
} from './psl-field-resolution';
|
|
68
|
+
import {
|
|
69
|
+
applyBackrelationCandidates,
|
|
70
|
+
type FkRelationMetadata,
|
|
71
|
+
indexFkRelations,
|
|
72
|
+
type ModelBackrelationCandidate,
|
|
73
|
+
normalizeReferentialAction,
|
|
74
|
+
parseRelationAttribute,
|
|
75
|
+
validateNavigationListFieldAttributes,
|
|
76
|
+
} from './psl-relation-resolution';
|
|
77
|
+
|
|
78
|
+
export interface InterpretPslDocumentToSqlContractInput {
|
|
35
79
|
readonly document: ParsePslDocumentResult;
|
|
36
|
-
readonly target
|
|
80
|
+
readonly target: TargetPackRef<'sql', 'postgres'>;
|
|
81
|
+
readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
|
|
37
82
|
readonly composedExtensionPacks?: readonly string[];
|
|
38
|
-
readonly
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const DEFAULT_POSTGRES_TARGET: TargetPackRef<'sql', 'postgres'> = {
|
|
42
|
-
kind: 'target',
|
|
43
|
-
familyId: 'sql',
|
|
44
|
-
targetId: 'postgres',
|
|
45
|
-
id: 'postgres',
|
|
46
|
-
version: '0.0.1',
|
|
47
|
-
capabilities: {},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const SCALAR_COLUMN_MAP: Record<string, ColumnDescriptor> = {
|
|
51
|
-
String: { codecId: 'pg/text@1', nativeType: 'text' },
|
|
52
|
-
Boolean: { codecId: 'pg/bool@1', nativeType: 'bool' },
|
|
53
|
-
Int: { codecId: 'pg/int4@1', nativeType: 'int4' },
|
|
54
|
-
BigInt: { codecId: 'pg/int8@1', nativeType: 'int8' },
|
|
55
|
-
Float: { codecId: 'pg/float8@1', nativeType: 'float8' },
|
|
56
|
-
Decimal: { codecId: 'pg/numeric@1', nativeType: 'numeric' },
|
|
57
|
-
DateTime: { codecId: 'pg/timestamptz@1', nativeType: 'timestamptz' },
|
|
58
|
-
Json: { codecId: 'pg/jsonb@1', nativeType: 'jsonb' },
|
|
59
|
-
Bytes: { codecId: 'pg/bytea@1', nativeType: 'bytea' },
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const GENERATED_ID_COLUMN_MAP: Partial<Record<string, ColumnDescriptor>> = {
|
|
63
|
-
ulid: { codecId: 'sql/char@1', nativeType: 'character', typeParams: { length: 26 } },
|
|
64
|
-
uuidv7: { codecId: 'sql/char@1', nativeType: 'character', typeParams: { length: 36 } },
|
|
65
|
-
uuidv4: { codecId: 'sql/char@1', nativeType: 'character', typeParams: { length: 36 } },
|
|
66
|
-
cuid2: { codecId: 'sql/char@1', nativeType: 'character', typeParams: { length: 24 } },
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
function resolveGeneratedColumnDescriptor(
|
|
70
|
-
executionDefault: ExecutionMutationDefaultValue,
|
|
71
|
-
): ColumnDescriptor | undefined {
|
|
72
|
-
if (executionDefault.kind !== 'generator') {
|
|
73
|
-
return undefined;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (executionDefault.id === 'nanoid') {
|
|
77
|
-
const rawSize = executionDefault.params?.['size'];
|
|
78
|
-
const length =
|
|
79
|
-
typeof rawSize === 'number' && Number.isInteger(rawSize) && rawSize >= 2 && rawSize <= 255
|
|
80
|
-
? rawSize
|
|
81
|
-
: 21;
|
|
82
|
-
return { codecId: 'sql/char@1', nativeType: 'character', typeParams: { length } };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return GENERATED_ID_COLUMN_MAP[executionDefault.id];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const REFERENTIAL_ACTION_MAP = {
|
|
89
|
-
NoAction: 'noAction',
|
|
90
|
-
Restrict: 'restrict',
|
|
91
|
-
Cascade: 'cascade',
|
|
92
|
-
SetNull: 'setNull',
|
|
93
|
-
SetDefault: 'setDefault',
|
|
94
|
-
noAction: 'noAction',
|
|
95
|
-
restrict: 'restrict',
|
|
96
|
-
cascade: 'cascade',
|
|
97
|
-
setNull: 'setNull',
|
|
98
|
-
setDefault: 'setDefault',
|
|
99
|
-
} as const;
|
|
100
|
-
|
|
101
|
-
type ResolvedField = {
|
|
102
|
-
readonly field: PslField;
|
|
103
|
-
readonly columnName: string;
|
|
104
|
-
readonly descriptor: ColumnDescriptor;
|
|
105
|
-
readonly defaultValue?: ColumnDefault;
|
|
106
|
-
readonly executionDefault?: ExecutionMutationDefaultValue;
|
|
107
|
-
readonly isId: boolean;
|
|
108
|
-
readonly isUnique: boolean;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
type ParsedRelationAttribute = {
|
|
112
|
-
readonly relationName?: string;
|
|
113
|
-
readonly fields?: readonly string[];
|
|
114
|
-
readonly references?: readonly string[];
|
|
115
|
-
readonly onDelete?: string;
|
|
116
|
-
readonly onUpdate?: string;
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
type FkRelationMetadata = {
|
|
120
|
-
readonly declaringModelName: string;
|
|
121
|
-
readonly declaringFieldName: string;
|
|
122
|
-
readonly declaringTableName: string;
|
|
123
|
-
readonly targetModelName: string;
|
|
124
|
-
readonly targetTableName: string;
|
|
125
|
-
readonly relationName?: string;
|
|
126
|
-
readonly localColumns: readonly string[];
|
|
127
|
-
readonly referencedColumns: readonly string[];
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
type ModelBackrelationCandidate = {
|
|
131
|
-
readonly modelName: string;
|
|
132
|
-
readonly tableName: string;
|
|
133
|
-
readonly field: PslField;
|
|
134
|
-
readonly targetModelName: string;
|
|
135
|
-
readonly relationName?: string;
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
type ModelRelationMetadata = {
|
|
139
|
-
readonly fieldName: string;
|
|
140
|
-
readonly toModel: string;
|
|
141
|
-
readonly toTable: string;
|
|
142
|
-
readonly cardinality: '1:N' | 'N:1';
|
|
143
|
-
readonly parentTable: string;
|
|
144
|
-
readonly parentColumns: readonly string[];
|
|
145
|
-
readonly childTable: string;
|
|
146
|
-
readonly childColumns: readonly string[];
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
type ResolvedModelEntry = {
|
|
150
|
-
readonly model: PslModel;
|
|
151
|
-
readonly mapping: ModelNameMapping;
|
|
152
|
-
readonly resolvedFields: readonly ResolvedField[];
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
function fkRelationPairKey(declaringModelName: string, targetModelName: string): string {
|
|
156
|
-
// NOTE: We assume PSL model identifiers do not contain the `::` separator.
|
|
157
|
-
return `${declaringModelName}::${targetModelName}`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
type ModelNameMapping = {
|
|
161
|
-
readonly model: PslModel;
|
|
162
|
-
readonly tableName: string;
|
|
163
|
-
readonly fieldColumns: Map<string, string>;
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
type DynamicTableBuilder = {
|
|
167
|
-
column(
|
|
168
|
-
name: string,
|
|
169
|
-
options: { type: ColumnDescriptor; nullable?: true; default?: ColumnDefault },
|
|
170
|
-
): DynamicTableBuilder;
|
|
171
|
-
generated(
|
|
172
|
-
name: string,
|
|
173
|
-
options: { type: ColumnDescriptor; generated: ExecutionMutationDefaultValue },
|
|
174
|
-
): DynamicTableBuilder;
|
|
175
|
-
unique(columns: readonly string[]): DynamicTableBuilder;
|
|
176
|
-
primaryKey(columns: readonly string[]): DynamicTableBuilder;
|
|
177
|
-
index(columns: readonly string[]): DynamicTableBuilder;
|
|
178
|
-
foreignKey(
|
|
179
|
-
columns: readonly string[],
|
|
180
|
-
references: { table: string; columns: readonly string[] },
|
|
181
|
-
options?: { onDelete?: string; onUpdate?: string },
|
|
182
|
-
): DynamicTableBuilder;
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
type DynamicModelBuilder = {
|
|
186
|
-
field(name: string, column: string): DynamicModelBuilder;
|
|
187
|
-
relation(
|
|
188
|
-
name: string,
|
|
189
|
-
options: {
|
|
190
|
-
toModel: string;
|
|
191
|
-
toTable: string;
|
|
192
|
-
cardinality: '1:1' | '1:N' | 'N:1';
|
|
193
|
-
on: {
|
|
194
|
-
parentTable: string;
|
|
195
|
-
parentColumns: readonly string[];
|
|
196
|
-
childTable: string;
|
|
197
|
-
childColumns: readonly string[];
|
|
198
|
-
};
|
|
199
|
-
},
|
|
200
|
-
): DynamicModelBuilder;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
type DynamicContractBuilder = {
|
|
204
|
-
target(target: TargetPackRef<'sql', 'postgres'>): DynamicContractBuilder;
|
|
205
|
-
storageType(
|
|
206
|
-
name: string,
|
|
207
|
-
typeInstance: {
|
|
208
|
-
codecId: string;
|
|
209
|
-
nativeType: string;
|
|
210
|
-
typeParams: Record<string, unknown>;
|
|
211
|
-
},
|
|
212
|
-
): DynamicContractBuilder;
|
|
213
|
-
table(
|
|
214
|
-
name: string,
|
|
215
|
-
callback: (tableBuilder: DynamicTableBuilder) => DynamicTableBuilder,
|
|
216
|
-
): DynamicContractBuilder;
|
|
217
|
-
model(
|
|
218
|
-
name: string,
|
|
219
|
-
table: string,
|
|
220
|
-
callback: (modelBuilder: DynamicModelBuilder) => DynamicModelBuilder,
|
|
221
|
-
): DynamicContractBuilder;
|
|
222
|
-
build(): ContractIR;
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
function lowerFirst(value: string): string {
|
|
226
|
-
if (value.length === 0) return value;
|
|
227
|
-
return value[0]?.toLowerCase() + value.slice(1);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function getAttribute(attributes: readonly PslAttribute[], name: string): PslAttribute | undefined {
|
|
231
|
-
return attributes.find((attribute) => attribute.name === name);
|
|
83
|
+
readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', 'postgres'>[];
|
|
84
|
+
readonly controlMutationDefaults?: ControlMutationDefaults;
|
|
85
|
+
readonly authoringContributions?: AuthoringContributions;
|
|
232
86
|
}
|
|
233
87
|
|
|
234
|
-
function
|
|
235
|
-
|
|
236
|
-
|
|
88
|
+
function buildComposedExtensionPackRefs(
|
|
89
|
+
target: TargetPackRef<'sql', 'postgres'>,
|
|
90
|
+
extensionIds: readonly string[],
|
|
91
|
+
extensionPackRefs: readonly ExtensionPackRef<'sql', 'postgres'>[] = [],
|
|
92
|
+
): Record<string, ExtensionPackRef<'sql', 'postgres'>> | undefined {
|
|
93
|
+
if (extensionIds.length === 0) {
|
|
237
94
|
return undefined;
|
|
238
95
|
}
|
|
239
|
-
return entry.value;
|
|
240
|
-
}
|
|
241
96
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
97
|
+
const extensionPackRefById = new Map(extensionPackRefs.map((packRef) => [packRef.id, packRef]));
|
|
98
|
+
|
|
99
|
+
return Object.fromEntries(
|
|
100
|
+
extensionIds.map((extensionId) => [
|
|
101
|
+
extensionId,
|
|
102
|
+
extensionPackRefById.get(extensionId) ??
|
|
103
|
+
({
|
|
104
|
+
kind: 'extension',
|
|
105
|
+
id: extensionId,
|
|
106
|
+
familyId: target.familyId,
|
|
107
|
+
targetId: target.targetId,
|
|
108
|
+
version: '0.0.1',
|
|
109
|
+
} satisfies ExtensionPackRef<'sql', 'postgres'>),
|
|
110
|
+
]),
|
|
111
|
+
);
|
|
249
112
|
}
|
|
250
113
|
|
|
251
|
-
function
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (!entry || entry.kind !== 'positional') {
|
|
258
|
-
return undefined;
|
|
259
|
-
}
|
|
260
|
-
return {
|
|
261
|
-
value: entry.value,
|
|
262
|
-
span: entry.span,
|
|
263
|
-
};
|
|
114
|
+
function diagnosticDedupKey(diagnostic: ContractSourceDiagnostic): string {
|
|
115
|
+
const span = diagnostic.span;
|
|
116
|
+
const spanKey = span
|
|
117
|
+
? `${span.start.offset}:${span.end.offset}:${span.start.line}:${span.end.line}`
|
|
118
|
+
: '';
|
|
119
|
+
return `${diagnostic.code}\u0000${diagnostic.sourceId}\u0000${spanKey}\u0000${diagnostic.message}`;
|
|
264
120
|
}
|
|
265
121
|
|
|
266
|
-
function
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
122
|
+
function dedupeDiagnostics(
|
|
123
|
+
diagnostics: readonly ContractSourceDiagnostic[],
|
|
124
|
+
): ContractSourceDiagnostic[] {
|
|
125
|
+
const seen = new Map<string, ContractSourceDiagnostic>();
|
|
126
|
+
for (const diagnostic of diagnostics) {
|
|
127
|
+
const key = diagnosticDedupKey(diagnostic);
|
|
128
|
+
if (!seen.has(key)) {
|
|
129
|
+
seen.set(key, diagnostic);
|
|
130
|
+
}
|
|
271
131
|
}
|
|
272
|
-
return
|
|
132
|
+
return [...seen.values()];
|
|
273
133
|
}
|
|
274
134
|
|
|
275
|
-
function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// own string literal rules to disallow unescaped interior delimiters.
|
|
279
|
-
const match = trimmed.match(/^(['"])(.*)\1$/);
|
|
280
|
-
if (!match) {
|
|
281
|
-
return undefined;
|
|
135
|
+
function compareStrings(left: string, right: string): -1 | 0 | 1 {
|
|
136
|
+
if (left < right) {
|
|
137
|
+
return -1;
|
|
282
138
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
function parseFieldList(value: string): readonly string[] | undefined {
|
|
287
|
-
const trimmed = value.trim();
|
|
288
|
-
if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) {
|
|
289
|
-
return undefined;
|
|
139
|
+
if (left > right) {
|
|
140
|
+
return 1;
|
|
290
141
|
}
|
|
291
|
-
|
|
292
|
-
const parts = body
|
|
293
|
-
.split(',')
|
|
294
|
-
.map((entry) => entry.trim())
|
|
295
|
-
.filter((entry) => entry.length > 0);
|
|
296
|
-
return parts;
|
|
142
|
+
return 0;
|
|
297
143
|
}
|
|
298
144
|
|
|
299
|
-
function
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return { kind: 'literal', value: numericValue };
|
|
307
|
-
}
|
|
308
|
-
if (/^(['"]).*\1$/.test(trimmed)) {
|
|
309
|
-
return { kind: 'literal', value: unquoteStringLiteral(trimmed) };
|
|
310
|
-
}
|
|
311
|
-
return undefined;
|
|
145
|
+
function mapParserDiagnostics(document: ParsePslDocumentResult): ContractSourceDiagnostic[] {
|
|
146
|
+
return document.diagnostics.map((diagnostic) => ({
|
|
147
|
+
code: diagnostic.code,
|
|
148
|
+
message: diagnostic.message,
|
|
149
|
+
sourceId: diagnostic.sourceId,
|
|
150
|
+
span: diagnostic.span,
|
|
151
|
+
}));
|
|
312
152
|
}
|
|
313
153
|
|
|
314
|
-
|
|
315
|
-
readonly
|
|
316
|
-
readonly fieldName: string;
|
|
317
|
-
readonly defaultAttribute: PslAttribute;
|
|
154
|
+
interface ProcessEnumDeclarationsInput {
|
|
155
|
+
readonly enums: readonly PslEnum[];
|
|
318
156
|
readonly sourceId: string;
|
|
319
|
-
readonly
|
|
157
|
+
readonly enumTypeConstructor: AuthoringTypeConstructorDescriptor | undefined;
|
|
320
158
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
321
|
-
}
|
|
322
|
-
readonly defaultValue?: ColumnDefault;
|
|
323
|
-
readonly executionDefault?: ExecutionMutationDefaultValue;
|
|
324
|
-
} {
|
|
325
|
-
const positionalEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'positional');
|
|
326
|
-
const namedEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'named');
|
|
327
|
-
|
|
328
|
-
if (namedEntries.length > 0 || positionalEntries.length !== 1) {
|
|
329
|
-
input.diagnostics.push({
|
|
330
|
-
code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
|
|
331
|
-
message: `Field "${input.modelName}.${input.fieldName}" requires exactly one positional @default(...) expression.`,
|
|
332
|
-
sourceId: input.sourceId,
|
|
333
|
-
span: input.defaultAttribute.span,
|
|
334
|
-
});
|
|
335
|
-
return {};
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const expressionEntry = getPositionalArgumentEntry(input.defaultAttribute);
|
|
339
|
-
if (!expressionEntry) {
|
|
340
|
-
input.diagnostics.push({
|
|
341
|
-
code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
|
|
342
|
-
message: `Field "${input.modelName}.${input.fieldName}" requires a positional @default(...) expression.`,
|
|
343
|
-
sourceId: input.sourceId,
|
|
344
|
-
span: input.defaultAttribute.span,
|
|
345
|
-
});
|
|
346
|
-
return {};
|
|
347
|
-
}
|
|
159
|
+
}
|
|
348
160
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
161
|
+
function processEnumDeclarations(input: ProcessEnumDeclarationsInput): {
|
|
162
|
+
readonly storageTypes: Record<string, StorageTypeInstance>;
|
|
163
|
+
readonly enumTypeDescriptors: Map<string, ColumnDescriptor>;
|
|
164
|
+
} {
|
|
165
|
+
const storageTypes: Record<string, StorageTypeInstance> = {};
|
|
166
|
+
const enumTypeDescriptors = new Map<string, ColumnDescriptor>();
|
|
353
167
|
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
message: `Unsupported default value "${expressionEntry.value}"`,
|
|
168
|
+
for (const enumDeclaration of input.enums) {
|
|
169
|
+
const nativeType = parseMapName({
|
|
170
|
+
attribute: getAttribute(enumDeclaration.attributes, 'map'),
|
|
171
|
+
defaultValue: enumDeclaration.name,
|
|
359
172
|
sourceId: input.sourceId,
|
|
360
|
-
|
|
173
|
+
diagnostics: input.diagnostics,
|
|
174
|
+
entityLabel: `Enum "${enumDeclaration.name}"`,
|
|
175
|
+
span: enumDeclaration.span,
|
|
361
176
|
});
|
|
362
|
-
|
|
177
|
+
const enumStorageType = input.enumTypeConstructor
|
|
178
|
+
? instantiateAuthoringTypeConstructor(input.enumTypeConstructor, [
|
|
179
|
+
nativeType,
|
|
180
|
+
enumDeclaration.values.map((value) => value.name),
|
|
181
|
+
])
|
|
182
|
+
: {
|
|
183
|
+
codecId: 'pg/enum@1',
|
|
184
|
+
nativeType,
|
|
185
|
+
typeParams: { values: enumDeclaration.values.map((value) => value.name) },
|
|
186
|
+
};
|
|
187
|
+
const descriptor: ColumnDescriptor = {
|
|
188
|
+
codecId: enumStorageType.codecId,
|
|
189
|
+
nativeType: enumStorageType.nativeType,
|
|
190
|
+
typeRef: enumDeclaration.name,
|
|
191
|
+
};
|
|
192
|
+
enumTypeDescriptors.set(enumDeclaration.name, descriptor);
|
|
193
|
+
storageTypes[enumDeclaration.name] = {
|
|
194
|
+
codecId: enumStorageType.codecId,
|
|
195
|
+
nativeType: enumStorageType.nativeType,
|
|
196
|
+
typeParams: enumStorageType.typeParams ?? {
|
|
197
|
+
values: enumDeclaration.values.map((value) => value.name),
|
|
198
|
+
},
|
|
199
|
+
};
|
|
363
200
|
}
|
|
364
201
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
registry: input.defaultFunctionRegistry,
|
|
368
|
-
context: {
|
|
369
|
-
sourceId: input.sourceId,
|
|
370
|
-
modelName: input.modelName,
|
|
371
|
-
fieldName: input.fieldName,
|
|
372
|
-
},
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
if (!lowered.ok) {
|
|
376
|
-
input.diagnostics.push(lowered.diagnostic);
|
|
377
|
-
return {};
|
|
378
|
-
}
|
|
202
|
+
return { storageTypes, enumTypeDescriptors };
|
|
203
|
+
}
|
|
379
204
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
205
|
+
interface ResolveNamedTypeDeclarationsInput {
|
|
206
|
+
readonly declarations: readonly PslNamedTypeDeclaration[];
|
|
207
|
+
readonly sourceId: string;
|
|
208
|
+
readonly enumTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
|
|
209
|
+
readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
|
|
210
|
+
readonly composedExtensions: ReadonlySet<string>;
|
|
211
|
+
readonly familyId: string;
|
|
212
|
+
readonly targetId: string;
|
|
213
|
+
readonly authoringContributions: AuthoringContributions | undefined;
|
|
214
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
384
215
|
}
|
|
385
216
|
|
|
386
|
-
function
|
|
387
|
-
readonly
|
|
388
|
-
readonly defaultValue: string;
|
|
217
|
+
function validateNamedTypeAttributes(input: {
|
|
218
|
+
readonly declaration: PslNamedTypeDeclaration;
|
|
389
219
|
readonly sourceId: string;
|
|
390
220
|
readonly diagnostics: ContractSourceDiagnostic[];
|
|
391
|
-
readonly
|
|
392
|
-
readonly
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
221
|
+
readonly composedExtensions: ReadonlySet<string>;
|
|
222
|
+
readonly allowDbNativeType: boolean;
|
|
223
|
+
readonly familyId: string;
|
|
224
|
+
readonly targetId: string;
|
|
225
|
+
}): {
|
|
226
|
+
readonly dbNativeTypeAttribute: PslAttribute | undefined;
|
|
227
|
+
readonly hasUnsupportedNamedTypeAttribute: boolean;
|
|
228
|
+
} {
|
|
229
|
+
const dbNativeTypeAttributes = input.allowDbNativeType
|
|
230
|
+
? input.declaration.attributes.filter((attribute) => attribute.name.startsWith('db.'))
|
|
231
|
+
: [];
|
|
232
|
+
const [dbNativeTypeAttribute, ...extraDbNativeTypeAttributes] = dbNativeTypeAttributes;
|
|
233
|
+
let hasUnsupportedNamedTypeAttribute = false;
|
|
397
234
|
|
|
398
|
-
const
|
|
399
|
-
if (!value) {
|
|
400
|
-
input.diagnostics.push({
|
|
401
|
-
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
402
|
-
message: `${input.entityLabel} @map requires a positional quoted string literal argument`,
|
|
403
|
-
sourceId: input.sourceId,
|
|
404
|
-
span: input.attribute.span,
|
|
405
|
-
});
|
|
406
|
-
return input.defaultValue;
|
|
407
|
-
}
|
|
408
|
-
const parsed = parseQuotedStringLiteral(value);
|
|
409
|
-
if (parsed === undefined) {
|
|
235
|
+
for (const extra of extraDbNativeTypeAttributes) {
|
|
410
236
|
input.diagnostics.push({
|
|
411
237
|
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
412
|
-
message:
|
|
238
|
+
message: `Named type "${input.declaration.name}" can declare at most one @db.* attribute`,
|
|
413
239
|
sourceId: input.sourceId,
|
|
414
|
-
span:
|
|
240
|
+
span: extra.span,
|
|
415
241
|
});
|
|
416
|
-
|
|
242
|
+
hasUnsupportedNamedTypeAttribute = true;
|
|
417
243
|
}
|
|
418
|
-
return parsed;
|
|
419
|
-
}
|
|
420
244
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const raw = namedLength ?? namedDim ?? positional;
|
|
430
|
-
if (!raw) {
|
|
431
|
-
input.diagnostics.push({
|
|
432
|
-
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
433
|
-
message: '@pgvector.column requires length/dim argument',
|
|
434
|
-
sourceId: input.sourceId,
|
|
435
|
-
span: input.attribute.span,
|
|
245
|
+
for (const attribute of input.declaration.attributes) {
|
|
246
|
+
if (input.allowDbNativeType && attribute.name.startsWith('db.')) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
|
|
251
|
+
familyId: input.familyId,
|
|
252
|
+
targetId: input.targetId,
|
|
436
253
|
});
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
254
|
+
if (uncomposedNamespace) {
|
|
255
|
+
reportUncomposedNamespace({
|
|
256
|
+
subjectLabel: `Attribute "@${attribute.name}"`,
|
|
257
|
+
namespace: uncomposedNamespace,
|
|
258
|
+
sourceId: input.sourceId,
|
|
259
|
+
span: attribute.span,
|
|
260
|
+
diagnostics: input.diagnostics,
|
|
261
|
+
});
|
|
262
|
+
hasUnsupportedNamedTypeAttribute = true;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
|
|
441
266
|
input.diagnostics.push({
|
|
442
|
-
code: '
|
|
443
|
-
message:
|
|
267
|
+
code: 'PSL_UNSUPPORTED_NAMED_TYPE_ATTRIBUTE',
|
|
268
|
+
message: `Named type "${input.declaration.name}" uses unsupported attribute "${attribute.name}"`,
|
|
444
269
|
sourceId: input.sourceId,
|
|
445
|
-
span:
|
|
270
|
+
span: attribute.span,
|
|
446
271
|
});
|
|
447
|
-
|
|
272
|
+
hasUnsupportedNamedTypeAttribute = true;
|
|
448
273
|
}
|
|
449
|
-
return parsed;
|
|
450
|
-
}
|
|
451
274
|
|
|
452
|
-
|
|
453
|
-
field: PslField,
|
|
454
|
-
enumTypeDescriptors: Map<string, ColumnDescriptor>,
|
|
455
|
-
namedTypeDescriptors: Map<string, ColumnDescriptor>,
|
|
456
|
-
): ColumnDescriptor | undefined {
|
|
457
|
-
if (field.typeRef && namedTypeDescriptors.has(field.typeRef)) {
|
|
458
|
-
return namedTypeDescriptors.get(field.typeRef);
|
|
459
|
-
}
|
|
460
|
-
if (namedTypeDescriptors.has(field.typeName)) {
|
|
461
|
-
return namedTypeDescriptors.get(field.typeName);
|
|
462
|
-
}
|
|
463
|
-
if (enumTypeDescriptors.has(field.typeName)) {
|
|
464
|
-
return enumTypeDescriptors.get(field.typeName);
|
|
465
|
-
}
|
|
466
|
-
return SCALAR_COLUMN_MAP[field.typeName];
|
|
275
|
+
return { dbNativeTypeAttribute, hasUnsupportedNamedTypeAttribute };
|
|
467
276
|
}
|
|
468
277
|
|
|
469
|
-
function
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
modelNames: Set<string>,
|
|
476
|
-
composedExtensions: Set<string>,
|
|
477
|
-
defaultFunctionRegistry: DefaultFunctionRegistry,
|
|
478
|
-
diagnostics: ContractSourceDiagnostic[],
|
|
479
|
-
sourceId: string,
|
|
480
|
-
): ResolvedField[] {
|
|
481
|
-
const resolvedFields: ResolvedField[] = [];
|
|
278
|
+
function resolveNamedTypeDeclarations(input: ResolveNamedTypeDeclarationsInput): {
|
|
279
|
+
readonly storageTypes: Record<string, StorageTypeInstance>;
|
|
280
|
+
readonly namedTypeDescriptors: Map<string, ColumnDescriptor>;
|
|
281
|
+
} {
|
|
282
|
+
const storageTypes: Record<string, StorageTypeInstance> = {};
|
|
283
|
+
const namedTypeDescriptors = new Map<string, ColumnDescriptor>();
|
|
482
284
|
|
|
483
|
-
for (const
|
|
484
|
-
if (
|
|
485
|
-
|
|
285
|
+
for (const declaration of input.declarations) {
|
|
286
|
+
if (declaration.typeConstructor) {
|
|
287
|
+
const { hasUnsupportedNamedTypeAttribute } = validateNamedTypeAttributes({
|
|
288
|
+
declaration,
|
|
289
|
+
sourceId: input.sourceId,
|
|
290
|
+
diagnostics: input.diagnostics,
|
|
291
|
+
composedExtensions: input.composedExtensions,
|
|
292
|
+
allowDbNativeType: false,
|
|
293
|
+
familyId: input.familyId,
|
|
294
|
+
targetId: input.targetId,
|
|
295
|
+
});
|
|
296
|
+
if (hasUnsupportedNamedTypeAttribute) {
|
|
486
297
|
continue;
|
|
487
298
|
}
|
|
488
|
-
diagnostics.push({
|
|
489
|
-
code: 'PSL_UNSUPPORTED_FIELD_LIST',
|
|
490
|
-
message: `Field "${model.name}.${field.name}" uses a scalar/storage list type, which is not supported in SQL PSL provider v1. Model-typed lists are only supported as backrelation navigation fields when they match an FK-side relation.`,
|
|
491
|
-
sourceId,
|
|
492
|
-
span: field.span,
|
|
493
|
-
});
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
299
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
300
|
+
const helperPath = declaration.typeConstructor.path.join('.');
|
|
301
|
+
const typeConstructor = resolvePslTypeConstructorDescriptor({
|
|
302
|
+
call: declaration.typeConstructor,
|
|
303
|
+
authoringContributions: input.authoringContributions,
|
|
304
|
+
composedExtensions: input.composedExtensions,
|
|
305
|
+
familyId: input.familyId,
|
|
306
|
+
targetId: input.targetId,
|
|
307
|
+
diagnostics: input.diagnostics,
|
|
308
|
+
sourceId: input.sourceId,
|
|
309
|
+
unsupportedCode: 'PSL_UNSUPPORTED_NAMED_TYPE_CONSTRUCTOR',
|
|
310
|
+
unsupportedMessage: `Named type "${declaration.name}" references unsupported constructor "${helperPath}"`,
|
|
311
|
+
});
|
|
312
|
+
if (!typeConstructor) {
|
|
506
313
|
continue;
|
|
507
314
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
}
|
|
315
|
+
|
|
316
|
+
const storageType = instantiatePslTypeConstructor({
|
|
317
|
+
call: declaration.typeConstructor,
|
|
318
|
+
descriptor: typeConstructor,
|
|
319
|
+
diagnostics: input.diagnostics,
|
|
320
|
+
sourceId: input.sourceId,
|
|
321
|
+
entityLabel: `Named type "${declaration.name}"`,
|
|
322
|
+
});
|
|
323
|
+
if (!storageType) {
|
|
515
324
|
continue;
|
|
516
325
|
}
|
|
517
|
-
diagnostics.push({
|
|
518
|
-
code: 'PSL_UNSUPPORTED_FIELD_ATTRIBUTE',
|
|
519
|
-
message: `Field "${model.name}.${field.name}" uses unsupported attribute "@${attribute.name}"`,
|
|
520
|
-
sourceId,
|
|
521
|
-
span: attribute.span,
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
326
|
|
|
525
|
-
|
|
526
|
-
|
|
327
|
+
namedTypeDescriptors.set(
|
|
328
|
+
declaration.name,
|
|
329
|
+
toNamedTypeFieldDescriptor(declaration.name, storageType),
|
|
330
|
+
);
|
|
331
|
+
storageTypes[declaration.name] = {
|
|
332
|
+
codecId: storageType.codecId,
|
|
333
|
+
nativeType: storageType.nativeType,
|
|
334
|
+
typeParams: storageType.typeParams ?? {},
|
|
335
|
+
};
|
|
527
336
|
continue;
|
|
528
337
|
}
|
|
529
338
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
} else {
|
|
542
|
-
const isBytesBase =
|
|
543
|
-
field.typeName === 'Bytes' ||
|
|
544
|
-
namedTypeBaseTypes.get(field.typeRef ?? field.typeName) === 'Bytes';
|
|
545
|
-
if (!isBytesBase) {
|
|
546
|
-
diagnostics.push({
|
|
547
|
-
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
548
|
-
message: `Field "${model.name}.${field.name}" uses @pgvector.column on unsupported base type "${field.typeName}"`,
|
|
549
|
-
sourceId,
|
|
550
|
-
span: pgvectorColumnAttribute.span,
|
|
551
|
-
});
|
|
552
|
-
} else {
|
|
553
|
-
const length = parsePgvectorLength({
|
|
554
|
-
attribute: pgvectorColumnAttribute,
|
|
555
|
-
diagnostics,
|
|
556
|
-
sourceId,
|
|
557
|
-
});
|
|
558
|
-
if (length !== undefined) {
|
|
559
|
-
descriptor = {
|
|
560
|
-
codecId: 'pg/vector@1',
|
|
561
|
-
nativeType: `vector(${length})`,
|
|
562
|
-
typeParams: { length },
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
339
|
+
// Parser invariant: when typeConstructor is absent, baseType is defined.
|
|
340
|
+
// The check below narrows `baseType` for TypeScript and guards against a
|
|
341
|
+
// parser regression; it is unreachable under a correct parser.
|
|
342
|
+
if (declaration.baseType === undefined) {
|
|
343
|
+
input.diagnostics.push({
|
|
344
|
+
code: 'PSL_UNSUPPORTED_NAMED_TYPE_BASE',
|
|
345
|
+
message: `Named type "${declaration.name}" must declare a base type or constructor`,
|
|
346
|
+
sourceId: input.sourceId,
|
|
347
|
+
span: declaration.span,
|
|
348
|
+
});
|
|
349
|
+
continue;
|
|
567
350
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
351
|
+
const { baseType } = declaration;
|
|
352
|
+
const baseDescriptor =
|
|
353
|
+
input.enumTypeDescriptors.get(baseType) ?? input.scalarTypeDescriptors.get(baseType);
|
|
354
|
+
if (!baseDescriptor) {
|
|
355
|
+
input.diagnostics.push({
|
|
356
|
+
code: 'PSL_UNSUPPORTED_NAMED_TYPE_BASE',
|
|
357
|
+
message: `Named type "${declaration.name}" references unsupported base type "${baseType}"`,
|
|
358
|
+
sourceId: input.sourceId,
|
|
359
|
+
span: declaration.span,
|
|
575
360
|
});
|
|
576
361
|
continue;
|
|
577
362
|
}
|
|
578
363
|
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
if (
|
|
591
|
-
const generatorDescription =
|
|
592
|
-
loweredDefault.executionDefault.kind === 'generator'
|
|
593
|
-
? `"${loweredDefault.executionDefault.id}"`
|
|
594
|
-
: 'for this field';
|
|
595
|
-
diagnostics.push({
|
|
596
|
-
code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
|
|
597
|
-
message: `Field "${model.name}.${field.name}" cannot be optional when using execution default ${generatorDescription}. Remove "?" or use a storage default.`,
|
|
598
|
-
sourceId,
|
|
599
|
-
span: defaultAttribute?.span ?? field.span,
|
|
600
|
-
});
|
|
364
|
+
const { dbNativeTypeAttribute, hasUnsupportedNamedTypeAttribute } = validateNamedTypeAttributes(
|
|
365
|
+
{
|
|
366
|
+
declaration,
|
|
367
|
+
sourceId: input.sourceId,
|
|
368
|
+
diagnostics: input.diagnostics,
|
|
369
|
+
composedExtensions: input.composedExtensions,
|
|
370
|
+
allowDbNativeType: true,
|
|
371
|
+
familyId: input.familyId,
|
|
372
|
+
targetId: input.targetId,
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
if (hasUnsupportedNamedTypeAttribute) {
|
|
601
376
|
continue;
|
|
602
377
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
378
|
+
|
|
379
|
+
if (dbNativeTypeAttribute) {
|
|
380
|
+
const descriptor = resolveDbNativeTypeAttribute({
|
|
381
|
+
attribute: dbNativeTypeAttribute,
|
|
382
|
+
baseType,
|
|
383
|
+
baseDescriptor,
|
|
384
|
+
diagnostics: input.diagnostics,
|
|
385
|
+
sourceId: input.sourceId,
|
|
386
|
+
entityLabel: `Named type "${declaration.name}"`,
|
|
387
|
+
});
|
|
388
|
+
if (!descriptor) {
|
|
389
|
+
continue;
|
|
607
390
|
}
|
|
391
|
+
namedTypeDescriptors.set(
|
|
392
|
+
declaration.name,
|
|
393
|
+
toNamedTypeFieldDescriptor(declaration.name, descriptor),
|
|
394
|
+
);
|
|
395
|
+
storageTypes[declaration.name] = {
|
|
396
|
+
codecId: descriptor.codecId,
|
|
397
|
+
nativeType: descriptor.nativeType,
|
|
398
|
+
typeParams: descriptor.typeParams ?? {},
|
|
399
|
+
};
|
|
400
|
+
continue;
|
|
608
401
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
isUnique: Boolean(getAttribute(field.attributes, 'unique')),
|
|
618
|
-
});
|
|
402
|
+
|
|
403
|
+
const descriptor = toNamedTypeFieldDescriptor(declaration.name, baseDescriptor);
|
|
404
|
+
namedTypeDescriptors.set(declaration.name, descriptor);
|
|
405
|
+
storageTypes[declaration.name] = {
|
|
406
|
+
codecId: baseDescriptor.codecId,
|
|
407
|
+
nativeType: baseDescriptor.nativeType,
|
|
408
|
+
typeParams: {},
|
|
409
|
+
};
|
|
619
410
|
}
|
|
620
411
|
|
|
621
|
-
return
|
|
412
|
+
return { storageTypes, namedTypeDescriptors };
|
|
622
413
|
}
|
|
623
414
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
415
|
+
interface BuildModelNodeInput {
|
|
416
|
+
readonly model: PslModel;
|
|
417
|
+
readonly mapping: ModelNameMapping;
|
|
418
|
+
readonly modelMappings: ReadonlyMap<string, ModelNameMapping>;
|
|
419
|
+
readonly modelNames: Set<string>;
|
|
420
|
+
readonly compositeTypeNames: ReadonlySet<string>;
|
|
421
|
+
readonly enumTypeDescriptors: Map<string, ColumnDescriptor>;
|
|
422
|
+
readonly namedTypeDescriptors: Map<string, ColumnDescriptor>;
|
|
423
|
+
readonly composedExtensions: Set<string>;
|
|
424
|
+
readonly familyId: string;
|
|
425
|
+
readonly targetId: string;
|
|
426
|
+
readonly authoringContributions: AuthoringContributions | undefined;
|
|
427
|
+
readonly defaultFunctionRegistry: ControlMutationDefaultRegistry;
|
|
428
|
+
readonly generatorDescriptorById: ReadonlyMap<string, MutationDefaultGeneratorDescriptor>;
|
|
429
|
+
readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
|
|
430
|
+
readonly sourceId: string;
|
|
431
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
631
432
|
}
|
|
632
433
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
return 1;
|
|
639
|
-
}
|
|
640
|
-
return 0;
|
|
434
|
+
interface BuildModelNodeResult {
|
|
435
|
+
readonly modelNode: ModelNode;
|
|
436
|
+
readonly fkRelationMetadata: FkRelationMetadata[];
|
|
437
|
+
readonly backrelationCandidates: ModelBackrelationCandidate[];
|
|
438
|
+
readonly resolvedFields: readonly ResolvedField[];
|
|
641
439
|
}
|
|
642
440
|
|
|
643
|
-
function
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
childColumns: relation.referencedColumns,
|
|
665
|
-
});
|
|
441
|
+
function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult {
|
|
442
|
+
const { model, mapping, sourceId, diagnostics } = input;
|
|
443
|
+
const tableName = mapping.tableName;
|
|
444
|
+
|
|
445
|
+
const resolvedFields = collectResolvedFields({
|
|
446
|
+
model,
|
|
447
|
+
mapping,
|
|
448
|
+
enumTypeDescriptors: input.enumTypeDescriptors,
|
|
449
|
+
namedTypeDescriptors: input.namedTypeDescriptors,
|
|
450
|
+
modelNames: input.modelNames,
|
|
451
|
+
compositeTypeNames: input.compositeTypeNames,
|
|
452
|
+
composedExtensions: input.composedExtensions,
|
|
453
|
+
authoringContributions: input.authoringContributions,
|
|
454
|
+
familyId: input.familyId,
|
|
455
|
+
targetId: input.targetId,
|
|
456
|
+
defaultFunctionRegistry: input.defaultFunctionRegistry,
|
|
457
|
+
generatorDescriptorById: input.generatorDescriptorById,
|
|
458
|
+
diagnostics,
|
|
459
|
+
sourceId,
|
|
460
|
+
scalarTypeDescriptors: input.scalarTypeDescriptors,
|
|
461
|
+
});
|
|
666
462
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function applyBackrelationCandidates(input: {
|
|
680
|
-
readonly backrelationCandidates: readonly ModelBackrelationCandidate[];
|
|
681
|
-
readonly fkRelationsByPair: Map<string, readonly FkRelationMetadata[]>;
|
|
682
|
-
readonly modelRelations: Map<string, ModelRelationMetadata[]>;
|
|
683
|
-
readonly diagnostics: ContractSourceDiagnostic[];
|
|
684
|
-
readonly sourceId: string;
|
|
685
|
-
}): void {
|
|
686
|
-
for (const candidate of input.backrelationCandidates) {
|
|
687
|
-
const pairKey = fkRelationPairKey(candidate.targetModelName, candidate.modelName);
|
|
688
|
-
const pairMatches = input.fkRelationsByPair.get(pairKey) ?? [];
|
|
689
|
-
const matches = candidate.relationName
|
|
690
|
-
? pairMatches.filter((relation) => relation.relationName === candidate.relationName)
|
|
691
|
-
: [...pairMatches];
|
|
692
|
-
|
|
693
|
-
if (matches.length === 0) {
|
|
694
|
-
input.diagnostics.push({
|
|
695
|
-
code: 'PSL_ORPHANED_BACKRELATION_LIST',
|
|
696
|
-
message: `Backrelation list field "${candidate.modelName}.${candidate.field.name}" has no matching FK-side relation on model "${candidate.targetModelName}". Add @relation(fields: [...], references: [...]) on the FK-side relation or use an explicit join model for many-to-many.`,
|
|
697
|
-
sourceId: input.sourceId,
|
|
698
|
-
span: candidate.field.span,
|
|
699
|
-
});
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
if (matches.length > 1) {
|
|
703
|
-
input.diagnostics.push({
|
|
704
|
-
code: 'PSL_AMBIGUOUS_BACKRELATION_LIST',
|
|
705
|
-
message: `Backrelation list field "${candidate.modelName}.${candidate.field.name}" matches multiple FK-side relations on model "${candidate.targetModelName}". Add @relation(name: "...") (or @relation("...")) to both sides to disambiguate.`,
|
|
706
|
-
sourceId: input.sourceId,
|
|
707
|
-
span: candidate.field.span,
|
|
708
|
-
});
|
|
709
|
-
continue;
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
invariant(matches.length === 1, 'Backrelation matching requires exactly one match');
|
|
713
|
-
const matched = matches[0];
|
|
714
|
-
assertDefined(matched, 'Backrelation matching requires a defined relation match');
|
|
715
|
-
|
|
716
|
-
const existing = input.modelRelations.get(candidate.modelName);
|
|
717
|
-
const current = existing ?? [];
|
|
718
|
-
if (!existing) {
|
|
719
|
-
input.modelRelations.set(candidate.modelName, current);
|
|
720
|
-
}
|
|
721
|
-
current.push({
|
|
722
|
-
fieldName: candidate.field.name,
|
|
723
|
-
toModel: matched.declaringModelName,
|
|
724
|
-
toTable: matched.declaringTableName,
|
|
725
|
-
cardinality: '1:N',
|
|
726
|
-
parentTable: candidate.tableName,
|
|
727
|
-
parentColumns: matched.referencedColumns,
|
|
728
|
-
childTable: matched.declaringTableName,
|
|
729
|
-
childColumns: matched.localColumns,
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function emitModelsWithRelations(input: {
|
|
735
|
-
readonly builder: DynamicContractBuilder;
|
|
736
|
-
readonly resolvedModels: ResolvedModelEntry[];
|
|
737
|
-
readonly modelRelations: Map<string, readonly ModelRelationMetadata[]>;
|
|
738
|
-
}): DynamicContractBuilder {
|
|
739
|
-
let nextBuilder = input.builder;
|
|
740
|
-
|
|
741
|
-
const sortedModels = input.resolvedModels.sort((left, right) => {
|
|
742
|
-
const tableComparison = compareStrings(left.mapping.tableName, right.mapping.tableName);
|
|
743
|
-
if (tableComparison === 0) {
|
|
744
|
-
return compareStrings(left.model.name, right.model.name);
|
|
745
|
-
}
|
|
746
|
-
return tableComparison;
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
for (const entry of sortedModels) {
|
|
750
|
-
const relationEntries = [...(input.modelRelations.get(entry.model.name) ?? [])].sort(
|
|
751
|
-
(left, right) => compareStrings(left.fieldName, right.fieldName),
|
|
752
|
-
);
|
|
753
|
-
nextBuilder = nextBuilder.model(
|
|
754
|
-
entry.model.name,
|
|
755
|
-
entry.mapping.tableName,
|
|
756
|
-
(modelBuilder: DynamicModelBuilder) => {
|
|
757
|
-
let next = modelBuilder;
|
|
758
|
-
for (const resolvedField of entry.resolvedFields) {
|
|
759
|
-
next = next.field(resolvedField.field.name, resolvedField.columnName);
|
|
760
|
-
}
|
|
761
|
-
for (const relation of relationEntries) {
|
|
762
|
-
next = next.relation(relation.fieldName, {
|
|
763
|
-
toModel: relation.toModel,
|
|
764
|
-
toTable: relation.toTable,
|
|
765
|
-
cardinality: relation.cardinality,
|
|
766
|
-
on: {
|
|
767
|
-
parentTable: relation.parentTable,
|
|
768
|
-
parentColumns: relation.parentColumns,
|
|
769
|
-
childTable: relation.childTable,
|
|
770
|
-
childColumns: relation.childColumns,
|
|
771
|
-
},
|
|
772
|
-
});
|
|
773
|
-
}
|
|
774
|
-
return next;
|
|
775
|
-
},
|
|
776
|
-
);
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
return nextBuilder;
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
function mapParserDiagnostics(document: ParsePslDocumentResult): ContractSourceDiagnostic[] {
|
|
783
|
-
return document.diagnostics.map((diagnostic) => ({
|
|
784
|
-
code: diagnostic.code,
|
|
785
|
-
message: diagnostic.message,
|
|
786
|
-
sourceId: diagnostic.sourceId,
|
|
787
|
-
span: diagnostic.span,
|
|
788
|
-
}));
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
function normalizeReferentialAction(input: {
|
|
792
|
-
readonly modelName: string;
|
|
793
|
-
readonly fieldName: string;
|
|
794
|
-
readonly actionName: 'onDelete' | 'onUpdate';
|
|
795
|
-
readonly actionToken: string;
|
|
796
|
-
readonly sourceId: string;
|
|
797
|
-
readonly span: PslSpan;
|
|
798
|
-
readonly diagnostics: ContractSourceDiagnostic[];
|
|
799
|
-
}): string | undefined {
|
|
800
|
-
const normalized =
|
|
801
|
-
REFERENTIAL_ACTION_MAP[input.actionToken as keyof typeof REFERENTIAL_ACTION_MAP];
|
|
802
|
-
if (normalized) {
|
|
803
|
-
return normalized;
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
input.diagnostics.push({
|
|
807
|
-
code: 'PSL_UNSUPPORTED_REFERENTIAL_ACTION',
|
|
808
|
-
message: `Relation field "${input.modelName}.${input.fieldName}" has unsupported ${input.actionName} action "${input.actionToken}"`,
|
|
809
|
-
sourceId: input.sourceId,
|
|
810
|
-
span: input.span,
|
|
811
|
-
});
|
|
812
|
-
return undefined;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
function parseAttributeFieldList(input: {
|
|
816
|
-
readonly attribute: PslAttribute;
|
|
817
|
-
readonly sourceId: string;
|
|
818
|
-
readonly diagnostics: ContractSourceDiagnostic[];
|
|
819
|
-
readonly code: string;
|
|
820
|
-
readonly messagePrefix: string;
|
|
821
|
-
}): readonly string[] | undefined {
|
|
822
|
-
const raw = getNamedArgument(input.attribute, 'fields') ?? getPositionalArgument(input.attribute);
|
|
823
|
-
if (!raw) {
|
|
824
|
-
input.diagnostics.push({
|
|
825
|
-
code: input.code,
|
|
826
|
-
message: `${input.messagePrefix} requires fields list argument`,
|
|
827
|
-
sourceId: input.sourceId,
|
|
828
|
-
span: input.attribute.span,
|
|
829
|
-
});
|
|
830
|
-
return undefined;
|
|
831
|
-
}
|
|
832
|
-
const fields = parseFieldList(raw);
|
|
833
|
-
if (!fields || fields.length === 0) {
|
|
834
|
-
input.diagnostics.push({
|
|
835
|
-
code: input.code,
|
|
836
|
-
message: `${input.messagePrefix} requires bracketed field list argument`,
|
|
837
|
-
sourceId: input.sourceId,
|
|
838
|
-
span: input.attribute.span,
|
|
463
|
+
const primaryKeyFields = resolvedFields.filter((field) => field.isId);
|
|
464
|
+
const primaryKeyColumns = primaryKeyFields.map((field) => field.columnName);
|
|
465
|
+
const primaryKeyName = primaryKeyFields.length === 1 ? primaryKeyFields[0]?.idName : undefined;
|
|
466
|
+
const isVariantModel = model.attributes.some((attr) => attr.name === 'base');
|
|
467
|
+
if (primaryKeyColumns.length === 0 && !isVariantModel) {
|
|
468
|
+
diagnostics.push({
|
|
469
|
+
code: 'PSL_MISSING_PRIMARY_KEY',
|
|
470
|
+
message: `Model "${model.name}" must declare at least one @id field for SQL provider`,
|
|
471
|
+
sourceId,
|
|
472
|
+
span: model.span,
|
|
839
473
|
});
|
|
840
|
-
return undefined;
|
|
841
474
|
}
|
|
842
|
-
return fields;
|
|
843
|
-
}
|
|
844
475
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
readonly sourceId: string;
|
|
850
|
-
readonly diagnostics: ContractSourceDiagnostic[];
|
|
851
|
-
readonly span: PslSpan;
|
|
852
|
-
readonly contextLabel: string;
|
|
853
|
-
}): readonly string[] | undefined {
|
|
854
|
-
const columns: string[] = [];
|
|
855
|
-
for (const fieldName of input.fieldNames) {
|
|
856
|
-
const columnName = input.mapping.fieldColumns.get(fieldName);
|
|
857
|
-
if (!columnName) {
|
|
858
|
-
input.diagnostics.push({
|
|
859
|
-
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
860
|
-
message: `${input.contextLabel} references unknown field "${input.modelName}.${fieldName}"`,
|
|
861
|
-
sourceId: input.sourceId,
|
|
862
|
-
span: input.span,
|
|
863
|
-
});
|
|
864
|
-
return undefined;
|
|
476
|
+
const resultBackrelationCandidates: ModelBackrelationCandidate[] = [];
|
|
477
|
+
for (const field of model.fields) {
|
|
478
|
+
if (!field.list || !input.modelNames.has(field.typeName)) {
|
|
479
|
+
continue;
|
|
865
480
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
function buildModelMappings(
|
|
872
|
-
models: readonly PslModel[],
|
|
873
|
-
diagnostics: ContractSourceDiagnostic[],
|
|
874
|
-
sourceId: string,
|
|
875
|
-
): Map<string, ModelNameMapping> {
|
|
876
|
-
const result = new Map<string, ModelNameMapping>();
|
|
877
|
-
for (const model of models) {
|
|
878
|
-
const mapAttribute = getAttribute(model.attributes, 'map');
|
|
879
|
-
const tableName = parseMapName({
|
|
880
|
-
attribute: mapAttribute,
|
|
881
|
-
defaultValue: lowerFirst(model.name),
|
|
481
|
+
const attributesValid = validateNavigationListFieldAttributes({
|
|
482
|
+
modelName: model.name,
|
|
483
|
+
field,
|
|
882
484
|
sourceId,
|
|
485
|
+
composedExtensions: input.composedExtensions,
|
|
883
486
|
diagnostics,
|
|
884
|
-
|
|
885
|
-
|
|
487
|
+
familyId: input.familyId,
|
|
488
|
+
targetId: input.targetId,
|
|
886
489
|
});
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
const
|
|
891
|
-
attribute:
|
|
892
|
-
|
|
490
|
+
const relationAttribute = getAttribute(field.attributes, 'relation');
|
|
491
|
+
let relationName: string | undefined;
|
|
492
|
+
if (relationAttribute) {
|
|
493
|
+
const parsedRelation = parseRelationAttribute({
|
|
494
|
+
attribute: relationAttribute,
|
|
495
|
+
modelName: model.name,
|
|
496
|
+
fieldName: field.name,
|
|
893
497
|
sourceId,
|
|
894
498
|
diagnostics,
|
|
895
|
-
entityLabel: `Field "${model.name}.${field.name}"`,
|
|
896
|
-
span: field.span,
|
|
897
499
|
});
|
|
898
|
-
|
|
500
|
+
if (!parsedRelation) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (parsedRelation.fields || parsedRelation.references) {
|
|
504
|
+
diagnostics.push({
|
|
505
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
506
|
+
message: `Backrelation list field "${model.name}.${field.name}" cannot declare fields/references; define them on the FK-side relation field`,
|
|
507
|
+
sourceId,
|
|
508
|
+
span: relationAttribute.span,
|
|
509
|
+
});
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (parsedRelation.onDelete || parsedRelation.onUpdate) {
|
|
513
|
+
diagnostics.push({
|
|
514
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
515
|
+
message: `Backrelation list field "${model.name}.${field.name}" cannot declare onDelete/onUpdate; define referential actions on the FK-side relation field`,
|
|
516
|
+
sourceId,
|
|
517
|
+
span: relationAttribute.span,
|
|
518
|
+
});
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
relationName = parsedRelation.relationName;
|
|
899
522
|
}
|
|
900
|
-
|
|
901
|
-
|
|
523
|
+
if (!attributesValid) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
resultBackrelationCandidates.push({
|
|
528
|
+
modelName: model.name,
|
|
902
529
|
tableName,
|
|
903
|
-
|
|
530
|
+
field,
|
|
531
|
+
targetModelName: field.typeName,
|
|
532
|
+
...ifDefined('relationName', relationName),
|
|
904
533
|
});
|
|
905
534
|
}
|
|
906
|
-
return result;
|
|
907
|
-
}
|
|
908
535
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
536
|
+
const relationAttributes = model.fields
|
|
537
|
+
.map((field) => ({
|
|
538
|
+
field,
|
|
539
|
+
relation: getAttribute(field.attributes, 'relation'),
|
|
540
|
+
}))
|
|
541
|
+
.filter((entry): entry is { field: PslField; relation: PslAttribute } =>
|
|
542
|
+
Boolean(entry.relation),
|
|
543
|
+
);
|
|
544
|
+
const uniqueConstraints: UniqueConstraintNode[] = resolvedFields
|
|
545
|
+
.filter((field) => field.isUnique)
|
|
546
|
+
.map((field) => ({
|
|
547
|
+
columns: [field.columnName],
|
|
548
|
+
...ifDefined('name', field.uniqueName),
|
|
549
|
+
}));
|
|
550
|
+
const indexNodes: IndexNode[] = [];
|
|
551
|
+
const foreignKeyNodes: ForeignKeyNode[] = [];
|
|
552
|
+
|
|
553
|
+
for (const modelAttribute of model.attributes) {
|
|
554
|
+
if (modelAttribute.name === 'map') {
|
|
919
555
|
continue;
|
|
920
556
|
}
|
|
921
|
-
if (
|
|
922
|
-
input.diagnostics.push({
|
|
923
|
-
code: 'PSL_EXTENSION_NAMESPACE_NOT_COMPOSED',
|
|
924
|
-
message: `Attribute "@${attribute.name}" uses unrecognized namespace "pgvector". Add extension pack "pgvector" to extensionPacks in prisma-next.config.ts.`,
|
|
925
|
-
sourceId: input.sourceId,
|
|
926
|
-
span: attribute.span,
|
|
927
|
-
});
|
|
928
|
-
valid = false;
|
|
557
|
+
if (modelAttribute.name === 'discriminator' || modelAttribute.name === 'base') {
|
|
929
558
|
continue;
|
|
930
559
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
}
|
|
939
|
-
return valid;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
function parseRelationAttribute(input: {
|
|
943
|
-
readonly attribute: PslAttribute;
|
|
944
|
-
readonly modelName: string;
|
|
945
|
-
readonly fieldName: string;
|
|
946
|
-
readonly sourceId: string;
|
|
947
|
-
readonly diagnostics: ContractSourceDiagnostic[];
|
|
948
|
-
}): ParsedRelationAttribute | undefined {
|
|
949
|
-
const positionalEntries = input.attribute.args.filter((arg) => arg.kind === 'positional');
|
|
950
|
-
if (positionalEntries.length > 1) {
|
|
951
|
-
input.diagnostics.push({
|
|
952
|
-
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
953
|
-
message: `Relation field "${input.modelName}.${input.fieldName}" has too many positional arguments`,
|
|
954
|
-
sourceId: input.sourceId,
|
|
955
|
-
span: input.attribute.span,
|
|
956
|
-
});
|
|
957
|
-
return undefined;
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
let relationNameFromPositional: string | undefined;
|
|
961
|
-
const positionalNameEntry = getPositionalArgumentEntry(input.attribute);
|
|
962
|
-
if (positionalNameEntry) {
|
|
963
|
-
const parsedName = parseQuotedStringLiteral(positionalNameEntry.value);
|
|
964
|
-
if (!parsedName) {
|
|
965
|
-
input.diagnostics.push({
|
|
966
|
-
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
967
|
-
message: `Relation field "${input.modelName}.${input.fieldName}" positional relation name must be a quoted string literal`,
|
|
968
|
-
sourceId: input.sourceId,
|
|
969
|
-
span: positionalNameEntry.span,
|
|
560
|
+
if (modelAttribute.name === 'unique' || modelAttribute.name === 'index') {
|
|
561
|
+
const fieldNames = parseAttributeFieldList({
|
|
562
|
+
attribute: modelAttribute,
|
|
563
|
+
sourceId,
|
|
564
|
+
diagnostics,
|
|
565
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
566
|
+
messagePrefix: `Model "${model.name}" @@${modelAttribute.name}`,
|
|
970
567
|
});
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
568
|
+
if (!fieldNames) {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
const columnNames = mapFieldNamesToColumns({
|
|
572
|
+
modelName: model.name,
|
|
573
|
+
fieldNames,
|
|
574
|
+
mapping,
|
|
575
|
+
sourceId,
|
|
576
|
+
diagnostics,
|
|
577
|
+
span: modelAttribute.span,
|
|
578
|
+
contextLabel: `Model "${model.name}" @@${modelAttribute.name}`,
|
|
579
|
+
});
|
|
580
|
+
if (!columnNames) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const constraintName = parseConstraintMapArgument({
|
|
584
|
+
attribute: modelAttribute,
|
|
585
|
+
sourceId,
|
|
586
|
+
diagnostics,
|
|
587
|
+
entityLabel: `Model "${model.name}" @@${modelAttribute.name}`,
|
|
588
|
+
span: modelAttribute.span,
|
|
589
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
590
|
+
});
|
|
591
|
+
if (modelAttribute.name === 'unique') {
|
|
592
|
+
uniqueConstraints.push({
|
|
593
|
+
columns: columnNames,
|
|
594
|
+
...ifDefined('name', constraintName),
|
|
595
|
+
});
|
|
596
|
+
} else {
|
|
597
|
+
indexNodes.push({
|
|
598
|
+
columns: columnNames,
|
|
599
|
+
...ifDefined('name', constraintName),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
978
602
|
continue;
|
|
979
603
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
604
|
+
const uncomposedNamespace = checkUncomposedNamespace(
|
|
605
|
+
modelAttribute.name,
|
|
606
|
+
input.composedExtensions,
|
|
607
|
+
{ familyId: input.familyId, targetId: input.targetId },
|
|
608
|
+
);
|
|
609
|
+
if (uncomposedNamespace) {
|
|
610
|
+
reportUncomposedNamespace({
|
|
611
|
+
subjectLabel: `Attribute "@@${modelAttribute.name}"`,
|
|
612
|
+
namespace: uncomposedNamespace,
|
|
613
|
+
sourceId,
|
|
614
|
+
span: modelAttribute.span,
|
|
615
|
+
diagnostics,
|
|
992
616
|
});
|
|
993
|
-
|
|
617
|
+
continue;
|
|
994
618
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
: undefined;
|
|
1001
|
-
if (namedRelationNameRaw && !namedRelationName) {
|
|
1002
|
-
input.diagnostics.push({
|
|
1003
|
-
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1004
|
-
message: `Relation field "${input.modelName}.${input.fieldName}" named relation name must be a quoted string literal`,
|
|
1005
|
-
sourceId: input.sourceId,
|
|
1006
|
-
span: input.attribute.span,
|
|
1007
|
-
});
|
|
1008
|
-
return undefined;
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
if (
|
|
1012
|
-
relationNameFromPositional &&
|
|
1013
|
-
namedRelationName &&
|
|
1014
|
-
relationNameFromPositional !== namedRelationName
|
|
1015
|
-
) {
|
|
1016
|
-
input.diagnostics.push({
|
|
1017
|
-
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1018
|
-
message: `Relation field "${input.modelName}.${input.fieldName}" has conflicting positional and named relation names`,
|
|
1019
|
-
sourceId: input.sourceId,
|
|
1020
|
-
span: input.attribute.span,
|
|
1021
|
-
});
|
|
1022
|
-
return undefined;
|
|
1023
|
-
}
|
|
1024
|
-
const relationName = namedRelationName ?? relationNameFromPositional;
|
|
1025
|
-
|
|
1026
|
-
const fieldsRaw = getNamedArgument(input.attribute, 'fields');
|
|
1027
|
-
const referencesRaw = getNamedArgument(input.attribute, 'references');
|
|
1028
|
-
if ((fieldsRaw && !referencesRaw) || (!fieldsRaw && referencesRaw)) {
|
|
1029
|
-
input.diagnostics.push({
|
|
1030
|
-
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1031
|
-
message: `Relation field "${input.modelName}.${input.fieldName}" requires fields and references arguments`,
|
|
1032
|
-
sourceId: input.sourceId,
|
|
1033
|
-
span: input.attribute.span,
|
|
619
|
+
diagnostics.push({
|
|
620
|
+
code: 'PSL_UNSUPPORTED_MODEL_ATTRIBUTE',
|
|
621
|
+
message: `Model "${model.name}" uses unsupported attribute "@@${modelAttribute.name}"`,
|
|
622
|
+
sourceId,
|
|
623
|
+
span: modelAttribute.span,
|
|
1034
624
|
});
|
|
1035
|
-
return undefined;
|
|
1036
625
|
}
|
|
1037
626
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
const parsedReferences = parseFieldList(referencesRaw);
|
|
1043
|
-
if (
|
|
1044
|
-
!parsedFields ||
|
|
1045
|
-
!parsedReferences ||
|
|
1046
|
-
parsedFields.length === 0 ||
|
|
1047
|
-
parsedReferences.length === 0
|
|
1048
|
-
) {
|
|
1049
|
-
input.diagnostics.push({
|
|
1050
|
-
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1051
|
-
message: `Relation field "${input.modelName}.${input.fieldName}" requires bracketed fields and references lists`,
|
|
1052
|
-
sourceId: input.sourceId,
|
|
1053
|
-
span: input.attribute.span,
|
|
1054
|
-
});
|
|
1055
|
-
return undefined;
|
|
627
|
+
const resultFkRelationMetadata: FkRelationMetadata[] = [];
|
|
628
|
+
for (const relationAttribute of relationAttributes) {
|
|
629
|
+
if (relationAttribute.field.list) {
|
|
630
|
+
continue;
|
|
1056
631
|
}
|
|
1057
|
-
fields = parsedFields;
|
|
1058
|
-
references = parsedReferences;
|
|
1059
|
-
}
|
|
1060
|
-
|
|
1061
|
-
const onDeleteArgument = getNamedArgument(input.attribute, 'onDelete');
|
|
1062
|
-
const onUpdateArgument = getNamedArgument(input.attribute, 'onUpdate');
|
|
1063
632
|
|
|
1064
|
-
|
|
1065
|
-
...ifDefined('relationName', relationName),
|
|
1066
|
-
...ifDefined('fields', fields),
|
|
1067
|
-
...ifDefined('references', references),
|
|
1068
|
-
...ifDefined('onDelete', onDeleteArgument ? unquoteStringLiteral(onDeleteArgument) : undefined),
|
|
1069
|
-
...ifDefined('onUpdate', onUpdateArgument ? unquoteStringLiteral(onUpdateArgument) : undefined),
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
export function interpretPslDocumentToSqlContractIR(
|
|
1074
|
-
input: InterpretPslDocumentToSqlContractIRInput,
|
|
1075
|
-
): Result<ContractIR, ContractSourceDiagnostics> {
|
|
1076
|
-
const diagnostics: ContractSourceDiagnostic[] = mapParserDiagnostics(input.document);
|
|
1077
|
-
const modelNames = new Set(input.document.ast.models.map((model) => model.name));
|
|
1078
|
-
const sourceId = input.document.ast.sourceId;
|
|
1079
|
-
const composedExtensions = new Set(input.composedExtensionPacks ?? []);
|
|
1080
|
-
const defaultFunctionRegistry =
|
|
1081
|
-
input.defaultFunctionRegistry ?? createBuiltinDefaultFunctionRegistry();
|
|
1082
|
-
|
|
1083
|
-
let builder = defineContract().target(
|
|
1084
|
-
input.target ?? DEFAULT_POSTGRES_TARGET,
|
|
1085
|
-
) as unknown as DynamicContractBuilder;
|
|
1086
|
-
const enumTypeDescriptors = new Map<string, ColumnDescriptor>();
|
|
1087
|
-
const namedTypeDescriptors = new Map<string, ColumnDescriptor>();
|
|
1088
|
-
const namedTypeBaseTypes = new Map<string, string>();
|
|
1089
|
-
|
|
1090
|
-
for (const enumDeclaration of input.document.ast.enums) {
|
|
1091
|
-
const nativeType = enumDeclaration.name.toLowerCase();
|
|
1092
|
-
const descriptor: ColumnDescriptor = {
|
|
1093
|
-
codecId: 'pg/enum@1',
|
|
1094
|
-
nativeType,
|
|
1095
|
-
typeRef: enumDeclaration.name,
|
|
1096
|
-
};
|
|
1097
|
-
enumTypeDescriptors.set(enumDeclaration.name, descriptor);
|
|
1098
|
-
builder = builder.storageType(enumDeclaration.name, {
|
|
1099
|
-
codecId: 'pg/enum@1',
|
|
1100
|
-
nativeType,
|
|
1101
|
-
typeParams: { values: enumDeclaration.values.map((value) => value.name) },
|
|
1102
|
-
});
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
for (const declaration of input.document.ast.types?.declarations ?? []) {
|
|
1106
|
-
const baseDescriptor =
|
|
1107
|
-
enumTypeDescriptors.get(declaration.baseType) ?? SCALAR_COLUMN_MAP[declaration.baseType];
|
|
1108
|
-
if (!baseDescriptor) {
|
|
633
|
+
if (!input.modelNames.has(relationAttribute.field.typeName)) {
|
|
1109
634
|
diagnostics.push({
|
|
1110
|
-
code: '
|
|
1111
|
-
message: `
|
|
635
|
+
code: 'PSL_INVALID_RELATION_TARGET',
|
|
636
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${relationAttribute.field.typeName}"`,
|
|
1112
637
|
sourceId,
|
|
1113
|
-
span:
|
|
638
|
+
span: relationAttribute.field.span,
|
|
1114
639
|
});
|
|
1115
640
|
continue;
|
|
1116
641
|
}
|
|
1117
|
-
namedTypeBaseTypes.set(declaration.name, declaration.baseType);
|
|
1118
642
|
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
643
|
+
const parsedRelation = parseRelationAttribute({
|
|
644
|
+
attribute: relationAttribute.relation,
|
|
645
|
+
modelName: model.name,
|
|
646
|
+
fieldName: relationAttribute.field.name,
|
|
647
|
+
sourceId,
|
|
648
|
+
diagnostics,
|
|
649
|
+
});
|
|
650
|
+
if (!parsedRelation) {
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
if (!parsedRelation.fields || !parsedRelation.references) {
|
|
1124
654
|
diagnostics.push({
|
|
1125
|
-
code: '
|
|
1126
|
-
message: `
|
|
655
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
656
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" requires fields and references arguments`,
|
|
1127
657
|
sourceId,
|
|
1128
|
-
span:
|
|
658
|
+
span: relationAttribute.relation.span,
|
|
1129
659
|
});
|
|
1130
660
|
continue;
|
|
1131
661
|
}
|
|
1132
662
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
'Attribute "@pgvector.column" uses unrecognized namespace "pgvector". Add extension pack "pgvector" to extensionPacks in prisma-next.config.ts.',
|
|
1139
|
-
sourceId,
|
|
1140
|
-
span: pgvectorAttribute.span,
|
|
1141
|
-
});
|
|
1142
|
-
continue;
|
|
1143
|
-
}
|
|
1144
|
-
if (declaration.baseType !== 'Bytes') {
|
|
1145
|
-
diagnostics.push({
|
|
1146
|
-
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
1147
|
-
message: `Named type "${declaration.name}" uses @pgvector.column on unsupported base type "${declaration.baseType}"`,
|
|
1148
|
-
sourceId,
|
|
1149
|
-
span: pgvectorAttribute.span,
|
|
1150
|
-
});
|
|
1151
|
-
continue;
|
|
1152
|
-
}
|
|
1153
|
-
const length = parsePgvectorLength({
|
|
1154
|
-
attribute: pgvectorAttribute,
|
|
1155
|
-
diagnostics,
|
|
663
|
+
const targetMapping = input.modelMappings.get(relationAttribute.field.typeName);
|
|
664
|
+
if (!targetMapping) {
|
|
665
|
+
diagnostics.push({
|
|
666
|
+
code: 'PSL_INVALID_RELATION_TARGET',
|
|
667
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${relationAttribute.field.typeName}"`,
|
|
1156
668
|
sourceId,
|
|
1157
|
-
|
|
1158
|
-
if (length === undefined) {
|
|
1159
|
-
continue;
|
|
1160
|
-
}
|
|
1161
|
-
namedTypeDescriptors.set(declaration.name, {
|
|
1162
|
-
codecId: 'pg/vector@1',
|
|
1163
|
-
nativeType: `vector(${length})`,
|
|
1164
|
-
typeRef: declaration.name,
|
|
1165
|
-
});
|
|
1166
|
-
builder = builder.storageType(declaration.name, {
|
|
1167
|
-
codecId: 'pg/vector@1',
|
|
1168
|
-
nativeType: `vector(${length})`,
|
|
1169
|
-
typeParams: { length },
|
|
669
|
+
span: relationAttribute.field.span,
|
|
1170
670
|
});
|
|
1171
671
|
continue;
|
|
1172
672
|
}
|
|
1173
673
|
|
|
1174
|
-
const
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
nativeType: baseDescriptor.nativeType,
|
|
1183
|
-
typeParams: {},
|
|
674
|
+
const localColumns = mapFieldNamesToColumns({
|
|
675
|
+
modelName: model.name,
|
|
676
|
+
fieldNames: parsedRelation.fields,
|
|
677
|
+
mapping,
|
|
678
|
+
sourceId,
|
|
679
|
+
diagnostics,
|
|
680
|
+
span: relationAttribute.relation.span,
|
|
681
|
+
contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
|
|
1184
682
|
});
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const modelMappings = buildModelMappings(input.document.ast.models, diagnostics, sourceId);
|
|
1188
|
-
const resolvedModels: Array<{
|
|
1189
|
-
model: PslModel;
|
|
1190
|
-
mapping: ModelNameMapping;
|
|
1191
|
-
resolvedFields: ResolvedField[];
|
|
1192
|
-
}> = [];
|
|
1193
|
-
const fkRelationMetadata: FkRelationMetadata[] = [];
|
|
1194
|
-
const backrelationCandidates: ModelBackrelationCandidate[] = [];
|
|
1195
|
-
|
|
1196
|
-
for (const model of input.document.ast.models) {
|
|
1197
|
-
const mapping = modelMappings.get(model.name);
|
|
1198
|
-
if (!mapping) {
|
|
683
|
+
if (!localColumns) {
|
|
1199
684
|
continue;
|
|
1200
685
|
}
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
mapping,
|
|
1205
|
-
enumTypeDescriptors,
|
|
1206
|
-
namedTypeDescriptors,
|
|
1207
|
-
namedTypeBaseTypes,
|
|
1208
|
-
modelNames,
|
|
1209
|
-
composedExtensions,
|
|
1210
|
-
defaultFunctionRegistry,
|
|
1211
|
-
diagnostics,
|
|
686
|
+
const referencedColumns = mapFieldNamesToColumns({
|
|
687
|
+
modelName: targetMapping.model.name,
|
|
688
|
+
fieldNames: parsedRelation.references,
|
|
689
|
+
mapping: targetMapping,
|
|
1212
690
|
sourceId,
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
691
|
+
diagnostics,
|
|
692
|
+
span: relationAttribute.relation.span,
|
|
693
|
+
contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
|
|
694
|
+
});
|
|
695
|
+
if (!referencedColumns) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (localColumns.length !== referencedColumns.length) {
|
|
1220
699
|
diagnostics.push({
|
|
1221
|
-
code: '
|
|
1222
|
-
message: `
|
|
700
|
+
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
701
|
+
message: `Relation field "${model.name}.${relationAttribute.field.name}" must provide the same number of fields and references`,
|
|
1223
702
|
sourceId,
|
|
1224
|
-
span:
|
|
703
|
+
span: relationAttribute.relation.span,
|
|
1225
704
|
});
|
|
705
|
+
continue;
|
|
1226
706
|
}
|
|
1227
707
|
|
|
1228
|
-
|
|
1229
|
-
|
|
708
|
+
const onDelete = parsedRelation.onDelete
|
|
709
|
+
? normalizeReferentialAction({
|
|
710
|
+
modelName: model.name,
|
|
711
|
+
fieldName: relationAttribute.field.name,
|
|
712
|
+
actionName: 'onDelete',
|
|
713
|
+
actionToken: parsedRelation.onDelete,
|
|
714
|
+
sourceId,
|
|
715
|
+
span: relationAttribute.field.span,
|
|
716
|
+
diagnostics,
|
|
717
|
+
})
|
|
718
|
+
: undefined;
|
|
719
|
+
const onUpdate = parsedRelation.onUpdate
|
|
720
|
+
? normalizeReferentialAction({
|
|
721
|
+
modelName: model.name,
|
|
722
|
+
fieldName: relationAttribute.field.name,
|
|
723
|
+
actionName: 'onUpdate',
|
|
724
|
+
actionToken: parsedRelation.onUpdate,
|
|
725
|
+
sourceId,
|
|
726
|
+
span: relationAttribute.field.span,
|
|
727
|
+
diagnostics,
|
|
728
|
+
})
|
|
729
|
+
: undefined;
|
|
730
|
+
|
|
731
|
+
foreignKeyNodes.push({
|
|
732
|
+
columns: localColumns,
|
|
733
|
+
references: {
|
|
734
|
+
model: targetMapping.model.name,
|
|
735
|
+
table: targetMapping.tableName,
|
|
736
|
+
columns: referencedColumns,
|
|
737
|
+
},
|
|
738
|
+
...ifDefined('name', parsedRelation.constraintName),
|
|
739
|
+
...ifDefined('onDelete', onDelete),
|
|
740
|
+
...ifDefined('onUpdate', onUpdate),
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
resultFkRelationMetadata.push({
|
|
744
|
+
declaringModelName: model.name,
|
|
745
|
+
declaringFieldName: relationAttribute.field.name,
|
|
746
|
+
declaringTableName: tableName,
|
|
747
|
+
targetModelName: targetMapping.model.name,
|
|
748
|
+
targetTableName: targetMapping.tableName,
|
|
749
|
+
...ifDefined('relationName', parsedRelation.relationName),
|
|
750
|
+
localColumns,
|
|
751
|
+
referencedColumns,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return {
|
|
756
|
+
modelNode: {
|
|
757
|
+
modelName: model.name,
|
|
758
|
+
tableName,
|
|
759
|
+
fields: resolvedFields.map((resolvedField) => ({
|
|
760
|
+
fieldName: resolvedField.field.name,
|
|
761
|
+
columnName: resolvedField.columnName,
|
|
762
|
+
descriptor: resolvedField.descriptor,
|
|
763
|
+
nullable: resolvedField.field.optional,
|
|
764
|
+
...ifDefined('default', resolvedField.defaultValue),
|
|
765
|
+
...ifDefined('executionDefault', resolvedField.executionDefault),
|
|
766
|
+
})),
|
|
767
|
+
...(primaryKeyColumns.length > 0
|
|
768
|
+
? {
|
|
769
|
+
id: {
|
|
770
|
+
columns: primaryKeyColumns,
|
|
771
|
+
...ifDefined('name', primaryKeyName),
|
|
772
|
+
},
|
|
773
|
+
}
|
|
774
|
+
: {}),
|
|
775
|
+
...(uniqueConstraints.length > 0 ? { uniques: uniqueConstraints } : {}),
|
|
776
|
+
...(indexNodes.length > 0 ? { indexes: indexNodes } : {}),
|
|
777
|
+
...(foreignKeyNodes.length > 0 ? { foreignKeys: foreignKeyNodes } : {}),
|
|
778
|
+
},
|
|
779
|
+
fkRelationMetadata: resultFkRelationMetadata,
|
|
780
|
+
backrelationCandidates: resultBackrelationCandidates,
|
|
781
|
+
resolvedFields,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
interface BuildValueObjectsInput {
|
|
786
|
+
readonly compositeTypes: readonly PslCompositeType[];
|
|
787
|
+
readonly enumTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
|
|
788
|
+
readonly namedTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
|
|
789
|
+
readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
|
|
790
|
+
readonly composedExtensions: ReadonlySet<string>;
|
|
791
|
+
readonly familyId: string;
|
|
792
|
+
readonly targetId: string;
|
|
793
|
+
readonly authoringContributions: AuthoringContributions | undefined;
|
|
794
|
+
readonly diagnostics: ContractSourceDiagnostic[];
|
|
795
|
+
readonly sourceId: string;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function buildValueObjects(input: BuildValueObjectsInput): Record<string, ContractValueObject> {
|
|
799
|
+
const {
|
|
800
|
+
compositeTypes,
|
|
801
|
+
enumTypeDescriptors,
|
|
802
|
+
namedTypeDescriptors,
|
|
803
|
+
scalarTypeDescriptors,
|
|
804
|
+
composedExtensions,
|
|
805
|
+
familyId,
|
|
806
|
+
targetId,
|
|
807
|
+
authoringContributions,
|
|
808
|
+
diagnostics,
|
|
809
|
+
sourceId,
|
|
810
|
+
} = input;
|
|
811
|
+
const valueObjects: Record<string, ContractValueObject> = {};
|
|
812
|
+
const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
|
|
813
|
+
|
|
814
|
+
for (const compositeType of compositeTypes) {
|
|
815
|
+
const fields: Record<string, ContractField> = {};
|
|
816
|
+
for (const field of compositeType.fields) {
|
|
817
|
+
if (compositeTypeNames.has(field.typeName)) {
|
|
818
|
+
const result: ContractField = {
|
|
819
|
+
type: { kind: 'valueObject', name: field.typeName },
|
|
820
|
+
nullable: field.optional,
|
|
821
|
+
};
|
|
822
|
+
fields[field.name] = field.list ? { ...result, many: true } : result;
|
|
1230
823
|
continue;
|
|
1231
824
|
}
|
|
1232
|
-
const
|
|
1233
|
-
modelName: model.name,
|
|
825
|
+
const resolved = resolveFieldTypeDescriptor({
|
|
1234
826
|
field,
|
|
1235
|
-
|
|
827
|
+
enumTypeDescriptors,
|
|
828
|
+
namedTypeDescriptors,
|
|
829
|
+
scalarTypeDescriptors,
|
|
830
|
+
authoringContributions,
|
|
1236
831
|
composedExtensions,
|
|
832
|
+
familyId,
|
|
833
|
+
targetId,
|
|
1237
834
|
diagnostics,
|
|
835
|
+
sourceId,
|
|
836
|
+
entityLabel: `Field "${compositeType.name}.${field.name}"`,
|
|
1238
837
|
});
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
if (relationAttribute) {
|
|
1242
|
-
const parsedRelation = parseRelationAttribute({
|
|
1243
|
-
attribute: relationAttribute,
|
|
1244
|
-
modelName: model.name,
|
|
1245
|
-
fieldName: field.name,
|
|
1246
|
-
sourceId,
|
|
1247
|
-
diagnostics,
|
|
1248
|
-
});
|
|
1249
|
-
if (!parsedRelation) {
|
|
1250
|
-
continue;
|
|
1251
|
-
}
|
|
1252
|
-
if (parsedRelation.fields || parsedRelation.references) {
|
|
838
|
+
if (!resolved.ok) {
|
|
839
|
+
if (!resolved.alreadyReported) {
|
|
1253
840
|
diagnostics.push({
|
|
1254
|
-
code: '
|
|
1255
|
-
message: `
|
|
841
|
+
code: 'PSL_UNSUPPORTED_FIELD_TYPE',
|
|
842
|
+
message: `Field "${compositeType.name}.${field.name}" type "${field.typeName}" is not supported`,
|
|
1256
843
|
sourceId,
|
|
1257
|
-
span:
|
|
844
|
+
span: field.span,
|
|
1258
845
|
});
|
|
1259
|
-
continue;
|
|
1260
846
|
}
|
|
1261
|
-
if (parsedRelation.onDelete || parsedRelation.onUpdate) {
|
|
1262
|
-
diagnostics.push({
|
|
1263
|
-
code: 'PSL_INVALID_RELATION_ATTRIBUTE',
|
|
1264
|
-
message: `Backrelation list field "${model.name}.${field.name}" cannot declare onDelete/onUpdate; define referential actions on the FK-side relation field`,
|
|
1265
|
-
sourceId,
|
|
1266
|
-
span: relationAttribute.span,
|
|
1267
|
-
});
|
|
1268
|
-
continue;
|
|
1269
|
-
}
|
|
1270
|
-
relationName = parsedRelation.relationName;
|
|
1271
|
-
}
|
|
1272
|
-
if (!attributesValid) {
|
|
1273
847
|
continue;
|
|
1274
848
|
}
|
|
849
|
+
const scalarField: ContractField = {
|
|
850
|
+
nullable: field.optional,
|
|
851
|
+
type: { kind: 'scalar', codecId: resolved.descriptor.codecId },
|
|
852
|
+
};
|
|
853
|
+
fields[field.name] = field.list ? { ...scalarField, many: true } : scalarField;
|
|
854
|
+
}
|
|
855
|
+
valueObjects[compositeType.name] = { fields };
|
|
856
|
+
}
|
|
1275
857
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
858
|
+
return valueObjects;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function patchModelDomainFields(
|
|
862
|
+
models: Record<string, ContractModel>,
|
|
863
|
+
modelResolvedFields: ReadonlyMap<string, readonly ResolvedField[]>,
|
|
864
|
+
): Record<string, ContractModel> {
|
|
865
|
+
let patched = models;
|
|
866
|
+
|
|
867
|
+
for (const [modelName, resolvedFields] of modelResolvedFields) {
|
|
868
|
+
const model = patched[modelName];
|
|
869
|
+
if (!model) continue;
|
|
870
|
+
|
|
871
|
+
let needsPatch = false;
|
|
872
|
+
const patchedFields: Record<string, ContractField> = { ...model.fields };
|
|
873
|
+
|
|
874
|
+
for (const rf of resolvedFields) {
|
|
875
|
+
if (rf.valueObjectTypeName) {
|
|
876
|
+
needsPatch = true;
|
|
877
|
+
patchedFields[rf.field.name] = {
|
|
878
|
+
nullable: rf.field.optional,
|
|
879
|
+
type: { kind: 'valueObject', name: rf.valueObjectTypeName },
|
|
880
|
+
...(rf.many ? { many: true as const } : {}),
|
|
881
|
+
};
|
|
882
|
+
} else if (rf.many && rf.scalarCodecId) {
|
|
883
|
+
needsPatch = true;
|
|
884
|
+
patchedFields[rf.field.name] = {
|
|
885
|
+
nullable: rf.field.optional,
|
|
886
|
+
type: { kind: 'scalar', codecId: rf.scalarCodecId },
|
|
887
|
+
many: true as const,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
1283
890
|
}
|
|
1284
891
|
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
}))
|
|
1290
|
-
.filter((entry): entry is { field: PslField; relation: PslAttribute } =>
|
|
1291
|
-
Boolean(entry.relation),
|
|
1292
|
-
);
|
|
892
|
+
if (needsPatch) {
|
|
893
|
+
patched = { ...patched, [modelName]: { ...model, fields: patchedFields } };
|
|
894
|
+
}
|
|
895
|
+
}
|
|
1293
896
|
|
|
1294
|
-
|
|
1295
|
-
|
|
897
|
+
return patched;
|
|
898
|
+
}
|
|
1296
899
|
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
generated: resolvedField.executionDefault,
|
|
1302
|
-
});
|
|
1303
|
-
} else {
|
|
1304
|
-
const options: {
|
|
1305
|
-
type: ColumnDescriptor;
|
|
1306
|
-
nullable?: true;
|
|
1307
|
-
default?: ColumnDefault;
|
|
1308
|
-
} = {
|
|
1309
|
-
type: resolvedField.descriptor,
|
|
1310
|
-
...ifDefined('nullable', resolvedField.field.optional ? (true as const) : undefined),
|
|
1311
|
-
...ifDefined('default', resolvedField.defaultValue),
|
|
1312
|
-
};
|
|
1313
|
-
table = table.column(resolvedField.columnName, options);
|
|
1314
|
-
}
|
|
900
|
+
type DiscriminatorDeclaration = {
|
|
901
|
+
readonly fieldName: string;
|
|
902
|
+
readonly span: ContractSourceDiagnosticSpan;
|
|
903
|
+
};
|
|
1315
904
|
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
905
|
+
type BaseDeclaration = {
|
|
906
|
+
readonly baseName: string;
|
|
907
|
+
readonly value: string;
|
|
908
|
+
readonly span: ContractSourceDiagnosticSpan;
|
|
909
|
+
};
|
|
1320
910
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
911
|
+
function collectPolymorphismDeclarations(
|
|
912
|
+
models: readonly PslModel[],
|
|
913
|
+
sourceId: string,
|
|
914
|
+
diagnostics: ContractSourceDiagnostic[],
|
|
915
|
+
): {
|
|
916
|
+
discriminatorDeclarations: Map<string, DiscriminatorDeclaration>;
|
|
917
|
+
baseDeclarations: Map<string, BaseDeclaration>;
|
|
918
|
+
} {
|
|
919
|
+
const discriminatorDeclarations = new Map<string, DiscriminatorDeclaration>();
|
|
920
|
+
const baseDeclarations = new Map<string, BaseDeclaration>();
|
|
1324
921
|
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
if (
|
|
1330
|
-
|
|
1331
|
-
attribute: modelAttribute,
|
|
1332
|
-
sourceId,
|
|
1333
|
-
diagnostics,
|
|
922
|
+
for (const model of models) {
|
|
923
|
+
for (const attr of model.attributes) {
|
|
924
|
+
if (attr.name === 'discriminator') {
|
|
925
|
+
const fieldName = getPositionalArgument(attr);
|
|
926
|
+
if (!fieldName) {
|
|
927
|
+
diagnostics.push({
|
|
1334
928
|
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
1335
|
-
|
|
1336
|
-
});
|
|
1337
|
-
if (!fieldNames) {
|
|
1338
|
-
continue;
|
|
1339
|
-
}
|
|
1340
|
-
const columnNames = mapFieldNamesToColumns({
|
|
1341
|
-
modelName: model.name,
|
|
1342
|
-
fieldNames,
|
|
1343
|
-
mapping,
|
|
929
|
+
message: `Model "${model.name}" @@discriminator requires a field name argument`,
|
|
1344
930
|
sourceId,
|
|
1345
|
-
|
|
1346
|
-
span: modelAttribute.span,
|
|
1347
|
-
contextLabel: `Model "${model.name}" @@${modelAttribute.name}`,
|
|
931
|
+
span: attr.span,
|
|
1348
932
|
});
|
|
1349
|
-
if (!columnNames) {
|
|
1350
|
-
continue;
|
|
1351
|
-
}
|
|
1352
|
-
if (modelAttribute.name === 'unique') {
|
|
1353
|
-
table = table.unique(columnNames);
|
|
1354
|
-
} else {
|
|
1355
|
-
table = table.index(columnNames);
|
|
1356
|
-
}
|
|
1357
933
|
continue;
|
|
1358
934
|
}
|
|
1359
|
-
|
|
935
|
+
const discField = model.fields.find((f) => f.name === fieldName);
|
|
936
|
+
if (discField && discField.typeName !== 'String') {
|
|
1360
937
|
diagnostics.push({
|
|
1361
|
-
code: '
|
|
1362
|
-
message: `
|
|
938
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
939
|
+
message: `Discriminator field "${fieldName}" on model "${model.name}" must be of type String, but is "${discField.typeName}"`,
|
|
1363
940
|
sourceId,
|
|
1364
|
-
span:
|
|
941
|
+
span: attr.span,
|
|
1365
942
|
});
|
|
1366
943
|
continue;
|
|
1367
944
|
}
|
|
1368
|
-
|
|
1369
|
-
code: 'PSL_UNSUPPORTED_MODEL_ATTRIBUTE',
|
|
1370
|
-
message: `Model "${model.name}" uses unsupported attribute "@@${modelAttribute.name}"`,
|
|
1371
|
-
sourceId,
|
|
1372
|
-
span: modelAttribute.span,
|
|
1373
|
-
});
|
|
945
|
+
discriminatorDeclarations.set(model.name, { fieldName, span: attr.span });
|
|
1374
946
|
}
|
|
1375
947
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
if (!modelNames.has(relationAttribute.field.typeName)) {
|
|
948
|
+
if (attr.name === 'base') {
|
|
949
|
+
const baseName = getPositionalArgument(attr, 0);
|
|
950
|
+
const rawValue = getPositionalArgument(attr, 1);
|
|
951
|
+
if (!baseName || !rawValue) {
|
|
1382
952
|
diagnostics.push({
|
|
1383
|
-
code: '
|
|
1384
|
-
message: `
|
|
953
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
954
|
+
message: `Model "${model.name}" @@base requires two arguments: base model name and discriminator value`,
|
|
1385
955
|
sourceId,
|
|
1386
|
-
span:
|
|
956
|
+
span: attr.span,
|
|
1387
957
|
});
|
|
1388
958
|
continue;
|
|
1389
959
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
attribute: relationAttribute.relation,
|
|
1393
|
-
modelName: model.name,
|
|
1394
|
-
fieldName: relationAttribute.field.name,
|
|
1395
|
-
sourceId,
|
|
1396
|
-
diagnostics,
|
|
1397
|
-
});
|
|
1398
|
-
if (!parsedRelation) {
|
|
1399
|
-
continue;
|
|
1400
|
-
}
|
|
1401
|
-
if (!parsedRelation.fields || !parsedRelation.references) {
|
|
960
|
+
const value = parseQuotedStringLiteral(rawValue);
|
|
961
|
+
if (value === undefined) {
|
|
1402
962
|
diagnostics.push({
|
|
1403
|
-
code: '
|
|
1404
|
-
message: `
|
|
963
|
+
code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
|
|
964
|
+
message: `Model "${model.name}" @@base discriminator value must be a quoted string literal`,
|
|
1405
965
|
sourceId,
|
|
1406
|
-
span:
|
|
966
|
+
span: attr.span,
|
|
1407
967
|
});
|
|
1408
968
|
continue;
|
|
1409
969
|
}
|
|
970
|
+
baseDeclarations.set(model.name, { baseName, value, span: attr.span });
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
1410
974
|
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
diagnostics.push({
|
|
1414
|
-
code: 'PSL_INVALID_RELATION_TARGET',
|
|
1415
|
-
message: `Relation field "${model.name}.${relationAttribute.field.name}" references unknown model "${relationAttribute.field.typeName}"`,
|
|
1416
|
-
sourceId,
|
|
1417
|
-
span: relationAttribute.field.span,
|
|
1418
|
-
});
|
|
1419
|
-
continue;
|
|
1420
|
-
}
|
|
975
|
+
return { discriminatorDeclarations, baseDeclarations };
|
|
976
|
+
}
|
|
1421
977
|
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
978
|
+
function resolvePolymorphism(
|
|
979
|
+
models: Record<string, ContractModel>,
|
|
980
|
+
discriminatorDeclarations: Map<string, DiscriminatorDeclaration>,
|
|
981
|
+
baseDeclarations: Map<string, BaseDeclaration>,
|
|
982
|
+
modelNames: Set<string>,
|
|
983
|
+
modelMappings: ReadonlyMap<string, ModelNameMapping>,
|
|
984
|
+
sourceId: string,
|
|
985
|
+
diagnostics: ContractSourceDiagnostic[],
|
|
986
|
+
): Record<string, ContractModel> {
|
|
987
|
+
let patched = models;
|
|
988
|
+
|
|
989
|
+
for (const [modelName, decl] of discriminatorDeclarations) {
|
|
990
|
+
if (baseDeclarations.has(modelName)) {
|
|
991
|
+
diagnostics.push({
|
|
992
|
+
code: 'PSL_DISCRIMINATOR_AND_BASE',
|
|
993
|
+
message: `Model "${modelName}" cannot have both @@discriminator and @@base`,
|
|
994
|
+
sourceId,
|
|
995
|
+
span: decl.span,
|
|
996
|
+
});
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const model = patched[modelName];
|
|
1001
|
+
if (!model) continue;
|
|
1002
|
+
|
|
1003
|
+
if (!Object.hasOwn(model.fields, decl.fieldName)) {
|
|
1004
|
+
diagnostics.push({
|
|
1005
|
+
code: 'PSL_DISCRIMINATOR_FIELD_NOT_FOUND',
|
|
1006
|
+
message: `Discriminator field "${decl.fieldName}" is not a field on model "${modelName}"`,
|
|
1007
|
+
sourceId,
|
|
1008
|
+
span: decl.span,
|
|
1009
|
+
});
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const variants: Record<string, { readonly value: string }> = {};
|
|
1014
|
+
const seenValues = new Map<string, string>();
|
|
1015
|
+
|
|
1016
|
+
for (const [variantName, baseDecl] of baseDeclarations) {
|
|
1017
|
+
if (baseDecl.baseName !== modelName) continue;
|
|
1018
|
+
|
|
1019
|
+
const existingVariant = seenValues.get(baseDecl.value);
|
|
1020
|
+
if (existingVariant) {
|
|
1021
|
+
diagnostics.push({
|
|
1022
|
+
code: 'PSL_DUPLICATE_DISCRIMINATOR_VALUE',
|
|
1023
|
+
message: `Discriminator value "${baseDecl.value}" is used by both "${existingVariant}" and "${variantName}" on base model "${modelName}"`,
|
|
1426
1024
|
sourceId,
|
|
1427
|
-
|
|
1428
|
-
span: relationAttribute.relation.span,
|
|
1429
|
-
contextLabel: `Relation field "${model.name}.${relationAttribute.field.name}"`,
|
|
1025
|
+
span: baseDecl.span,
|
|
1430
1026
|
});
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
seenValues.set(baseDecl.value, variantName);
|
|
1030
|
+
variants[variantName] = { value: baseDecl.value };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
if (Object.keys(variants).length === 0) {
|
|
1034
|
+
diagnostics.push({
|
|
1035
|
+
code: 'PSL_ORPHANED_DISCRIMINATOR',
|
|
1036
|
+
message: `Model "${modelName}" has @@discriminator but no variant models declare @@base(${modelName}, ...)`,
|
|
1037
|
+
sourceId,
|
|
1038
|
+
span: decl.span,
|
|
1039
|
+
});
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
patched = {
|
|
1044
|
+
...patched,
|
|
1045
|
+
[modelName]: { ...model, discriminator: { field: decl.fieldName }, variants },
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
for (const [variantName, baseDecl] of baseDeclarations) {
|
|
1050
|
+
if (!modelNames.has(baseDecl.baseName)) {
|
|
1051
|
+
diagnostics.push({
|
|
1052
|
+
code: 'PSL_BASE_TARGET_NOT_FOUND',
|
|
1053
|
+
message: `Model "${variantName}" @@base references non-existent model "${baseDecl.baseName}"`,
|
|
1054
|
+
sourceId,
|
|
1055
|
+
span: baseDecl.span,
|
|
1056
|
+
});
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
if (!discriminatorDeclarations.has(baseDecl.baseName)) {
|
|
1061
|
+
diagnostics.push({
|
|
1062
|
+
code: 'PSL_ORPHANED_BASE',
|
|
1063
|
+
message: `Model "${variantName}" declares @@base(${baseDecl.baseName}, ...) but "${baseDecl.baseName}" has no @@discriminator`,
|
|
1064
|
+
sourceId,
|
|
1065
|
+
span: baseDecl.span,
|
|
1066
|
+
});
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (discriminatorDeclarations.has(variantName)) {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const variantModel = patched[variantName];
|
|
1075
|
+
if (!variantModel) continue;
|
|
1076
|
+
|
|
1077
|
+
const baseMapping = modelMappings.get(baseDecl.baseName);
|
|
1078
|
+
const variantMapping = modelMappings.get(variantName);
|
|
1079
|
+
const hasExplicitMap =
|
|
1080
|
+
variantMapping?.model.attributes.some((attr) => attr.name === 'map') ?? false;
|
|
1081
|
+
const resolvedTable = hasExplicitMap ? variantMapping?.tableName : baseMapping?.tableName;
|
|
1082
|
+
|
|
1083
|
+
patched = {
|
|
1084
|
+
...patched,
|
|
1085
|
+
[variantName]: {
|
|
1086
|
+
...variantModel,
|
|
1087
|
+
base: baseDecl.baseName,
|
|
1088
|
+
...(resolvedTable ? { storage: { ...variantModel.storage, table: resolvedTable } } : {}),
|
|
1089
|
+
},
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
return patched;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
export function interpretPslDocumentToSqlContract(
|
|
1097
|
+
input: InterpretPslDocumentToSqlContractInput,
|
|
1098
|
+
): Result<Contract, ContractSourceDiagnostics> {
|
|
1099
|
+
const sourceId = input.document.ast.sourceId;
|
|
1100
|
+
if (!input.target) {
|
|
1101
|
+
return notOk({
|
|
1102
|
+
summary: 'PSL to SQL contract interpretation failed',
|
|
1103
|
+
diagnostics: [
|
|
1104
|
+
{
|
|
1105
|
+
code: 'PSL_TARGET_CONTEXT_REQUIRED',
|
|
1106
|
+
message: 'PSL interpretation requires an explicit target context from composition.',
|
|
1438
1107
|
sourceId,
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1108
|
+
},
|
|
1109
|
+
],
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (!input.scalarTypeDescriptors) {
|
|
1113
|
+
return notOk({
|
|
1114
|
+
summary: 'PSL to SQL contract interpretation failed',
|
|
1115
|
+
diagnostics: [
|
|
1116
|
+
{
|
|
1117
|
+
code: 'PSL_SCALAR_TYPE_CONTEXT_REQUIRED',
|
|
1118
|
+
message: 'PSL interpretation requires composed scalar type descriptors.',
|
|
1119
|
+
sourceId,
|
|
1120
|
+
},
|
|
1121
|
+
],
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1455
1124
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
fieldName: relationAttribute.field.name,
|
|
1471
|
-
actionName: 'onUpdate',
|
|
1472
|
-
actionToken: parsedRelation.onUpdate,
|
|
1473
|
-
sourceId,
|
|
1474
|
-
span: relationAttribute.field.span,
|
|
1475
|
-
diagnostics,
|
|
1476
|
-
})
|
|
1477
|
-
: undefined;
|
|
1478
|
-
|
|
1479
|
-
table = table.foreignKey(
|
|
1480
|
-
localColumns,
|
|
1481
|
-
{
|
|
1482
|
-
table: targetMapping.tableName,
|
|
1483
|
-
columns: referencedColumns,
|
|
1484
|
-
},
|
|
1485
|
-
{
|
|
1486
|
-
...ifDefined('onDelete', onDelete),
|
|
1487
|
-
...ifDefined('onUpdate', onUpdate),
|
|
1488
|
-
},
|
|
1489
|
-
);
|
|
1490
|
-
|
|
1491
|
-
fkRelationMetadata.push({
|
|
1492
|
-
declaringModelName: model.name,
|
|
1493
|
-
declaringFieldName: relationAttribute.field.name,
|
|
1494
|
-
declaringTableName: tableName,
|
|
1495
|
-
targetModelName: targetMapping.model.name,
|
|
1496
|
-
targetTableName: targetMapping.tableName,
|
|
1497
|
-
...ifDefined('relationName', parsedRelation.relationName),
|
|
1498
|
-
localColumns,
|
|
1499
|
-
referencedColumns,
|
|
1500
|
-
});
|
|
1501
|
-
}
|
|
1125
|
+
const diagnostics: ContractSourceDiagnostic[] = mapParserDiagnostics(input.document);
|
|
1126
|
+
const models = input.document.ast.models ?? [];
|
|
1127
|
+
const enums = input.document.ast.enums ?? [];
|
|
1128
|
+
const compositeTypes = input.document.ast.compositeTypes ?? [];
|
|
1129
|
+
const modelNames = new Set(models.map((model) => model.name));
|
|
1130
|
+
const compositeTypeNames = new Set(compositeTypes.map((ct) => ct.name));
|
|
1131
|
+
const composedExtensions = new Set(input.composedExtensionPacks ?? []);
|
|
1132
|
+
const defaultFunctionRegistry: ControlMutationDefaultRegistry =
|
|
1133
|
+
input.controlMutationDefaults?.defaultFunctionRegistry ?? new Map();
|
|
1134
|
+
const generatorDescriptors = input.controlMutationDefaults?.generatorDescriptors ?? [];
|
|
1135
|
+
const generatorDescriptorById = new Map<string, MutationDefaultGeneratorDescriptor>();
|
|
1136
|
+
for (const descriptor of generatorDescriptors) {
|
|
1137
|
+
generatorDescriptorById.set(descriptor.id, descriptor);
|
|
1138
|
+
}
|
|
1502
1139
|
|
|
1503
|
-
|
|
1140
|
+
const enumResult = processEnumDeclarations({
|
|
1141
|
+
enums,
|
|
1142
|
+
sourceId,
|
|
1143
|
+
enumTypeConstructor: getAuthoringTypeConstructor(input.authoringContributions, ['enum']),
|
|
1144
|
+
diagnostics,
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
const namedTypeResult = resolveNamedTypeDeclarations({
|
|
1148
|
+
declarations: input.document.ast.types?.declarations ?? [],
|
|
1149
|
+
sourceId,
|
|
1150
|
+
enumTypeDescriptors: enumResult.enumTypeDescriptors,
|
|
1151
|
+
scalarTypeDescriptors: input.scalarTypeDescriptors,
|
|
1152
|
+
composedExtensions,
|
|
1153
|
+
familyId: input.target.familyId,
|
|
1154
|
+
targetId: input.target.targetId,
|
|
1155
|
+
authoringContributions: input.authoringContributions,
|
|
1156
|
+
diagnostics,
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
const storageTypes = { ...enumResult.storageTypes, ...namedTypeResult.storageTypes };
|
|
1160
|
+
|
|
1161
|
+
const modelMappings = buildModelMappings(models, diagnostics, sourceId);
|
|
1162
|
+
const modelNodes: ModelNode[] = [];
|
|
1163
|
+
const fkRelationMetadata: FkRelationMetadata[] = [];
|
|
1164
|
+
const backrelationCandidates: ModelBackrelationCandidate[] = [];
|
|
1165
|
+
const modelResolvedFields = new Map<string, readonly ResolvedField[]>();
|
|
1166
|
+
|
|
1167
|
+
for (const model of models) {
|
|
1168
|
+
const mapping = modelMappings.get(model.name);
|
|
1169
|
+
if (!mapping) {
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
const result = buildModelNodeFromPsl({
|
|
1173
|
+
model,
|
|
1174
|
+
mapping,
|
|
1175
|
+
modelMappings,
|
|
1176
|
+
modelNames,
|
|
1177
|
+
compositeTypeNames,
|
|
1178
|
+
enumTypeDescriptors: enumResult.enumTypeDescriptors,
|
|
1179
|
+
namedTypeDescriptors: namedTypeResult.namedTypeDescriptors,
|
|
1180
|
+
composedExtensions,
|
|
1181
|
+
familyId: input.target.familyId,
|
|
1182
|
+
targetId: input.target.targetId,
|
|
1183
|
+
authoringContributions: input.authoringContributions,
|
|
1184
|
+
defaultFunctionRegistry,
|
|
1185
|
+
generatorDescriptorById,
|
|
1186
|
+
scalarTypeDescriptors: input.scalarTypeDescriptors,
|
|
1187
|
+
sourceId,
|
|
1188
|
+
diagnostics,
|
|
1504
1189
|
});
|
|
1190
|
+
modelNodes.push(result.modelNode);
|
|
1191
|
+
fkRelationMetadata.push(...result.fkRelationMetadata);
|
|
1192
|
+
backrelationCandidates.push(...result.backrelationCandidates);
|
|
1193
|
+
modelResolvedFields.set(model.name, result.resolvedFields);
|
|
1505
1194
|
}
|
|
1506
1195
|
|
|
1507
1196
|
const { modelRelations, fkRelationsByPair } = indexFkRelations({ fkRelationMetadata });
|
|
@@ -1512,31 +1201,90 @@ export function interpretPslDocumentToSqlContractIR(
|
|
|
1512
1201
|
diagnostics,
|
|
1513
1202
|
sourceId,
|
|
1514
1203
|
});
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1204
|
+
|
|
1205
|
+
const { discriminatorDeclarations, baseDeclarations } = collectPolymorphismDeclarations(
|
|
1206
|
+
models,
|
|
1207
|
+
sourceId,
|
|
1208
|
+
diagnostics,
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
const valueObjects = buildValueObjects({
|
|
1212
|
+
compositeTypes,
|
|
1213
|
+
enumTypeDescriptors: enumResult.enumTypeDescriptors,
|
|
1214
|
+
namedTypeDescriptors: namedTypeResult.namedTypeDescriptors,
|
|
1215
|
+
scalarTypeDescriptors: input.scalarTypeDescriptors,
|
|
1216
|
+
composedExtensions,
|
|
1217
|
+
familyId: input.target.familyId,
|
|
1218
|
+
targetId: input.target.targetId,
|
|
1219
|
+
authoringContributions: input.authoringContributions,
|
|
1220
|
+
diagnostics,
|
|
1221
|
+
sourceId,
|
|
1519
1222
|
});
|
|
1520
1223
|
|
|
1521
1224
|
if (diagnostics.length > 0) {
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1225
|
+
return notOk({
|
|
1226
|
+
summary: 'PSL to SQL contract interpretation failed',
|
|
1227
|
+
diagnostics: dedupeDiagnostics(diagnostics),
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const contract = buildSqlContractFromDefinition({
|
|
1232
|
+
target: input.target,
|
|
1233
|
+
...ifDefined(
|
|
1234
|
+
'extensionPacks',
|
|
1235
|
+
buildComposedExtensionPackRefs(
|
|
1236
|
+
input.target,
|
|
1237
|
+
[...composedExtensions].sort(compareStrings),
|
|
1238
|
+
input.composedExtensionPackRefs,
|
|
1239
|
+
),
|
|
1240
|
+
),
|
|
1241
|
+
...(Object.keys(storageTypes).length > 0 ? { storageTypes } : {}),
|
|
1242
|
+
models: modelNodes.map((model) => ({
|
|
1243
|
+
...model,
|
|
1244
|
+
...(modelRelations.has(model.modelName)
|
|
1245
|
+
? {
|
|
1246
|
+
relations: [...(modelRelations.get(model.modelName) ?? [])].sort((left, right) =>
|
|
1247
|
+
compareStrings(left.fieldName, right.fieldName),
|
|
1248
|
+
),
|
|
1249
|
+
}
|
|
1250
|
+
: {}),
|
|
1251
|
+
})),
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
let patchedModels = patchModelDomainFields(
|
|
1255
|
+
contract.models as Record<string, ContractModel>,
|
|
1256
|
+
modelResolvedFields,
|
|
1257
|
+
);
|
|
1533
1258
|
|
|
1259
|
+
const polyDiagnostics: ContractSourceDiagnostic[] = [];
|
|
1260
|
+
patchedModels = resolvePolymorphism(
|
|
1261
|
+
patchedModels,
|
|
1262
|
+
discriminatorDeclarations,
|
|
1263
|
+
baseDeclarations,
|
|
1264
|
+
modelNames,
|
|
1265
|
+
modelMappings,
|
|
1266
|
+
sourceId,
|
|
1267
|
+
polyDiagnostics,
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
if (polyDiagnostics.length > 0) {
|
|
1534
1271
|
return notOk({
|
|
1535
|
-
summary: 'PSL to SQL
|
|
1536
|
-
diagnostics:
|
|
1272
|
+
summary: 'PSL to SQL contract interpretation failed',
|
|
1273
|
+
diagnostics: polyDiagnostics,
|
|
1537
1274
|
});
|
|
1538
1275
|
}
|
|
1539
1276
|
|
|
1540
|
-
const
|
|
1541
|
-
|
|
1277
|
+
const variantModelNames = new Set(baseDeclarations.keys());
|
|
1278
|
+
const filteredRoots = Object.fromEntries(
|
|
1279
|
+
Object.entries(contract.roots).filter(([, modelName]) => !variantModelNames.has(modelName)),
|
|
1280
|
+
);
|
|
1281
|
+
|
|
1282
|
+
const patchedContract: Contract = {
|
|
1283
|
+
...contract,
|
|
1284
|
+
roots: filteredRoots,
|
|
1285
|
+
models: patchedModels,
|
|
1286
|
+
...(Object.keys(valueObjects).length > 0 ? { valueObjects } : {}),
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
return ok(patchedContract);
|
|
1542
1290
|
}
|