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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,404 @@
1
+ import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
2
+ import type { ColumnDefault, ExecutionMutationDefaultValue } from '@prisma-next/contract/types';
3
+ import type {
4
+ AuthoringContributions,
5
+ AuthoringTypeConstructorDescriptor,
6
+ } from '@prisma-next/framework-components/authoring';
7
+ import { isAuthoringTypeConstructorDescriptor } from '@prisma-next/framework-components/authoring';
8
+ import type { PslAttribute, PslField } from '@prisma-next/psl-parser';
9
+ import type {
10
+ ControlMutationDefaultRegistry,
11
+ MutationDefaultGeneratorDescriptor,
12
+ } from './default-function-registry';
13
+ import {
14
+ lowerDefaultFunctionWithRegistry,
15
+ parseDefaultFunctionCall,
16
+ } from './default-function-registry';
17
+ import {
18
+ getNamedArgument,
19
+ getPositionalArgument,
20
+ getPositionalArgumentEntry,
21
+ getPositionalArguments,
22
+ parseOptionalNumericArguments,
23
+ parseOptionalSingleIntegerArgument,
24
+ pushInvalidAttributeArgument,
25
+ unquoteStringLiteral,
26
+ } from './psl-attribute-parsing';
27
+
28
+ export type ColumnDescriptor = {
29
+ readonly codecId: string;
30
+ readonly nativeType: string;
31
+ readonly typeRef?: string;
32
+ readonly typeParams?: Record<string, unknown>;
33
+ };
34
+
35
+ export function toNamedTypeFieldDescriptor(
36
+ typeRef: string,
37
+ descriptor: Pick<ColumnDescriptor, 'codecId' | 'nativeType'>,
38
+ ): ColumnDescriptor {
39
+ return {
40
+ codecId: descriptor.codecId,
41
+ nativeType: descriptor.nativeType,
42
+ typeRef,
43
+ };
44
+ }
45
+
46
+ export function getAuthoringTypeConstructor(
47
+ contributions: AuthoringContributions | undefined,
48
+ path: readonly string[],
49
+ ): AuthoringTypeConstructorDescriptor | undefined {
50
+ let current: unknown = contributions?.type;
51
+
52
+ for (const segment of path) {
53
+ if (typeof current !== 'object' || current === null || Array.isArray(current)) {
54
+ return undefined;
55
+ }
56
+ current = (current as Record<string, unknown>)[segment];
57
+ }
58
+
59
+ return isAuthoringTypeConstructorDescriptor(current) ? current : undefined;
60
+ }
61
+
62
+ /**
63
+ * Declarative specification for @db.* native type attributes.
64
+ *
65
+ * Argument kinds:
66
+ * - `noArgs`: No arguments accepted; `codecId: null` means inherit from baseDescriptor.
67
+ * - `optionalLength`: Zero or one positional integer (minimum 1), stored as `{ length }`.
68
+ * - `optionalPrecision`: Zero or one positional integer (minimum 0), stored as `{ precision }`.
69
+ * - `optionalNumeric`: Zero, one, or two positional integers (precision + scale).
70
+ */
71
+ export type NativeTypeSpec =
72
+ | {
73
+ readonly args: 'noArgs';
74
+ readonly baseType: string;
75
+ readonly codecId: string | null;
76
+ readonly nativeType: string;
77
+ }
78
+ | {
79
+ readonly args: 'optionalLength';
80
+ readonly baseType: string;
81
+ readonly codecId: string;
82
+ readonly nativeType: string;
83
+ }
84
+ | {
85
+ readonly args: 'optionalPrecision';
86
+ readonly baseType: string;
87
+ readonly codecId: string;
88
+ readonly nativeType: string;
89
+ }
90
+ | {
91
+ readonly args: 'optionalNumeric';
92
+ readonly baseType: string;
93
+ readonly codecId: string;
94
+ readonly nativeType: string;
95
+ };
96
+
97
+ export const NATIVE_TYPE_SPECS: Readonly<Record<string, NativeTypeSpec>> = {
98
+ 'db.VarChar': {
99
+ args: 'optionalLength',
100
+ baseType: 'String',
101
+ codecId: 'sql/varchar@1',
102
+ nativeType: 'character varying',
103
+ },
104
+ 'db.Char': {
105
+ args: 'optionalLength',
106
+ baseType: 'String',
107
+ codecId: 'sql/char@1',
108
+ nativeType: 'character',
109
+ },
110
+ 'db.Uuid': { args: 'noArgs', baseType: 'String', codecId: null, nativeType: 'uuid' },
111
+ 'db.SmallInt': { args: 'noArgs', baseType: 'Int', codecId: 'pg/int2@1', nativeType: 'int2' },
112
+ 'db.Real': { args: 'noArgs', baseType: 'Float', codecId: 'pg/float4@1', nativeType: 'float4' },
113
+ 'db.Numeric': {
114
+ args: 'optionalNumeric',
115
+ baseType: 'Decimal',
116
+ codecId: 'pg/numeric@1',
117
+ nativeType: 'numeric',
118
+ },
119
+ 'db.Timestamp': {
120
+ args: 'optionalPrecision',
121
+ baseType: 'DateTime',
122
+ codecId: 'pg/timestamp@1',
123
+ nativeType: 'timestamp',
124
+ },
125
+ 'db.Timestamptz': {
126
+ args: 'optionalPrecision',
127
+ baseType: 'DateTime',
128
+ codecId: 'pg/timestamptz@1',
129
+ nativeType: 'timestamptz',
130
+ },
131
+ 'db.Date': { args: 'noArgs', baseType: 'DateTime', codecId: null, nativeType: 'date' },
132
+ 'db.Time': {
133
+ args: 'optionalPrecision',
134
+ baseType: 'DateTime',
135
+ codecId: 'pg/time@1',
136
+ nativeType: 'time',
137
+ },
138
+ 'db.Timetz': {
139
+ args: 'optionalPrecision',
140
+ baseType: 'DateTime',
141
+ codecId: 'pg/timetz@1',
142
+ nativeType: 'timetz',
143
+ },
144
+ 'db.Json': { args: 'noArgs', baseType: 'Json', codecId: 'pg/json@1', nativeType: 'json' },
145
+ };
146
+
147
+ export function resolveDbNativeTypeAttribute(input: {
148
+ readonly attribute: PslAttribute;
149
+ readonly baseType: string;
150
+ readonly baseDescriptor: ColumnDescriptor;
151
+ readonly diagnostics: ContractSourceDiagnostic[];
152
+ readonly sourceId: string;
153
+ readonly entityLabel: string;
154
+ }): ColumnDescriptor | undefined {
155
+ const spec = NATIVE_TYPE_SPECS[input.attribute.name];
156
+ if (!spec) {
157
+ input.diagnostics.push({
158
+ code: 'PSL_UNSUPPORTED_NAMED_TYPE_ATTRIBUTE',
159
+ message: `${input.entityLabel} uses unsupported attribute "@${input.attribute.name}"`,
160
+ sourceId: input.sourceId,
161
+ span: input.attribute.span,
162
+ });
163
+ return undefined;
164
+ }
165
+
166
+ if (input.baseType !== spec.baseType) {
167
+ return pushInvalidAttributeArgument({
168
+ diagnostics: input.diagnostics,
169
+ sourceId: input.sourceId,
170
+ span: input.attribute.span,
171
+ message: `${input.entityLabel} uses @${input.attribute.name} on unsupported base type "${input.baseType}". Expected "${spec.baseType}".`,
172
+ });
173
+ }
174
+
175
+ switch (spec.args) {
176
+ case 'noArgs': {
177
+ if (getPositionalArguments(input.attribute).length > 0 || input.attribute.args.length > 0) {
178
+ return pushInvalidAttributeArgument({
179
+ diagnostics: input.diagnostics,
180
+ sourceId: input.sourceId,
181
+ span: input.attribute.span,
182
+ message: `${input.entityLabel} @${input.attribute.name} does not accept arguments.`,
183
+ });
184
+ }
185
+ return {
186
+ codecId: spec.codecId ?? input.baseDescriptor.codecId,
187
+ nativeType: spec.nativeType,
188
+ };
189
+ }
190
+ case 'optionalLength': {
191
+ const length = parseOptionalSingleIntegerArgument({
192
+ attribute: input.attribute,
193
+ diagnostics: input.diagnostics,
194
+ sourceId: input.sourceId,
195
+ entityLabel: input.entityLabel,
196
+ minimum: 1,
197
+ valueLabel: 'positive integer length',
198
+ });
199
+ if (length === undefined) {
200
+ return undefined;
201
+ }
202
+ return {
203
+ codecId: spec.codecId,
204
+ nativeType: spec.nativeType,
205
+ ...(length === null ? {} : { typeParams: { length } }),
206
+ };
207
+ }
208
+ case 'optionalPrecision': {
209
+ const precision = parseOptionalSingleIntegerArgument({
210
+ attribute: input.attribute,
211
+ diagnostics: input.diagnostics,
212
+ sourceId: input.sourceId,
213
+ entityLabel: input.entityLabel,
214
+ minimum: 0,
215
+ valueLabel: 'non-negative integer precision',
216
+ });
217
+ if (precision === undefined) {
218
+ return undefined;
219
+ }
220
+ return {
221
+ codecId: spec.codecId,
222
+ nativeType: spec.nativeType,
223
+ ...(precision === null ? {} : { typeParams: { precision } }),
224
+ };
225
+ }
226
+ case 'optionalNumeric': {
227
+ const numeric = parseOptionalNumericArguments({
228
+ attribute: input.attribute,
229
+ diagnostics: input.diagnostics,
230
+ sourceId: input.sourceId,
231
+ entityLabel: input.entityLabel,
232
+ });
233
+ if (numeric === undefined) {
234
+ return undefined;
235
+ }
236
+ return {
237
+ codecId: spec.codecId,
238
+ nativeType: spec.nativeType,
239
+ ...(numeric === null ? {} : { typeParams: numeric }),
240
+ };
241
+ }
242
+ }
243
+ }
244
+
245
+ export function parsePgvectorLength(input: {
246
+ readonly attribute: PslAttribute;
247
+ readonly diagnostics: ContractSourceDiagnostic[];
248
+ readonly sourceId: string;
249
+ }): number | undefined {
250
+ const namedLength = getNamedArgument(input.attribute, 'length');
251
+ const namedDim = getNamedArgument(input.attribute, 'dim');
252
+ const positional = getPositionalArgument(input.attribute);
253
+ const raw = namedLength ?? namedDim ?? positional;
254
+ if (!raw) {
255
+ input.diagnostics.push({
256
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
257
+ message: '@pgvector.column requires length/dim argument',
258
+ sourceId: input.sourceId,
259
+ span: input.attribute.span,
260
+ });
261
+ return undefined;
262
+ }
263
+ const parsed = Number(unquoteStringLiteral(raw));
264
+ if (!Number.isInteger(parsed) || parsed < 1) {
265
+ input.diagnostics.push({
266
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
267
+ message: '@pgvector.column length/dim must be a positive integer',
268
+ sourceId: input.sourceId,
269
+ span: input.attribute.span,
270
+ });
271
+ return undefined;
272
+ }
273
+ return parsed;
274
+ }
275
+
276
+ export function parseDefaultLiteralValue(expression: string): ColumnDefault | undefined {
277
+ const trimmed = expression.trim();
278
+ if (trimmed === 'true' || trimmed === 'false') {
279
+ return { kind: 'literal', value: trimmed === 'true' };
280
+ }
281
+ const numericValue = Number(trimmed);
282
+ if (!Number.isNaN(numericValue) && trimmed.length > 0 && !/^(['"]).*\1$/.test(trimmed)) {
283
+ return { kind: 'literal', value: numericValue };
284
+ }
285
+ if (/^(['"]).*\1$/.test(trimmed)) {
286
+ return { kind: 'literal', value: unquoteStringLiteral(trimmed) };
287
+ }
288
+ return undefined;
289
+ }
290
+
291
+ export function lowerDefaultForField(input: {
292
+ readonly modelName: string;
293
+ readonly fieldName: string;
294
+ readonly defaultAttribute: PslAttribute;
295
+ readonly columnDescriptor: ColumnDescriptor;
296
+ readonly generatorDescriptorById: ReadonlyMap<string, MutationDefaultGeneratorDescriptor>;
297
+ readonly sourceId: string;
298
+ readonly defaultFunctionRegistry: ControlMutationDefaultRegistry;
299
+ readonly diagnostics: ContractSourceDiagnostic[];
300
+ }): {
301
+ readonly defaultValue?: ColumnDefault;
302
+ readonly executionDefault?: ExecutionMutationDefaultValue;
303
+ } {
304
+ const positionalEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'positional');
305
+ const namedEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'named');
306
+
307
+ if (namedEntries.length > 0 || positionalEntries.length !== 1) {
308
+ input.diagnostics.push({
309
+ code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
310
+ message: `Field "${input.modelName}.${input.fieldName}" requires exactly one positional @default(...) expression.`,
311
+ sourceId: input.sourceId,
312
+ span: input.defaultAttribute.span,
313
+ });
314
+ return {};
315
+ }
316
+
317
+ const expressionEntry = getPositionalArgumentEntry(input.defaultAttribute);
318
+ if (!expressionEntry) {
319
+ input.diagnostics.push({
320
+ code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
321
+ message: `Field "${input.modelName}.${input.fieldName}" requires a positional @default(...) expression.`,
322
+ sourceId: input.sourceId,
323
+ span: input.defaultAttribute.span,
324
+ });
325
+ return {};
326
+ }
327
+
328
+ const literalDefault = parseDefaultLiteralValue(expressionEntry.value);
329
+ if (literalDefault) {
330
+ return { defaultValue: literalDefault };
331
+ }
332
+
333
+ const defaultFunctionCall = parseDefaultFunctionCall(expressionEntry.value, expressionEntry.span);
334
+ if (!defaultFunctionCall) {
335
+ input.diagnostics.push({
336
+ code: 'PSL_INVALID_DEFAULT_VALUE',
337
+ message: `Unsupported default value "${expressionEntry.value}"`,
338
+ sourceId: input.sourceId,
339
+ span: input.defaultAttribute.span,
340
+ });
341
+ return {};
342
+ }
343
+
344
+ const lowered = lowerDefaultFunctionWithRegistry({
345
+ call: defaultFunctionCall,
346
+ registry: input.defaultFunctionRegistry,
347
+ context: {
348
+ sourceId: input.sourceId,
349
+ modelName: input.modelName,
350
+ fieldName: input.fieldName,
351
+ columnCodecId: input.columnDescriptor.codecId,
352
+ },
353
+ });
354
+
355
+ if (!lowered.ok) {
356
+ input.diagnostics.push(lowered.diagnostic);
357
+ return {};
358
+ }
359
+
360
+ if (lowered.value.kind === 'storage') {
361
+ return { defaultValue: lowered.value.defaultValue };
362
+ }
363
+
364
+ const generatorDescriptor = input.generatorDescriptorById.get(lowered.value.generated.id);
365
+ if (!generatorDescriptor) {
366
+ input.diagnostics.push({
367
+ code: 'PSL_INVALID_DEFAULT_APPLICABILITY',
368
+ message: `Default generator "${lowered.value.generated.id}" is not available in the composed mutation default registry.`,
369
+ sourceId: input.sourceId,
370
+ span: expressionEntry.span,
371
+ });
372
+ return {};
373
+ }
374
+
375
+ if (!generatorDescriptor.applicableCodecIds.includes(input.columnDescriptor.codecId)) {
376
+ input.diagnostics.push({
377
+ code: 'PSL_INVALID_DEFAULT_APPLICABILITY',
378
+ message: `Default generator "${generatorDescriptor.id}" is not applicable to "${input.modelName}.${input.fieldName}" with codecId "${input.columnDescriptor.codecId}".`,
379
+ sourceId: input.sourceId,
380
+ span: expressionEntry.span,
381
+ });
382
+ return {};
383
+ }
384
+
385
+ return { executionDefault: lowered.value.generated };
386
+ }
387
+
388
+ export function resolveColumnDescriptor(
389
+ field: PslField,
390
+ enumTypeDescriptors: Map<string, ColumnDescriptor>,
391
+ namedTypeDescriptors: Map<string, ColumnDescriptor>,
392
+ scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
393
+ ): ColumnDescriptor | undefined {
394
+ if (field.typeRef && namedTypeDescriptors.has(field.typeRef)) {
395
+ return namedTypeDescriptors.get(field.typeRef);
396
+ }
397
+ if (namedTypeDescriptors.has(field.typeName)) {
398
+ return namedTypeDescriptors.get(field.typeName);
399
+ }
400
+ if (enumTypeDescriptors.has(field.typeName)) {
401
+ return enumTypeDescriptors.get(field.typeName);
402
+ }
403
+ return scalarTypeDescriptors.get(field.typeName);
404
+ }