@prisma-next/sql-contract-psl 0.13.0 → 0.14.0-dev.10

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/src/provider.ts CHANGED
@@ -1,15 +1,18 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import type { ContractConfig } from '@prisma-next/config/config-types';
2
+ import type { ContractConfig, ContractSourceDiagnostic } from '@prisma-next/config/config-types';
3
3
  import { applySpecifierDefaultControlPolicy } from '@prisma-next/contract/apply-specifier-default-control-policy';
4
4
  import type { ControlPolicy } from '@prisma-next/contract/types';
5
5
  import type { CodecLookup } from '@prisma-next/framework-components/codec';
6
6
  import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/framework-components/components';
7
7
  import type { Namespace } from '@prisma-next/framework-components/ir';
8
- import { parsePslDocument } from '@prisma-next/psl-parser';
8
+ import { buildSymbolTable, rangeToPslSpan } from '@prisma-next/psl-parser';
9
+ import type { ParseDiagnostic, SourceFile } from '@prisma-next/psl-parser/syntax';
10
+ import { parse } from '@prisma-next/psl-parser/syntax';
9
11
  import type { SqlNamespaceTablesInput } from '@prisma-next/sql-contract/types';
10
12
  import { ifDefined } from '@prisma-next/utils/defined';
11
13
  import { notOk, ok } from '@prisma-next/utils/result';
12
14
  import { basename, extname } from 'pathe';
15
+
13
16
  import { interpretPslDocumentToSqlContract } from './interpreter';
14
17
  import type { ColumnDescriptor } from './psl-column-resolution';
15
18
 
@@ -42,6 +45,19 @@ function defaultOutputFromSchemaPath(schemaPath: string): string {
42
45
  return `${base}.json`;
43
46
  }
44
47
 
