@prisma-next/sql-contract-psl 0.5.0-dev.50 → 0.5.0-dev.52

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.
@@ -77,19 +77,19 @@ import {
77
77
 
78
78
  export interface InterpretPslDocumentToSqlContractInput {
79
79
  readonly document: ParsePslDocumentResult;
80
- readonly target: TargetPackRef<'sql', 'postgres'>;
80
+ readonly target: TargetPackRef<'sql', string>;
81
81
  readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
82
82
  readonly composedExtensionPacks?: readonly string[];
83
- readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', 'postgres'>[];
83
+ readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', string>[];
84
84
  readonly controlMutationDefaults?: ControlMutationDefaults;
85
85
  readonly authoringContributions?: AuthoringContributions;
86
86
  }
87
87
 
88
88
  function buildComposedExtensionPackRefs(
89
- target: TargetPackRef<'sql', 'postgres'>,
89
+ target: TargetPackRef<'sql', string>,
90
90
  extensionIds: readonly string[],
91
- extensionPackRefs: readonly ExtensionPackRef<'sql', 'postgres'>[] = [],
92
- ): Record<string, ExtensionPackRef<'sql', 'postgres'>> | undefined {
91
+ extensionPackRefs: readonly ExtensionPackRef<'sql', string>[] = [],
92
+ ): Record<string, ExtensionPackRef<'sql', string>> | undefined {
93
93
  if (extensionIds.length === 0) {
94
94
  return undefined;
95
95
  }
@@ -106,7 +106,7 @@ function buildComposedExtensionPackRefs(
106
106
  familyId: target.familyId,
107
107
  targetId: target.targetId,
108
108
  version: '0.0.1',
109
- } satisfies ExtensionPackRef<'sql', 'postgres'>),
109
+ } satisfies ExtensionPackRef<'sql', string>),
110
110
  ]),
111
111
  );
112
112
  }