48
+ function mapParseDiagnostics(
49
+ diagnostics: readonly ParseDiagnostic[],
50
+ sourceFile: SourceFile,
51
+ sourceId: string,
52
+ ): ContractSourceDiagnostic[] {
53
+ return diagnostics.map((diagnostic) => ({
54
+ code: diagnostic.code,
55
+ message: diagnostic.message,
56
+ sourceId,
57
+ span: rangeToPslSpan(diagnostic.range, sourceFile),
58
+ }));
59
+ }
60
+
45
61
  function buildColumnDescriptorMap(
46
62
  scalarTypeDescriptors: ReadonlyMap<string, string>,
47
63
  codecLookup: CodecLookup,
@@ -58,6 +74,7 @@ function buildColumnDescriptorMap(
58
74
  export function prismaContract(schemaPath: string, options: PrismaContractOptions): ContractConfig {
59
75
  return {
60
76
  source: {
77
+ sourceFormat: 'psl',
61
78
  inputs: [schemaPath],
62
79
  load: async (context) => {
63
80
  const [absoluteSchemaPath] = context.resolvedInputs;
@@ -84,18 +101,31 @@ export function prismaContract(schemaPath: string, options: PrismaContractOption
84
101
  });
85
102
  }
86
103
 
87
- const document = parsePslDocument({
88
- schema,
89
- sourceId: schemaPath,
90
- });
91
-
92
104
  const scalarTypeDescriptors = buildColumnDescriptorMap(
93
105
  context.scalarTypeDescriptors,
94
106
  context.codecLookup,
95
107
  );
96
108
 
97
- const interpreted = interpretPslDocumentToSqlContract({
109
+ const { document, sourceFile, diagnostics: parseDiagnostics } = parse(schema);
110
+ const { table: symbolTable, diagnostics: symbolTableDiagnostics } = buildSymbolTable({
98
111
  document,
112
+ sourceFile,
113
+ scalarTypes: [...context.scalarTypeDescriptors.keys()],
114
+ pslBlockDescriptors: context.authoringContributions.pslBlockDescriptors,
115
+ });
116
+
117
+ // Do not short-circuit on provider-level diagnostics; recovered CST can
118
+ // still produce interpreter diagnostics in the same response.
119
+ const seedDiagnostics = [
120
+ ...mapParseDiagnostics(parseDiagnostics, sourceFile, schemaPath),
121
+ ...mapParseDiagnostics(symbolTableDiagnostics, sourceFile, schemaPath),
122
+ ];
123
+
124
+ const interpreted = interpretPslDocumentToSqlContract({
125
+ symbolTable,
126
+ sourceFile,
127
+ sourceId: schemaPath,
128
+ seedDiagnostics,
99
129
  target: options.target,
100
130
  authoringContributions: context.authoringContributions,
101
131
  scalarTypeDescriptors,
@@ -114,6 +144,7 @@ export function prismaContract(schemaPath: string, options: PrismaContractOption
114
144
  ),
115
145
  controlMutationDefaults: context.controlMutationDefaults,
116
146
  ...ifDefined('createNamespace', options.createNamespace),
147
+ codecLookup: context.codecLookup,
117
148
  });
118
149
  if (!interpreted.ok) {
119
150
  return interpreted;
@@ -1,9 +1,13 @@
1
1
  import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
2
2
  import type { ControlPolicy } from '@prisma-next/contract/types';
3
- import type { PslAttribute, PslSpan } from '@prisma-next/psl-parser';
4
- import { getPositionalArgument, parseQuotedStringLiteral } from '@prisma-next/psl-parser';
3
+ import type { PslSpan, ResolvedAttribute } from '@prisma-next/psl-parser';
4
+ import { parseQuotedStringLiteral } from '@prisma-next/psl-parser';
5
5
 
6
- export { getPositionalArgument, parseQuotedStringLiteral };
6
+ export { parseQuotedStringLiteral };
7
+
8
+ export function getPositionalArgument(attribute: ResolvedAttribute, index = 0): string | undefined {
9
+ return attribute.args.filter((arg) => arg.kind === 'positional')[index]?.value;
10
+ }
7
11
 
8
12
  export function lowerFirst(value: string): string {
9
13
  if (value.length === 0) return value;
@@ -11,13 +15,13 @@ export function lowerFirst(value: string): string {
11
15
  }
12
16
 
13
17
  export function getAttribute(
14
- attributes: readonly PslAttribute[] | undefined,
18
+ attributes: readonly ResolvedAttribute[] | undefined,
15
19
  name: string,
16
- ): PslAttribute | undefined {
20
+ ): ResolvedAttribute | undefined {
17
21
  return attributes?.find((attribute) => attribute.name === name);
18
22
  }
19
23
 
20
- export function getNamedArgument(attribute: PslAttribute, name: string): string | undefined {
24
+ export function getNamedArgument(attribute: ResolvedAttribute, name: string): string | undefined {
21
25
  const entry = attribute.args.find((arg) => arg.kind === 'named' && arg.name === name);
22
26
  if (!entry || entry.kind !== 'named') {
23
27
  return undefined;
@@ -26,7 +30,7 @@ export function getNamedArgument(attribute: PslAttribute, name: string): string
26
30
  }
27
31
 
28
32
  export function getPositionalArgumentEntry(
29
- attribute: PslAttribute,
33
+ attribute: ResolvedAttribute,
30
34
  index = 0,
31
35
  ): { value: string; span: PslSpan } | undefined {
32
36
  const entries = attribute.args.filter((arg) => arg.kind === 'positional');
@@ -63,7 +67,7 @@ export function parseFieldList(value: string): readonly string[] | undefined {
63
67
  }
64
68
 
65
69
  export function parseMapName(input: {
66
- readonly attribute: PslAttribute | undefined;
70
+ readonly attribute: ResolvedAttribute | undefined;
67
71
  readonly defaultValue: string;
68
72
  readonly sourceId: string;
69
73
  readonly diagnostics: ContractSourceDiagnostic[];
@@ -98,7 +102,7 @@ export function parseMapName(input: {
98
102
  }
99
103
 
100
104
  export function parseConstraintMapArgument(input: {
101
- readonly attribute: PslAttribute | undefined;
105
+ readonly attribute: ResolvedAttribute | undefined;
102
106
  readonly sourceId: string;
103
107
  readonly diagnostics: ContractSourceDiagnostic[];
104
108
  readonly entityLabel: string;
@@ -128,7 +132,7 @@ export function parseConstraintMapArgument(input: {
128
132
  return undefined;
129
133
  }
130
134
 
131
- export function getPositionalArguments(attribute: PslAttribute): readonly string[] {
135
+ export function getPositionalArguments(attribute: ResolvedAttribute): readonly string[] {
132
136
  return attribute.args
133
137
  .filter((arg) => arg.kind === 'positional')
134
138
  .map((arg) => (arg.kind === 'positional' ? arg.value : ''));
@@ -276,7 +280,7 @@ export function pushInvalidAttributeArgument(input: {
276
280
  }
277
281
 
278
282
  export function parseOptionalSingleIntegerArgument(input: {
279
- readonly attribute: PslAttribute;
283
+ readonly attribute: ResolvedAttribute;
280
284
  readonly diagnostics: ContractSourceDiagnostic[];
281
285
  readonly sourceId: string;
282
286
  readonly entityLabel: string;
@@ -319,7 +323,7 @@ export function parseOptionalSingleIntegerArgument(input: {
319
323
  }
320
324
 
321
325
  export function parseOptionalNumericArguments(input: {
322
- readonly attribute: PslAttribute;
326
+ readonly attribute: ResolvedAttribute;
323
327
  readonly diagnostics: ContractSourceDiagnostic[];
324
328
  readonly sourceId: string;
325
329
  readonly entityLabel: string;
@@ -374,7 +378,7 @@ export function parseOptionalNumericArguments(input: {
374
378
  }
375
379
 
376
380
  export function parseAttributeFieldList(input: {
377
- readonly attribute: PslAttribute;
381
+ readonly attribute: ResolvedAttribute;
378
382
  readonly sourceId: string;
379
383
  readonly diagnostics: ContractSourceDiagnostic[];
380
384
  readonly code: string;
@@ -426,7 +430,7 @@ function isControlPolicyLiteral(value: string): value is ControlPolicy {
426
430
  }
427
431
 
428
432
  export function parseControlPolicyAttribute(input: {
429
- readonly attribute: PslAttribute;
433
+ readonly attribute: ResolvedAttribute;
430
434
  readonly sourceId: string;
431
435
  readonly diagnostics: ContractSourceDiagnostic[];
432
436
  }): ControlPolicy | undefined {
@@ -1,6 +1,6 @@
1
1
  import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
2
2
  import type { AuthoringArgumentDescriptor } from '@prisma-next/framework-components/authoring';
3
- import type { PslAttributeArgument, PslSpan } from '@prisma-next/psl-parser';
3
+ import type { PslSpan, ResolvedAttributeArg } from '@prisma-next/psl-parser';
4
4
  import { unquoteStringLiteral } from './psl-attribute-parsing';
5
5
 
6
6
  const INVALID_AUTHORING_ARGUMENT = Symbol('invalidAuthoringArgument');
@@ -351,7 +351,7 @@ function pushInvalidPslHelperArgument(input: {
351
351
  }
352
352
 
353
353
  export function mapPslHelperArgs(input: {
354
- readonly args: readonly PslAttributeArgument[];
354
+ readonly args: readonly ResolvedAttributeArg[];
355
355
  readonly descriptors: readonly AuthoringArgumentDescriptor[];
356
356
  readonly helperLabel: string;
357
357
  readonly span: PslSpan;
@@ -20,12 +20,13 @@ import type {
20
20
  MutationDefaultGeneratorDescriptor,
21
21
  } from '@prisma-next/framework-components/control';
22
22
  import type {
23
- PslAttribute,
24
- PslField,
23
+ FieldSymbol,
25
24
  PslSpan,
26
- PslTypeConstructorCall,
25
+ ResolvedAttribute,
26
+ ResolvedTypeConstructorCall,
27
27
  } from '@prisma-next/psl-parser';
28
28
  import { blindCast } from '@prisma-next/utils/casts';
29
+
29
30
  import {
30
31
  lowerDefaultFunctionWithRegistry,
31
32
  parseDefaultFunctionCall,
@@ -207,7 +208,7 @@ export function reportUnknownFieldPreset(input: {
207
208
  }
208
209
 
209
210
  export function instantiatePslTypeConstructor(input: {
210
- readonly call: PslTypeConstructorCall;
211
+ readonly call: ResolvedTypeConstructorCall;
211
212
  readonly descriptor: AuthoringTypeConstructorDescriptor;
212
213
  readonly diagnostics: ContractSourceDiagnostic[];
213
214
  readonly sourceId: string;
@@ -265,7 +266,7 @@ function pushUnsupportedTypeConstructorDiagnostic(input: {
265
266
  }
266
267
 
267
268
  export function resolvePslTypeConstructorDescriptor(input: {
268
- readonly call: PslTypeConstructorCall;
269
+ readonly call: ResolvedTypeConstructorCall;
269
270
  readonly authoringContributions: AuthoringContributions | undefined;
270
271
  readonly composedExtensions: ReadonlySet<string>;
271
272
  readonly familyId: string;
@@ -314,8 +315,8 @@ export function resolvePslTypeConstructorDescriptor(input: {
314
315
  *
315
316
  * Symmetric with `instantiatePslTypeConstructor` but richer: a field preset can contribute `default`, `executionDefaults`, `id`, `unique`, and `nullable` in addition to the storage-type triple. PSL → typed-args coercion happens here (via `mapPslHelperArgs`) so that `instantiateAuthoringFieldPreset` itself stays typed-input-only and TS keeps its zero-runtime-validation cost.
316
317
  */
317
- export function instantiatePslFieldPreset(input: {
318
- readonly call: PslTypeConstructorCall;
318
+ export function instantiateFieldPreset(input: {
319
+ readonly call: ResolvedTypeConstructorCall;
319
320
  readonly descriptor: AuthoringFieldPresetDescriptor;
320
321
  readonly diagnostics: ContractSourceDiagnostic[];
321
322
  readonly sourceId: string;
@@ -395,7 +396,7 @@ export type ResolveFieldTypeResult =
395
396
  | { readonly ok: false; readonly alreadyReported: boolean };
396
397
 
397
398
  export function resolveFieldTypeDescriptor(input: {
398
- readonly field: PslField;
399
+ readonly field: FieldSymbol;
399
400
  readonly enumTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
400
401
  readonly namedTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
401
402
  readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
@@ -407,6 +408,10 @@ export function resolveFieldTypeDescriptor(input: {
407
408
  readonly sourceId: string;
408
409
  readonly entityLabel: string;
409
410
  }): ResolveFieldTypeResult {
411
+ // Avoid cascading unsupported-type diagnostics after invalid qualification.
412
+ if (input.field.malformedType) {
413
+ return { ok: false, alreadyReported: true };
414
+ }
410
415
  if (input.field.typeConstructor) {
411
416
  // Field presets carry richer semantics than type constructors, so a field preset match is the complete answer. Shared composition rejects exact cross-registry collisions before PSL resolution can observe them.
412
417
  const presetDescriptor = getAuthoringFieldPreset(
@@ -414,7 +419,7 @@ export function resolveFieldTypeDescriptor(input: {
414
419
  input.field.typeConstructor.path,
415
420
  );
416
421
  if (presetDescriptor) {
417
- const instantiated = instantiatePslFieldPreset({
422
+ const instantiated = instantiateFieldPreset({
418
423
  call: input.field.typeConstructor,
419
424
  descriptor: presetDescriptor,
420
425
  diagnostics: input.diagnostics,
@@ -550,7 +555,7 @@ export const NATIVE_TYPE_SPECS: Readonly<Record<string, NativeTypeSpec>> = {
550
555
  codecId: 'sql/char@1',
551
556
  nativeType: 'character',
552
557
  },
553
- 'db.Uuid': { args: 'noArgs', baseType: 'String', codecId: null, nativeType: 'uuid' },
558
+ 'db.Uuid': { args: 'noArgs', baseType: 'String', codecId: 'pg/uuid@1', nativeType: 'uuid' },
554
559
  'db.SmallInt': { args: 'noArgs', baseType: 'Int', codecId: 'pg/int2@1', nativeType: 'int2' },
555
560
  'db.Real': { args: 'noArgs', baseType: 'Float', codecId: 'pg/float4@1', nativeType: 'float4' },
556
561
  'db.Numeric': {
@@ -588,7 +593,7 @@ export const NATIVE_TYPE_SPECS: Readonly<Record<string, NativeTypeSpec>> = {
588
593
  };
589
594
 
590
595
  export function resolveDbNativeTypeAttribute(input: {
591
- readonly attribute: PslAttribute;
596
+ readonly attribute: ResolvedAttribute;
592
597
  readonly baseType: string;
593
598
  readonly baseDescriptor: ColumnDescriptor;
594
599
  readonly diagnostics: ContractSourceDiagnostic[];
@@ -703,7 +708,7 @@ export function parseDefaultLiteralValue(expression: string): ColumnDefault | un
703
708
  export function lowerDefaultForField(input: {
704
709
  readonly modelName: string;
705
710
  readonly fieldName: string;
706
- readonly defaultAttribute: PslAttribute;
711
+ readonly defaultAttribute: ResolvedAttribute;
707
712
  readonly columnDescriptor: ColumnDescriptor;
708
713
  readonly generatorDescriptorById: ReadonlyMap<string, MutationDefaultGeneratorDescriptor>;
709
714
  readonly sourceId: string;
@@ -809,14 +814,11 @@ export function lowerDefaultForField(input: {
809
814
  }
810
815
 
811
816
  export function resolveColumnDescriptor(
812
- field: PslField,
817
+ field: FieldSymbol,
813
818
  enumTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
814
819
  namedTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
815
820
  scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>,
816
821
  ): ColumnDescriptor | undefined {
817
- if (field.typeRef && namedTypeDescriptors.has(field.typeRef)) {
818
- return namedTypeDescriptors.get(field.typeRef);
819
- }
820
822
  if (namedTypeDescriptors.has(field.typeName)) {
821
823
  return namedTypeDescriptors.get(field.typeName);
822
824
  }
@@ -1,11 +1,17 @@
1
1
  import type { ContractSourceDiagnostic } from '@prisma-next/config/config-types';
2
- import type { ColumnDefault, ExecutionMutationDefaultPhases } from '@prisma-next/contract/types';
2
+ import type {
3
+ ColumnDefault,
4
+ ColumnDefaultLiteralInputValue,
5
+ ExecutionMutationDefaultPhases,
6
+ } from '@prisma-next/contract/types';
3
7
  import type { AuthoringContributions } from '@prisma-next/framework-components/authoring';
4
8
  import type {
5
9
  ControlMutationDefaultRegistry,
6
10
  MutationDefaultGeneratorDescriptor,
7
11
  } from '@prisma-next/framework-components/control';
8
- import type { PslAttribute, PslField, PslModel } from '@prisma-next/psl-parser';
12
+ import type { FieldSymbol, ModelSymbol, ResolvedAttribute } from '@prisma-next/psl-parser';
13
+ import type { EnumTypeHandle } from '@prisma-next/sql-contract-ts/contract-builder';
14
+ import { blindCast } from '@prisma-next/utils/casts';
9
15
  import { ifDefined } from '@prisma-next/utils/defined';
10
16
  import {
11
17
  getAttribute,
@@ -21,10 +27,74 @@ import {
21
27
  resolveFieldTypeDescriptor,
22
28
  } from './psl-column-resolution';
23
29
 
30
+ type LoweredFieldDefault = {
31
+ readonly defaultValue?: ColumnDefault;
32
+ readonly executionDefaults?: ExecutionMutationDefaultPhases;
33
+ };
34
+
35
+ function lowerEnumDefaultForField(input: {
36
+ readonly modelName: string;
37
+ readonly fieldName: string;
38
+ readonly defaultAttribute: ResolvedAttribute;
39
+ readonly enumHandle: EnumTypeHandle;
40
+ readonly sourceId: string;
41
+ readonly diagnostics: ContractSourceDiagnostic[];
42
+ }): LoweredFieldDefault {
43
+ const positionalEntries = input.defaultAttribute.args.filter((arg) => arg.kind === 'positional');
44
+ const hasNamedEntries = input.defaultAttribute.args.some((arg) => arg.kind === 'named');
45
+ const expressionEntry = positionalEntries[0];
46
+ if (hasNamedEntries || positionalEntries.length !== 1 || expressionEntry === undefined) {
47
+ input.diagnostics.push({
48
+ code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT',
49
+ message: `Field "${input.modelName}.${input.fieldName}" @default on an enum field expects exactly one positional enum member argument.`,
50
+ sourceId: input.sourceId,
51
+ span: input.defaultAttribute.span,
52
+ });
53
+ return {};
54
+ }
55
+
56
+ const raw = expressionEntry.value.trim();
57
+ const isQuotedString = /^(['"]).*\1$/.test(raw);
58
+ const isFunctionCall = raw.includes('(') && raw.endsWith(')');
59
+
60
+ if (isQuotedString || isFunctionCall) {
61
+ input.diagnostics.push({
62
+ code: 'PSL_ENUM_DEFAULT_MUST_BE_MEMBER_NAME',
63
+ message: `Field "${input.modelName}.${input.fieldName}" @default on an enum field must name a member (e.g. @default(Low)), not a raw value or function.`,
64
+ sourceId: input.sourceId,
65
+ span: input.defaultAttribute.span,
66
+ });
67
+ return {};
68
+ }
69
+
70
+ const match = input.enumHandle.enumMembers.find((m) => m.name === raw);
71
+ if (!match) {
72
+ const validNames = input.enumHandle.enumMembers.map((m) => m.name).join(', ');
73
+ input.diagnostics.push({
74
+ code: 'PSL_ENUM_UNKNOWN_DEFAULT_MEMBER',
75
+ message: `Field "${input.modelName}.${input.fieldName}" @default(${raw}) does not name a member of ${input.enumHandle.enumName}. Valid members: ${validNames}.`,
76
+ sourceId: input.sourceId,
77
+ span: input.defaultAttribute.span,
78
+ });
79
+ return {};
80
+ }
81
+
82
+ return {
83
+ defaultValue: {
84
+ kind: 'literal',
85
+ value: blindCast<
86
+ ColumnDefaultLiteralInputValue,
87
+ 'enum member values are codec-validated JsonValue-compatible scalars'
88
+ >(match.value),
89
+ },
90
+ };
91
+ }
92
+
24
93
  export type ResolvedField = {
25
- readonly field: PslField;
94
+ readonly field: FieldSymbol;
26
95
  readonly columnName: string;
27
96
  readonly descriptor: ColumnDescriptor;
97
+ readonly nullable: boolean;
28
98
  readonly defaultValue?: ColumnDefault;
29
99
  readonly executionDefaults?: ExecutionMutationDefaultPhases;
30
100
  readonly isId: boolean;
@@ -37,7 +107,7 @@ export type ResolvedField = {
37
107
  };
38
108
 
39
109
  export type ModelNameMapping = {
40
- readonly model: PslModel;
110
+ readonly model: ModelSymbol;
41
111
  readonly tableName: string;
42
112
  readonly fieldColumns: Map<string, string>;
43
113
  };
@@ -50,7 +120,7 @@ export type ModelNameMapping = {
50
120
  * {@link modelCoordinateKey} rather than the bare model name.
51
121
  */
52
122
  export type ModelNamespaceEntry = {
53
- readonly model: PslModel;
123
+ readonly model: ModelSymbol;
54
124
  readonly namespaceId: string | undefined;
55
125
  };
56
126
 
@@ -61,7 +131,7 @@ export function modelCoordinateKey(namespaceId: string, modelName: string): stri
61
131
  }
62
132
 
63
133
  export interface CollectResolvedFieldsInput {
64
- readonly model: PslModel;
134
+ readonly model: ModelSymbol;
65
135
  readonly mapping: ModelNameMapping;
66
136
  readonly enumTypeDescriptors: Map<string, ColumnDescriptor>;
67
137
  readonly namedTypeDescriptors: Map<string, ColumnDescriptor>;
@@ -76,6 +146,7 @@ export interface CollectResolvedFieldsInput {
76
146
  readonly diagnostics: ContractSourceDiagnostic[];
77
147
  readonly sourceId: string;
78
148
  readonly scalarTypeDescriptors: ReadonlyMap<string, ColumnDescriptor>;
149
+ readonly enumHandles?: ReadonlyMap<string, EnumTypeHandle>;
79
150
  }
80
151
 
81
152
  const BUILTIN_FIELD_ATTRIBUTE_NAMES: ReadonlySet<string> = new Set([
@@ -95,12 +166,12 @@ const BUILTIN_FIELD_ATTRIBUTE_NAMES: ReadonlySet<string> = new Set([
95
166
  * migrated (so they don't get told to do what they just did).
96
167
  *
97
168
  * Pairing the suppression predicate with the hint makes each entry
98
- * self-contained: a future entry for, say, `@id` ↔ `id.uuidv7()` cannot
169
+ * self-contained: a future entry for, say, `@id` ↔ `id.uuidv7String()` cannot
99
170
  * silently inherit the wrong predicate when added.
100
171
  */
101
172
  interface RemovedAttributeRule {
102
173
  readonly hint: string;
103
- readonly suppressWhen: (field: PslField) => boolean;
174
+ readonly suppressWhen: (field: FieldSymbol) => boolean;
104
175
  }
105
176
 
106
177
  const REMOVED_ATTRIBUTE_RULES: ReadonlyMap<string, RemovedAttributeRule> = new Map([
@@ -130,8 +201,8 @@ const REMOVED_ATTRIBUTE_RULES: ReadonlyMap<string, RemovedAttributeRule> = new M
130
201
  }
131
202
 
132
203
  function validateFieldAttributes(input: {
133
- readonly model: PslModel;
134
- readonly field: PslField;
204
+ readonly model: ModelSymbol;
205
+ readonly field: FieldSymbol;
135
206
  readonly composedExtensions: ReadonlySet<string>;
136
207
  readonly authoringContributions: AuthoringContributions | undefined;
137
208
  readonly diagnostics: ContractSourceDiagnostic[];
@@ -177,13 +248,13 @@ function validateFieldAttributes(input: {
177
248
  }
178
249
 
179
250
  function extractFieldConstraintNames(input: {
180
- readonly model: PslModel;
181
- readonly field: PslField;
251
+ readonly model: ModelSymbol;
252
+ readonly field: FieldSymbol;
182
253
  readonly sourceId: string;
183
254
  readonly diagnostics: ContractSourceDiagnostic[];
184
255
  }): {
185
- readonly idAttribute: PslAttribute | undefined;
186
- readonly uniqueAttribute: PslAttribute | undefined;
256
+ readonly idAttribute: ResolvedAttribute | undefined;
257
+ readonly uniqueAttribute: ResolvedAttribute | undefined;
187
258
  readonly idName: string | undefined;
188
259
  readonly uniqueName: string | undefined;
189
260
  } {
@@ -225,10 +296,11 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
225
296
  diagnostics,
226
297
  sourceId,
227
298
  scalarTypeDescriptors,
299
+ enumHandles,
228
300
  } = input;
229
301
  const resolvedFields: ResolvedField[] = [];
230
302
 
231
- for (const field of model.fields) {
303
+ for (const field of Object.values(model.fields)) {
232
304
  const isModelField = modelNames.has(field.typeName);
233
305
 
234
306
  if (field.list && isModelField) {
@@ -350,17 +422,27 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
350
422
  });
351
423
  continue;
352
424
  }
353
- const loweredDefault = defaultAttribute
354
- ? lowerDefaultForField({
355
- modelName: model.name,
356
- fieldName: field.name,
357
- defaultAttribute,
358
- columnDescriptor: descriptor,
359
- generatorDescriptorById,
360
- sourceId,
361
- defaultFunctionRegistry,
362
- diagnostics,
363
- })
425
+ const enumHandle = enumHandles?.get(field.typeName);
426
+ const loweredDefault: LoweredFieldDefault = defaultAttribute
427
+ ? enumHandle
428
+ ? lowerEnumDefaultForField({
429
+ modelName: model.name,
430
+ fieldName: field.name,
431
+ defaultAttribute,
432
+ enumHandle,
433
+ sourceId,
434
+ diagnostics,
435
+ })
436
+ : lowerDefaultForField({
437
+ modelName: model.name,
438
+ fieldName: field.name,
439
+ defaultAttribute,
440
+ columnDescriptor: descriptor,
441
+ generatorDescriptorById,
442
+ sourceId,
443
+ defaultFunctionRegistry,
444
+ diagnostics,
445
+ })
364
446
  : {};
365
447
  const loweredOnCreate = loweredDefault.executionDefaults?.onCreate;
366
448
  if (field.optional && loweredOnCreate) {
@@ -374,7 +456,8 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
374
456
  });
375
457
  continue;
376
458
  }
377
- if (loweredOnCreate) {
459
+ const fieldUsesNamedType = namedTypeDescriptors.has(field.typeName);
460
+ if (loweredOnCreate && !fieldUsesNamedType) {
378
461
  const generatorDescriptor = generatorDescriptorById.get(loweredOnCreate.id);
379
462
  const generatedDescriptor = generatorDescriptor?.resolveGeneratedColumnDescriptor?.({
380
463
  generated: loweredOnCreate,
@@ -426,6 +509,7 @@ export function collectResolvedFields(input: CollectResolvedFieldsInput): Resolv
426
509
  field,
427
510
  columnName: mappedColumnName,
428
511
  descriptor,
512
+ nullable: presetContributions?.nullable ?? field.optional,
429
513
  ...ifDefined('defaultValue', fieldDefaultValue),
430
514
  ...ifDefined('executionDefaults', fieldExecutionDefaults),
431
515
  isId: isIdField || Boolean(presetContributions?.id),
@@ -459,7 +543,7 @@ export function buildModelMappings(
459
543
  span: model.span,
460
544
  });
461
545
  const fieldColumns = new Map<string, string>();
462
- for (const field of model.fields) {
546
+ for (const field of Object.values(model.fields)) {
463
547
  const fieldMapAttribute = getAttribute(field.attributes, 'map');
464
548
  const columnName = parseMapName({
465
549
  attribute: fieldMapAttribute,