@@ -219,6 +219,7 @@ function validateNamedTypeAttributes(input: {
219
219
  readonly sourceId: string;
220
220
  readonly diagnostics: ContractSourceDiagnostic[];
221
221
  readonly composedExtensions: ReadonlySet<string>;
222
+ readonly authoringContributions: AuthoringContributions | undefined;
222
223
  readonly allowDbNativeType: boolean;
223
224
  readonly familyId: string;
224
225
  readonly targetId: string;
@@ -250,6 +251,7 @@ function validateNamedTypeAttributes(input: {
250
251
  const uncomposedNamespace = checkUncomposedNamespace(attribute.name, input.composedExtensions, {
251
252
  familyId: input.familyId,
252
253
  targetId: input.targetId,
254
+ authoringContributions: input.authoringContributions,
253
255
  });
254
256
  if (uncomposedNamespace) {
255
257
  reportUncomposedNamespace({
@@ -289,6 +291,7 @@ function resolveNamedTypeDeclarations(input: ResolveNamedTypeDeclarationsInput):
289
291
  sourceId: input.sourceId,
290
292
  diagnostics: input.diagnostics,
291
293
  composedExtensions: input.composedExtensions,
294
+ authoringContributions: input.authoringContributions,
292
295
  allowDbNativeType: false,
293
296
  familyId: input.familyId,
294
297
  targetId: input.targetId,
@@ -367,6 +370,7 @@ function resolveNamedTypeDeclarations(input: ResolveNamedTypeDeclarationsInput):
367
370
  sourceId: input.sourceId,
368
371
  diagnostics: input.diagnostics,
369
372
  composedExtensions: input.composedExtensions,
373
+ authoringContributions: input.authoringContributions,
370
374
  allowDbNativeType: true,
371
375
  familyId: input.familyId,
372
376
  targetId: input.targetId,
@@ -483,6 +487,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
483
487
  field,
484
488
  sourceId,
485
489
  composedExtensions: input.composedExtensions,
490
+ authoringContributions: input.authoringContributions,
486
491
  diagnostics,
487
492
  familyId: input.familyId,
488
493
  targetId: input.targetId,
@@ -604,7 +609,11 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
604
609
  const uncomposedNamespace = checkUncomposedNamespace(
605
610
  modelAttribute.name,
606
611
  input.composedExtensions,
607
- { familyId: input.familyId, targetId: input.targetId },
612
+ {
613
+ familyId: input.familyId,
614
+ targetId: input.targetId,
615
+ authoringContributions: input.authoringContributions,
616
+ },
608
617
  );
609
618
  if (uncomposedNamespace) {
610
619
  reportUncomposedNamespace({
@@ -762,7 +771,7 @@ function buildModelNodeFromPsl(input: BuildModelNodeInput): BuildModelNodeResult
762
771
  descriptor: resolvedField.descriptor,
763
772
  nullable: resolvedField.field.optional,
764
773
  ...ifDefined('default', resolvedField.defaultValue),
765
- ...ifDefined('executionDefault', resolvedField.executionDefault),
774
+ ...ifDefined('executionDefaults', resolvedField.executionDefaults),
766
775
  })),
767
776
  ...(primaryKeyColumns.length > 0
768
777
  ? {
package/src/provider.ts CHANGED
@@ -10,8 +10,8 @@ import type { ColumnDescriptor } from './psl-column-resolution';
10
10
 
11
11
  export interface PrismaContractOptions {
12
12
  readonly output?: string;
13
- readonly target: TargetPackRef<'sql', 'postgres'>;
14
- readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', 'postgres'>[];
13
+ readonly target: TargetPackRef<'sql', string>;
14
+ readonly composedExtensionPackRefs?: readonly ExtensionPackRef<'sql', string>[];
15
15
  }
16
16
 
17
17
  function buildColumnDescriptorMap(
@@ -1,11 +1,15 @@
1
1
  import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
2
- import type { ColumnDefault, ExecutionMutationDefaultValue } from '@prisma-next/contract/types';
2
+ import type { ColumnDefault, ExecutionMutationDefaultPhases } from '@prisma-next/contract/types';
3
3
  import type {
4
4
  AuthoringContributions,
5
+ AuthoringFieldPresetDescriptor,
5
6
  AuthoringTypeConstructorDescriptor,
6
7
  } from '@prisma-next/framework-components/authoring';
7
8
  import {
9
+ hasRegisteredFieldNamespace,
10
+ instantiateAuthoringFieldPreset,
8
11
  instantiateAuthoringTypeConstructor,
12
+ isAuthoringFieldPresetDescriptor,
9
13
  isAuthoringTypeConstructorDescriptor,
10
14
  validateAuthoringHelperArguments,
11
15
  } from '@prisma-next/framework-components/authoring';
@@ -67,6 +71,34 @@ export function getAuthoringTypeConstructor(
67
71
  return isAuthoringTypeConstructorDescriptor(current) ? current : undefined;
68
72
  }
69
73
 
74
+ /**
75
+ * Walks `authoringContributions.field` segment-by-segment and returns the
76
+ * field-preset descriptor at the resolved path, or `undefined` if no descriptor
77
+ * is registered.
78
+ *
79
+ * Symmetric with `getAuthoringTypeConstructor`. Field presets are strictly
80
+ * richer than type constructors — they can contribute `default` /
81
+ * `executionDefaults` / `id` / `unique` / `nullable` in addition to the
82
+ * `codecId` / `nativeType` / `typeParams` triple. PSL resolution tries field
83
+ * presets first, then falls back to type constructors on miss (see
84
+ * `resolveFieldTypeDescriptor`).
85
+ */
86
+ export function getAuthoringFieldPreset(
87
+ contributions: AuthoringContributions | undefined,
88
+ path: readonly string[],
89
+ ): AuthoringFieldPresetDescriptor | undefined {
90
+ let current: unknown = contributions?.field;
91
+
92
+ for (const segment of path) {
93
+ if (typeof current !== 'object' || current === null || Array.isArray(current)) {
94
+ return undefined;
95
+ }
96
+ current = (current as Record<string, unknown>)[segment];
97
+ }
98
+
99
+ return isAuthoringFieldPresetDescriptor(current) ? current : undefined;
100
+ }
101
+
70
102
  /**
71
103
  * Returns the namespace prefix of `attributeName` if it references an
72
104
  * unrecognized extension namespace, otherwise `undefined`. A namespace is
@@ -75,16 +107,22 @@ export function getAuthoringTypeConstructor(
75
107
  * - `db` (native-type spec, always allowed),
76
108
  * - the active family id (e.g. `sql`),
77
109
  * - the active target id (e.g. `postgres`),
110
+ * - a registered field-preset namespace (e.g. `temporal`),
78
111
  * - present in `composedExtensions`.
79
112
  *
80
- * Family/target namespaces are exempted so that e.g. `@sql.foo` surfaces as
81
- * PSL_UNSUPPORTED_*_ATTRIBUTE (the attribute isn't defined) rather than
82
- * PSL_EXTENSION_NAMESPACE_NOT_COMPOSED (the namespace is already composed).
113
+ * Family/target/field-preset namespaces are exempted so that e.g. `@sql.foo`
114
+ * surfaces as PSL_UNSUPPORTED_*_ATTRIBUTE (the attribute isn't defined)
115
+ * rather than PSL_EXTENSION_NAMESPACE_NOT_COMPOSED (the namespace is already
116
+ * composed).
83
117
  */
84
118
  export function checkUncomposedNamespace(
85
119
  attributeName: string,
86
120
  composedExtensions: ReadonlySet<string>,
87
- context?: { readonly familyId?: string; readonly targetId?: string },
121
+ context?: {
122
+ readonly familyId?: string;
123
+ readonly targetId?: string;
124
+ readonly authoringContributions?: AuthoringContributions | undefined;
125
+ },
88
126
  ): string | undefined {
89
127
  const dotIndex = attributeName.indexOf('.');
90
128
  if (dotIndex <= 0 || dotIndex === attributeName.length - 1) {
@@ -95,6 +133,7 @@ export function checkUncomposedNamespace(
95
133
  namespace === 'db' ||
96
134
  namespace === context?.familyId ||
97
135
  namespace === context?.targetId ||
136
+ hasRegisteredFieldNamespace(context?.authoringContributions, namespace) ||
98
137
  composedExtensions.has(namespace)
99
138
  ) {
100
139
  return undefined;
@@ -126,6 +165,29 @@ export function reportUncomposedNamespace(input: {
126
165
  });
127
166
  }
128
167
 
168
+ /**
169
+ * Pushes the canonical `PSL_UNKNOWN_FIELD_PRESET` diagnostic when a typoed
170
+ * preset name is referenced inside a registered field-preset namespace. The
171
+ * `data` payload exposes the namespace and full helper path so machine
172
+ * consumers (agents, IDE extensions) don't have to parse the prose.
173
+ */
174
+ export function reportUnknownFieldPreset(input: {
175
+ readonly entityLabel: string;
176
+ readonly namespace: string;
177
+ readonly helperPath: string;
178
+ readonly sourceId: string;
179
+ readonly span: PslSpan;
180
+ readonly diagnostics: ContractSourceDiagnostic[];
181
+ }): void {
182
+ input.diagnostics.push({
183
+ code: 'PSL_UNKNOWN_FIELD_PRESET',
184
+ message: `${input.entityLabel} references unknown field preset "${input.helperPath}". Check the spelling against the available presets in the "${input.namespace}" namespace.`,
185
+ sourceId: input.sourceId,
186
+ span: input.span,
187
+ data: { namespace: input.namespace, helperPath: input.helperPath },
188
+ });
189
+ }
190
+
129
191
  export function instantiatePslTypeConstructor(input: {
130
192
  readonly call: PslTypeConstructorCall;
131
193
  readonly descriptor: AuthoringTypeConstructorDescriptor;
@@ -200,17 +262,19 @@ export function resolvePslTypeConstructorDescriptor(input: {
200
262
  return descriptor;
201
263
  }
202
264
 
203
- const namespace = input.call.path.length > 1 ? input.call.path[0] : undefined;
204
- if (
205
- namespace &&
206
- namespace !== 'db' &&
207
- namespace !== input.familyId &&
208
- namespace !== input.targetId &&
209
- !input.composedExtensions.has(namespace)
210
- ) {
265
+ const uncomposedNamespace = checkUncomposedNamespace(
266
+ input.call.path.join('.'),
267
+ input.composedExtensions,
268
+ {
269
+ familyId: input.familyId,
270
+ targetId: input.targetId,
271
+ authoringContributions: input.authoringContributions,
272
+ },
273
+ );
274
+ if (uncomposedNamespace) {
211
275
  reportUncomposedNamespace({
212
276
  subjectLabel: `Type constructor "${input.call.path.join('.')}"`,
213
- namespace,
277
+ namespace: uncomposedNamespace,
214
278
  sourceId: input.sourceId,
215
279
  span: input.call.span,
216
280
  diagnostics: input.diagnostics,
@@ -227,8 +291,99 @@ export function resolvePslTypeConstructorDescriptor(input: {
227
291
  });
228
292
  }
229
293
 
294
+ /**
295
+ * Instantiates a field-preset call against its descriptor, coercing PSL AST
296
+ * arguments into the descriptor's typed argument shape and returning the
297
+ * preset's full set of contract contributions.
298
+ *
299
+ * Symmetric with `instantiatePslTypeConstructor` but richer: a field preset
300
+ * can contribute `default`, `executionDefaults`, `id`, `unique`, and
301
+ * `nullable` in addition to the storage-type triple. PSL → typed-args
302
+ * coercion happens here (via `mapPslHelperArgs`) so that
303
+ * `instantiateAuthoringFieldPreset` itself stays typed-input-only and TS
304
+ * keeps its zero-runtime-validation cost.
305
+ */
306
+ export function instantiatePslFieldPreset(input: {
307
+ readonly call: PslTypeConstructorCall;
308
+ readonly descriptor: AuthoringFieldPresetDescriptor;
309
+ readonly diagnostics: ContractSourceDiagnostic[];
310
+ readonly sourceId: string;
311
+ readonly entityLabel: string;
312
+ }):
313
+ | {
314
+ readonly descriptor: ColumnDescriptor;
315
+ readonly nullable: boolean;
316
+ readonly default?: ColumnDefault;
317
+ readonly executionDefaults?: ExecutionMutationDefaultPhases;
318
+ readonly id: boolean;
319
+ readonly unique: boolean;
320
+ }
321
+ | undefined {
322
+ const helperPath = input.call.path.join('.');
323
+ const args = mapPslHelperArgs({
324
+ args: input.call.args,
325
+ descriptors: input.descriptor.args ?? [],
326
+ helperLabel: `preset "${helperPath}"`,
327
+ span: input.call.span,
328
+ diagnostics: input.diagnostics,
329
+ sourceId: input.sourceId,
330
+ entityLabel: input.entityLabel,
331
+ });
332
+ if (!args) {
333
+ return undefined;
334
+ }
335
+
336
+ try {
337
+ validateAuthoringHelperArguments(helperPath, input.descriptor.args, args);
338
+ const instantiated = instantiateAuthoringFieldPreset(input.descriptor, args);
339
+ return {
340
+ descriptor: {
341
+ codecId: instantiated.descriptor.codecId,
342
+ nativeType: instantiated.descriptor.nativeType,
343
+ ...(instantiated.descriptor.typeParams !== undefined
344
+ ? { typeParams: instantiated.descriptor.typeParams }
345
+ : {}),
346
+ },
347
+ nullable: instantiated.nullable,
348
+ ...(instantiated.default !== undefined ? { default: instantiated.default } : {}),
349
+ ...(instantiated.executionDefaults !== undefined
350
+ ? { executionDefaults: instantiated.executionDefaults }
351
+ : {}),
352
+ id: instantiated.id,
353
+ unique: instantiated.unique,
354
+ };
355
+ } catch (error) {
356
+ const message = error instanceof Error ? error.message : String(error);
357
+ input.diagnostics.push({
358
+ code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT',
359
+ message: `${input.entityLabel} preset "${helperPath}" ${message}`,
360
+ sourceId: input.sourceId,
361
+ span: input.call.span,
362
+ });
363
+ return undefined;
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Contract contributions a field preset adds beyond the bare storage-type
369
+ * triple. Set when a field is resolved through the field-preset dispatch
370
+ * path; absent when resolved through the type-constructor path or as a
371
+ * scalar/enum/named-type lookup.
372
+ */
373
+ export type FieldPresetContributions = {
374
+ readonly nullable: boolean;
375
+ readonly id: boolean;
376
+ readonly unique: boolean;
377
+ readonly default?: ColumnDefault;
378
+ readonly executionDefaults?: ExecutionMutationDefaultPhases;
379
+ };
380
+
230
381
  export type ResolveFieldTypeResult =
231
- | { readonly ok: true; readonly descriptor: ColumnDescriptor }
382
+ | {
383
+ readonly ok: true;
384
+ readonly descriptor: ColumnDescriptor;
385
+ readonly presetContributions?: FieldPresetContributions;
386
+ }
232
387
  | { readonly ok: false; readonly alreadyReported: boolean };
233
388
 
234
389
  export function resolveFieldTypeDescriptor(input: {
@@ -245,18 +400,73 @@ export function resolveFieldTypeDescriptor(input: {
245
400
  readonly entityLabel: string;
246
401
  }): ResolveFieldTypeResult {
247
402
  if (input.field.typeConstructor) {
403
+ // Field presets carry richer semantics than type constructors, so a field
404
+ // preset match is the complete answer. Shared composition rejects exact
405
+ // cross-registry collisions before PSL resolution can observe them.
406
+ const presetDescriptor = getAuthoringFieldPreset(
407
+ input.authoringContributions,
408
+ input.field.typeConstructor.path,
409
+ );
410
+ if (presetDescriptor) {
411
+ const instantiated = instantiatePslFieldPreset({
412
+ call: input.field.typeConstructor,
413
+ descriptor: presetDescriptor,
414
+ diagnostics: input.diagnostics,
415
+ sourceId: input.sourceId,
416
+ entityLabel: input.entityLabel,
417
+ });
418
+ if (!instantiated) {
419
+ return { ok: false, alreadyReported: true };
420
+ }
421
+ const presetContributions: FieldPresetContributions = {
422
+ nullable: instantiated.nullable,
423
+ id: instantiated.id,
424
+ unique: instantiated.unique,
425
+ ...(instantiated.default !== undefined ? { default: instantiated.default } : {}),
426
+ ...(instantiated.executionDefaults !== undefined
427
+ ? { executionDefaults: instantiated.executionDefaults }
428
+ : {}),
429
+ };
430
+ return { ok: true, descriptor: instantiated.descriptor, presetContributions };
431
+ }
432
+
248
433
  const helperPath = input.field.typeConstructor.path.join('.');
249
- const descriptor = resolvePslTypeConstructorDescriptor({
250
- call: input.field.typeConstructor,
251
- authoringContributions: input.authoringContributions,
252
- composedExtensions: input.composedExtensions,
253
- familyId: input.familyId,
254
- targetId: input.targetId,
255
- diagnostics: input.diagnostics,
256
- sourceId: input.sourceId,
257
- unsupportedCode: 'PSL_UNSUPPORTED_FIELD_TYPE',
258
- unsupportedMessage: `${input.entityLabel} type constructor "${helperPath}" is not supported in SQL PSL provider v1`,
259
- });
434
+ const namespacePrefix =
435
+ input.field.typeConstructor.path.length > 1 ? input.field.typeConstructor.path[0] : undefined;
436
+ const typeDescriptor = getAuthoringTypeConstructor(
437
+ input.authoringContributions,
438
+ input.field.typeConstructor.path,
439
+ );
440
+
441
+ if (
442
+ !typeDescriptor &&
443
+ namespacePrefix &&
444
+ hasRegisteredFieldNamespace(input.authoringContributions, namespacePrefix)
445
+ ) {
446
+ reportUnknownFieldPreset({
447
+ entityLabel: input.entityLabel,
448
+ namespace: namespacePrefix,
449
+ helperPath,
450
+ sourceId: input.sourceId,
451
+ span: input.field.typeConstructor.span,
452
+ diagnostics: input.diagnostics,
453
+ });
454
+ return { ok: false, alreadyReported: true };
455
+ }
456
+
457
+ const descriptor =
458
+ typeDescriptor ??
459
+ resolvePslTypeConstructorDescriptor({
460
+ call: input.field.typeConstructor,
461
+ authoringContributions: input.authoringContributions,
462
+ composedExtensions: input.composedExtensions,
463
+ familyId: input.familyId,
464
+ targetId: input.targetId,
465
+ diagnostics: input.diagnostics,
466
+ sourceId: input.sourceId,
467
+ unsupportedCode: 'PSL_UNSUPPORTED_FIELD_TYPE',
468
+ unsupportedMessage: `${input.entityLabel} type constructor "${helperPath}" is not supported in SQL PSL provider v1`,
469
+ });
260
470
  if (!descriptor) {
261
471
  return { ok: false, alreadyReported: true };
262
472
  }
@@ -495,7 +705,7 @@ export function lowerDefaultForField(input: {
495
705
  readonly diagnostics: ContractSourceDiagnostic[];
496
706
  }): {
497
707
  readonly defaultValue?: ColumnDefault;
498
- readonly executionDefault?: ExecutionMutationDefaultValue;
708
+ readonly executionDefaults?: ExecutionMutationDefaultPhases;
499
709
  } {
500
710
  const positionalEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'positional');
501
711
  const namedEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'named');
@@ -568,6 +778,21 @@ export function lowerDefaultForField(input: {
568
778
  return {};
569
779
  }
570
780
 
781
+ // Preset-only generators (e.g. `timestampNow`) co-register their codec
782
+ // through the preset descriptor, so they don't carry an
783
+ // `applicableCodecIds` list. Such a generator surfacing on the
784
+ // `@default(...)` lowering path is itself the bug — emit a diagnostic
785
+ // pointing the user at the correct authoring surface.
786
+ if (generatorDescriptor.applicableCodecIds === undefined) {
787
+ input.diagnostics.push({
788
+ code: 'PSL_INVALID_DEFAULT_APPLICABILITY',
789
+ message: `Default generator "${generatorDescriptor.id}" is not applicable to "@default(...)" lowering. Use the corresponding field preset (e.g. \`temporal.${generatorDescriptor.id === 'timestampNow' ? 'updatedAt' : generatorDescriptor.id}()\`) instead.`,
790
+ sourceId: input.sourceId,
791
+ span: expressionEntry.span,
792
+ });
793
+ return {};
794
+ }
795
+
571
796
  if (!generatorDescriptor.applicableCodecIds.includes(input.columnDescriptor.codecId)) {
572
797
  input.diagnostics.push({
573
798
  code: 'PSL_INVALID_DEFAULT_APPLICABILITY',
@@ -578,7 +803,7 @@ export function lowerDefaultForField(input: {
578
803
  return {};
579
804
  }
580
805
 
581
- return { executionDefault: lowered.value.generated };
806
+ return { executionDefaults: { onCreate: lowered.value.generated } };
582
807
  }
583
808
 
584
809
  export function resolveColumnDescriptor(