@prisma-next/family-sql 0.5.0-dev.27 → 0.5.0-dev.29

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,832 @@
1
+ import type { ColumnDefault } from '@prisma-next/contract/types';
2
+ import type {
3
+ PslAttribute,
4
+ PslAttributeArgument,
5
+ PslDocumentAst,
6
+ PslEnum,
7
+ PslField,
8
+ PslFieldAttribute,
9
+ PslModel,
10
+ PslModelAttribute,
11
+ PslNamedTypeDeclaration,
12
+ PslSpan,
13
+ PslTypesBlock,
14
+ } from '@prisma-next/framework-components/psl-ast';
15
+ import type { SqlColumnIR, SqlSchemaIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
16
+ import type { DefaultMappingOptions } from './default-mapping';
17
+ import { mapDefault } from './default-mapping';
18
+ import { toEnumName, toFieldName, toModelName, toNamedTypeName } from './name-transforms';
19
+ import { createPostgresDefaultMapping } from './postgres-default-mapping';
20
+ import { createPostgresTypeMap, extractEnumInfo } from './postgres-type-map';
21
+ import type {
22
+ EnumInfo,
23
+ PslNativeTypeAttribute,
24
+ PslPrinterOptions,
25
+ PslTypeMap,
26
+ RelationField,
27
+ } from './printer-config';
28
+ import { parseRawDefault } from './raw-default-parser';
29
+ import { inferRelations } from './relation-inference';
30
+
31
+ const SYNTHETIC_SPAN: PslSpan = {
32
+ start: { offset: 0, line: 1, column: 1 },
33
+ end: { offset: 0, line: 1, column: 1 },
34
+ };
35
+
36
+ const PSL_SCALAR_TYPE_NAMES = new Set([
37
+ 'String',
38
+ 'Boolean',
39
+ 'Int',
40
+ 'BigInt',
41
+ 'Float',
42
+ 'Decimal',
43
+ 'DateTime',
44
+ 'Json',
45
+ 'Bytes',
46
+ ]);
47
+
48
+ type ResolvedColumnFieldName = {
49
+ readonly fieldName: string;
50
+ readonly fieldMap?: string | undefined;
51
+ };
52
+
53
+ type TableColumnFieldNameMap = ReadonlyMap<string, ResolvedColumnFieldName>;
54
+
55
+ type NamedTypeRegistryEntry = {
56
+ readonly name: string;
57
+ readonly baseType: string;
58
+ readonly nativeTypeAttribute: PslNativeTypeAttribute;
59
+ };
60
+
61
+ type NamedTypeRegistry = {
62
+ readonly entriesByKey: Map<string, NamedTypeRegistryEntry>;
63
+ readonly usedNames: Set<string>;
64
+ };
65
+
66
+ type TopLevelNameResult = {
67
+ readonly name: string;
68
+ readonly map?: string | undefined;
69
+ };
70
+
71
+ /**
72
+ * Converts a SQL schema IR into a PSL AST suitable for `printPsl`.
73
+ *
74
+ * This function owns all SQL-specific concerns: native type mapping (Postgres),
75
+ * relation inference from foreign keys, enum extraction, and raw default parsing.
76
+ * The output is a fully-formed `PslDocumentAst` with synthetic spans.
77
+ */
78
+ export function sqlSchemaIrToPslAst(schemaIR: SqlSchemaIR): PslDocumentAst {
79
+ const enumInfo = extractEnumInfo(schemaIR.annotations);
80
+ const options: PslPrinterOptions = {
81
+ typeMap: createPostgresTypeMap(enumInfo.typeNames),
82
+ defaultMapping: createPostgresDefaultMapping(),
83
+ enumInfo,
84
+ parseRawDefault,
85
+ };
86
+
87
+ return buildPslDocumentAst(schemaIR, options);
88
+ }
89
+
90
+ function buildPslDocumentAst(schemaIR: SqlSchemaIR, options: PslPrinterOptions): PslDocumentAst {
91
+ const { typeMap, defaultMapping, enumInfo, parseRawDefault: rawDefaultParser } = options;
92
+ const emptyEnumInfo: EnumInfo = {
93
+ typeNames: new Set<string>(),
94
+ definitions: new Map<string, readonly string[]>(),
95
+ };
96
+ const { typeNames: enumTypeNames, definitions: enumDefinitions } = enumInfo ?? emptyEnumInfo;
97
+
98
+ const modelNames = buildTopLevelNameMap(
99
+ Object.keys(schemaIR.tables),
100
+ toModelName,
101
+ 'model',
102
+ 'table',
103
+ );
104
+ const enumNames = buildTopLevelNameMap(enumTypeNames, toEnumName, 'enum', 'enum type');
105
+ assertNoCrossKindNameCollisions(modelNames, enumNames);
106
+
107
+ const modelNameMap = new Map(
108
+ [...modelNames].map(([tableName, result]) => [tableName, result.name]),
109
+ );
110
+ const enumNameMap = new Map(
111
+ [...enumNames].map(([pgTypeName, result]) => [pgTypeName, result.name]),
112
+ );
113
+ const reservedNamedTypeNames = createReservedNamedTypeNames(modelNames, enumNames);
114
+
115
+ const fieldNamesByTable = buildFieldNamesByTable(schemaIR.tables);
116
+ const { relationsByTable } = inferRelations(schemaIR.tables, modelNameMap);
117
+ const namedTypes = seedNamedTypeRegistry(schemaIR, typeMap, enumNameMap, reservedNamedTypeNames);
118
+
119
+ const models: PslModel[] = [];
120
+ for (const table of Object.values(schemaIR.tables)) {
121
+ models.push(
122
+ buildModel(
123
+ table,
124
+ typeMap,
125
+ enumNameMap,
126
+ fieldNamesByTable,
127
+ namedTypes,
128
+ defaultMapping,
129
+ rawDefaultParser,
130
+ relationsByTable.get(table.name) ?? [],
131
+ ),
132
+ );
133
+ }
134
+
135
+ const sortedModels = topologicalSort(models, schemaIR.tables, modelNameMap);
136
+
137
+ const enums: PslEnum[] = [];
138
+ for (const [pgTypeName, values] of enumDefinitions) {
139
+ const enumName = enumNames.get(pgTypeName) as TopLevelNameResult;
140
+ enums.push(buildEnum(enumName, values));
141
+ }
142
+ enums.sort((a, b) => a.name.localeCompare(b.name));
143
+
144
+ const namedTypeEntries = [...namedTypes.entriesByKey.values()].sort((a, b) =>
145
+ a.name.localeCompare(b.name),
146
+ );
147
+ const types: PslTypesBlock | undefined =
148
+ namedTypeEntries.length > 0
149
+ ? {
150
+ kind: 'types',
151
+ declarations: namedTypeEntries.map(buildNamedTypeDeclaration),
152
+ span: SYNTHETIC_SPAN,
153
+ }
154
+ : undefined;
155
+
156
+ const ast: PslDocumentAst = {
157
+ kind: 'document',
158
+ sourceId: '<sql-schema-ir>',
159
+ models: sortedModels,
160
+ enums,
161
+ compositeTypes: [],
162
+ ...(types ? { types } : {}),
163
+ span: SYNTHETIC_SPAN,
164
+ };
165
+
166
+ return ast;
167
+ }
168
+
169
+ function buildModel(
170
+ table: SqlTableIR,
171
+ typeMap: PslTypeMap,
172
+ enumNameMap: ReadonlyMap<string, string>,
173
+ fieldNamesByTable: ReadonlyMap<string, TableColumnFieldNameMap>,
174
+ namedTypes: NamedTypeRegistry,
175
+ defaultMapping: DefaultMappingOptions | undefined,
176
+ rawDefaultParser: PslPrinterOptions['parseRawDefault'],
177
+ relationFields: readonly RelationField[],
178
+ ): PslModel {
179
+ const { name: modelName, map: mapName } = toModelName(table.name);
180
+ const fieldNameMap = fieldNamesByTable.get(table.name);
181
+
182
+ const pkColumns = new Set(table.primaryKey?.columns ?? []);
183
+ const isSinglePk = pkColumns.size === 1;
184
+ const singlePkConstraintName = isSinglePk ? table.primaryKey?.name : undefined;
185
+
186
+ const uniqueColumns = new Map<string, string | undefined>();
187
+ for (const unique of table.uniques) {
188
+ if (unique.columns.length === 1) {
189
+ const [columnName = ''] = unique.columns;
190
+ const existingConstraintName = uniqueColumns.get(columnName);
191
+ if (!uniqueColumns.has(columnName) || (existingConstraintName === undefined && unique.name)) {
192
+ uniqueColumns.set(columnName, unique.name);
193
+ }
194
+ }
195
+ }
196
+
197
+ const fields: PslField[] = [];
198
+ for (const column of Object.values(table.columns)) {
199
+ fields.push(
200
+ buildScalarField(
201
+ column,
202
+ table,
203
+ typeMap,
204
+ enumNameMap,
205
+ fieldNameMap,
206
+ namedTypes,
207
+ defaultMapping,
208
+ rawDefaultParser,
209
+ pkColumns,
210
+ isSinglePk,
211
+ singlePkConstraintName,
212
+ uniqueColumns,
213
+ ),
214
+ );
215
+ }
216
+
217
+ const usedFieldNames = new Set(fields.map((field) => field.name));
218
+ for (const rel of relationFields) {
219
+ fields.push(buildRelationField(rel, table.name, fieldNamesByTable, usedFieldNames));
220
+ }
221
+
222
+ const modelAttributes: PslModelAttribute[] = [];
223
+
224
+ if (table.primaryKey && table.primaryKey.columns.length > 1) {
225
+ const pkFieldNames = table.primaryKey.columns.map((columnName) =>
226
+ resolveColumnFieldName(fieldNamesByTable, table.name, columnName),
227
+ );
228
+ modelAttributes.push(buildModelConstraintAttribute('id', pkFieldNames, table.primaryKey.name));
229
+ }
230
+
231
+ for (const unique of table.uniques) {
232
+ if (unique.columns.length > 1) {
233
+ const uniqueFieldNames = unique.columns.map((columnName) =>
234
+ resolveColumnFieldName(fieldNamesByTable, table.name, columnName),
235
+ );
236
+ modelAttributes.push(buildModelConstraintAttribute('unique', uniqueFieldNames, unique.name));
237
+ }
238
+ }
239
+
240
+ for (const index of table.indexes) {
241
+ if (!index.unique) {
242
+ const indexFieldNames = index.columns.map((columnName) =>
243
+ resolveColumnFieldName(fieldNamesByTable, table.name, columnName),
244
+ );
245
+ modelAttributes.push(buildModelConstraintAttribute('index', indexFieldNames, index.name));
246
+ }
247
+ }
248
+
249
+ if (mapName) {
250
+ modelAttributes.push(buildMapAttribute('model', mapName));
251
+ }
252
+
253
+ // Surface introspection advisory: tables without a primary key cannot serve
254
+ // as the right-hand side of a `findUnique`-style query downstream, so the
255
+ // user should add an `@id` policy. This warning has shipped since
256
+ // `contract infer` was introduced and is part of the spec § A9 byte-identity
257
+ // contract for SQL output.
258
+ const comment = table.primaryKey
259
+ ? undefined
260
+ : '// WARNING: This table has no primary key in the database';
261
+
262
+ return {
263
+ kind: 'model',
264
+ name: modelName,
265
+ fields,
266
+ attributes: modelAttributes,
267
+ span: SYNTHETIC_SPAN,
268
+ ...(comment !== undefined ? { comment } : {}),
269
+ };
270
+ }
271
+
272
+ function buildScalarField(
273
+ column: SqlColumnIR,
274
+ table: SqlTableIR,
275
+ typeMap: PslTypeMap,
276
+ enumNameMap: ReadonlyMap<string, string>,
277
+ fieldNameMap: TableColumnFieldNameMap | undefined,
278
+ namedTypes: NamedTypeRegistry,
279
+ defaultMapping: DefaultMappingOptions | undefined,
280
+ rawDefaultParser: PslPrinterOptions['parseRawDefault'],
281
+ pkColumns: ReadonlySet<string>,
282
+ isSinglePk: boolean,
283
+ singlePkConstraintName: string | undefined,
284
+ uniqueColumns: ReadonlyMap<string, string | undefined>,
285
+ ): PslField {
286
+ const resolvedField = fieldNameMap?.get(column.name);
287
+ const fieldName = resolvedField?.fieldName ?? toFieldName(column.name).name;
288
+ const fieldMap = resolvedField?.fieldMap;
289
+
290
+ const resolution = typeMap.resolve(column.nativeType, table.annotations);
291
+
292
+ if ('unsupported' in resolution) {
293
+ const attrs: PslFieldAttribute[] = [];
294
+ if (fieldMap !== undefined) {
295
+ attrs.push(buildMapAttribute('field', fieldMap));
296
+ }
297
+ return {
298
+ kind: 'field',
299
+ name: fieldName,
300
+ typeName: `Unsupported("${escapePslString(resolution.nativeType)}")`,
301
+ optional: column.nullable,
302
+ list: false,
303
+ attributes: attrs,
304
+ span: SYNTHETIC_SPAN,
305
+ };
306
+ }
307
+
308
+ let typeName = resolution.pslType;
309
+ const enumPslName = enumNameMap.get(column.nativeType);
310
+ if (enumPslName) {
311
+ typeName = enumPslName;
312
+ }
313
+ if (resolution.nativeTypeAttribute && !enumPslName) {
314
+ typeName = resolveNamedTypeName(namedTypes, resolution);
315
+ }
316
+
317
+ const attributes: PslFieldAttribute[] = [];
318
+ const isId = isSinglePk && pkColumns.has(column.name);
319
+ if (isId) {
320
+ attributes.push(buildSimpleConstraintFieldAttribute('id', singlePkConstraintName));
321
+ }
322
+
323
+ if (column.default !== undefined) {
324
+ const parsed = parseColumnDefault(column.default, column.nativeType, rawDefaultParser);
325
+ if (parsed) {
326
+ const result = mapDefault(parsed, defaultMapping);
327
+ if ('attribute' in result) {
328
+ attributes.push(parseDefaultAttributeString(result.attribute));
329
+ }
330
+ // 'comment' fallback (unrecognized raw default) is dropped — the
331
+ // M1 legacy path emitted a `// Raw default: ...` line above the field via
332
+ // `PrinterField.comment`. M2 drops this since it would require comment
333
+ // nodes in the AST.
334
+ }
335
+ }
336
+
337
+ if (uniqueColumns.has(column.name) && !isId) {
338
+ const uniqueConstraintName = uniqueColumns.get(column.name);
339
+ attributes.push(buildSimpleConstraintFieldAttribute('unique', uniqueConstraintName));
340
+ }
341
+
342
+ if (fieldMap !== undefined) {
343
+ attributes.push(buildMapAttribute('field', fieldMap));
344
+ }
345
+
346
+ return {
347
+ kind: 'field',
348
+ name: fieldName,
349
+ typeName,
350
+ optional: column.nullable,
351
+ list: false,
352
+ attributes,
353
+ span: SYNTHETIC_SPAN,
354
+ };
355
+ }
356
+
357
+ function buildRelationField(
358
+ rel: RelationField,
359
+ hostTableName: string,
360
+ fieldNamesByTable: ReadonlyMap<string, TableColumnFieldNameMap>,
361
+ usedFieldNames: Set<string>,
362
+ ): PslField {
363
+ const fieldName = createUniqueFieldName(rel.fieldName, usedFieldNames);
364
+ usedFieldNames.add(fieldName);
365
+
366
+ const args: PslAttributeArgument[] = [];
367
+
368
+ if (rel.fields && rel.references) {
369
+ if (rel.relationName) {
370
+ args.push(namedArg('name', `"${escapePslString(rel.relationName)}"`));
371
+ }
372
+ args.push(
373
+ namedArg(
374
+ 'fields',
375
+ `[${rel.fields
376
+ .map((columnName) => resolveColumnFieldName(fieldNamesByTable, hostTableName, columnName))
377
+ .join(', ')}]`,
378
+ ),
379
+ );
380
+ args.push(
381
+ namedArg(
382
+ 'references',
383
+ `[${rel.references
384
+ .map((columnName) =>
385
+ resolveColumnFieldName(fieldNamesByTable, rel.referencedTableName ?? '', columnName),
386
+ )
387
+ .join(', ')}]`,
388
+ ),
389
+ );
390
+ if (rel.onDelete) {
391
+ args.push(namedArg('onDelete', rel.onDelete));
392
+ }
393
+ if (rel.onUpdate) {
394
+ args.push(namedArg('onUpdate', rel.onUpdate));
395
+ }
396
+ if (rel.fkName) {
397
+ args.push(namedArg('map', `"${escapePslString(rel.fkName)}"`));
398
+ }
399
+ } else if (rel.relationName) {
400
+ args.push(namedArg('name', `"${escapePslString(rel.relationName)}"`));
401
+ }
402
+
403
+ const attrs: PslFieldAttribute[] =
404
+ args.length > 0 ? [buildAttribute('field', 'relation', args)] : [];
405
+
406
+ return {
407
+ kind: 'field',
408
+ name: fieldName,
409
+ typeName: rel.typeName,
410
+ optional: rel.optional,
411
+ list: rel.list,
412
+ attributes: attrs,
413
+ span: SYNTHETIC_SPAN,
414
+ };
415
+ }
416
+
417
+ function buildModelConstraintAttribute(
418
+ name: 'id' | 'unique' | 'index',
419
+ fields: readonly string[],
420
+ constraintName?: string,
421
+ ): PslModelAttribute {
422
+ const args: PslAttributeArgument[] = [positionalArg(`[${fields.join(', ')}]`)];
423
+ if (constraintName !== undefined) {
424
+ args.push(namedArg('map', `"${escapePslString(constraintName)}"`));
425
+ }
426
+ return buildAttribute('model', name, args);
427
+ }
428
+
429
+ function buildSimpleConstraintFieldAttribute(
430
+ name: 'id' | 'unique',
431
+ constraintName: string | undefined,
432
+ ): PslFieldAttribute {
433
+ if (constraintName === undefined) {
434
+ return buildAttribute('field', name, []);
435
+ }
436
+ return buildAttribute('field', name, [namedArg('map', `"${escapePslString(constraintName)}"`)]);
437
+ }
438
+
439
+ function parseDefaultAttributeString(attributeText: string): PslFieldAttribute {
440
+ // Strip leading "@default(" and trailing ")" — `mapDefault` always returns one
441
+ // top-level positional expression.
442
+ const inner = attributeText.replace(/^@default\(/, '').replace(/\)$/, '');
443
+ return buildAttribute('field', 'default', [positionalArg(inner)]);
444
+ }
445
+
446
+ function buildMapAttribute(target: 'model' | 'field' | 'enum', mapName: string): PslAttribute {
447
+ return buildAttribute(target, 'map', [positionalArg(`"${escapePslString(mapName)}"`)]);
448
+ }
449
+
450
+ function buildAttribute(
451
+ target: PslAttribute['target'],
452
+ name: string,
453
+ args: readonly PslAttributeArgument[],
454
+ ): PslAttribute {
455
+ return {
456
+ kind: 'attribute',
457
+ target,
458
+ name,
459
+ args,
460
+ span: SYNTHETIC_SPAN,
461
+ };
462
+ }
463
+
464
+ function positionalArg(value: string): PslAttributeArgument {
465
+ return { kind: 'positional', value, span: SYNTHETIC_SPAN };
466
+ }
467
+
468
+ function namedArg(name: string, value: string): PslAttributeArgument {
469
+ return { kind: 'named', name, value, span: SYNTHETIC_SPAN };
470
+ }
471
+
472
+ function buildEnum(name: TopLevelNameResult, values: readonly string[]): PslEnum {
473
+ const attrs: PslAttribute[] = [];
474
+ if (name.map) {
475
+ attrs.push(buildMapAttribute('enum', name.map));
476
+ }
477
+ return {
478
+ kind: 'enum',
479
+ name: name.name,
480
+ values: values.map((value) => ({
481
+ kind: 'enumValue',
482
+ name: value,
483
+ span: SYNTHETIC_SPAN,
484
+ })),
485
+ attributes: attrs,
486
+ span: SYNTHETIC_SPAN,
487
+ };
488
+ }
489
+
490
+ function buildNamedTypeDeclaration(entry: NamedTypeRegistryEntry): PslNamedTypeDeclaration {
491
+ const attribute = buildAttribute(
492
+ 'namedType',
493
+ entry.nativeTypeAttribute.name,
494
+ (entry.nativeTypeAttribute.args ?? []).map(positionalArg),
495
+ );
496
+ return {
497
+ kind: 'namedType',
498
+ name: entry.name,
499
+ baseType: entry.baseType,
500
+ attributes: [attribute],
501
+ span: SYNTHETIC_SPAN,
502
+ };
503
+ }
504
+
505
+ function escapePslString(value: string): string {
506
+ return value
507
+ .replace(/\\/g, '\\\\')
508
+ .replace(/"/g, '\\"')
509
+ .replace(/\n/g, '\\n')
510
+ .replace(/\r/g, '\\r');
511
+ }
512
+
513
+ /**
514
+ * Resolves a `SqlColumnIR.default` value into a normalized {@link ColumnDefault}.
515
+ *
516
+ * `SqlSchemaIR` types the column default as `string` (a raw database default
517
+ * expression). Some legacy fixtures and tests still pass already-normalized
518
+ * `ColumnDefault` objects in the same slot, so we accept either shape
519
+ * defensively at runtime.
520
+ */
521
+ function parseColumnDefault(
522
+ value: unknown,
523
+ nativeType: string | undefined,
524
+ rawDefaultParser: PslPrinterOptions['parseRawDefault'],
525
+ ): ColumnDefault | undefined {
526
+ if (typeof value === 'string') {
527
+ return rawDefaultParser ? rawDefaultParser(value, nativeType) : undefined;
528
+ }
529
+ if (value !== null && typeof value === 'object' && 'kind' in (value as Record<string, unknown>)) {
530
+ return value as ColumnDefault;
531
+ }
532
+ return undefined;
533
+ }
534
+
535
+ function buildFieldNamesByTable(
536
+ tables: Record<string, SqlTableIR>,
537
+ ): ReadonlyMap<string, TableColumnFieldNameMap> {
538
+ const fieldNamesByTable = new Map<string, TableColumnFieldNameMap>();
539
+
540
+ for (const table of Object.values(tables)) {
541
+ const columns = Object.values(table.columns).map((column, index) => {
542
+ const { name, map } = toFieldName(column.name);
543
+ return {
544
+ columnName: column.name,
545
+ desiredFieldName: name,
546
+ fieldMap: map,
547
+ index,
548
+ };
549
+ });
550
+
551
+ const assignmentOrder = [...columns].sort((left, right) => {
552
+ const mapComparison =
553
+ Number(left.fieldMap !== undefined) - Number(right.fieldMap !== undefined);
554
+ if (mapComparison !== 0) {
555
+ return mapComparison;
556
+ }
557
+ return left.index - right.index;
558
+ });
559
+
560
+ const usedFieldNames = new Set<string>();
561
+ const tableFieldNames = new Map<string, ResolvedColumnFieldName>();
562
+
563
+ for (const column of assignmentOrder) {
564
+ const fieldName = createUniqueFieldName(column.desiredFieldName, usedFieldNames);
565
+ usedFieldNames.add(fieldName);
566
+ tableFieldNames.set(column.columnName, {
567
+ fieldName,
568
+ fieldMap: column.fieldMap,
569
+ });
570
+ }
571
+
572
+ fieldNamesByTable.set(table.name, tableFieldNames);
573
+ }
574
+
575
+ return fieldNamesByTable;
576
+ }
577
+
578
+ function resolveColumnFieldName(
579
+ fieldNamesByTable: ReadonlyMap<string, TableColumnFieldNameMap>,
580
+ tableName: string,
581
+ columnName: string,
582
+ ): string {
583
+ return (
584
+ fieldNamesByTable.get(tableName)?.get(columnName)?.fieldName ?? toFieldName(columnName).name
585
+ );
586
+ }
587
+
588
+ function createUniqueFieldName(desiredName: string, usedFieldNames: ReadonlySet<string>): string {
589
+ if (!usedFieldNames.has(desiredName)) {
590
+ return desiredName;
591
+ }
592
+
593
+ let counter = 2;
594
+ while (usedFieldNames.has(`${desiredName}${counter}`)) {
595
+ counter++;
596
+ }
597
+ return `${desiredName}${counter}`;
598
+ }
599
+
600
+ function buildTopLevelNameMap(
601
+ sources: Iterable<string>,
602
+ normalize: (source: string) => TopLevelNameResult,
603
+ kind: 'model' | 'enum',
604
+ sourceKind: 'table' | 'enum type',
605
+ ): Map<string, TopLevelNameResult> {
606
+ const results = new Map<string, TopLevelNameResult>();
607
+ const normalizedToSources = new Map<string, string[]>();
608
+
609
+ for (const source of sources) {
610
+ const normalized = normalize(source);
611
+ results.set(source, normalized);
612
+ normalizedToSources.set(normalized.name, [
613
+ ...(normalizedToSources.get(normalized.name) ?? []),
614
+ source,
615
+ ]);
616
+ }
617
+
618
+ const duplicates = [...normalizedToSources.entries()].filter(
619
+ ([, conflictingSources]) => conflictingSources.length > 1,
620
+ );
621
+ if (duplicates.length > 0) {
622
+ const details = duplicates.map(
623
+ ([normalizedName, conflictingSources]) =>
624
+ `- ${kind} "${normalizedName}" from ${sourceKind}s ${conflictingSources
625
+ .map((source) => `"${source}"`)
626
+ .join(', ')}`,
627
+ );
628
+ throw new Error(`PSL ${kind} name collisions detected:\n${details.join('\n')}`);
629
+ }
630
+
631
+ return results;
632
+ }
633
+
634
+ function assertNoCrossKindNameCollisions(
635
+ modelNames: ReadonlyMap<string, TopLevelNameResult>,
636
+ enumNames: ReadonlyMap<string, TopLevelNameResult>,
637
+ ): void {
638
+ const enumSourceByName = new Map([...enumNames].map(([source, result]) => [result.name, source]));
639
+
640
+ const collisions = [...modelNames.entries()]
641
+ .map(([tableName, result]) => {
642
+ const enumSource = enumSourceByName.get(result.name);
643
+ return enumSource
644
+ ? `- identifier "${result.name}" from table "${tableName}" collides with enum type "${enumSource}"`
645
+ : undefined;
646
+ })
647
+ .filter((detail): detail is string => detail !== undefined);
648
+
649
+ if (collisions.length > 0) {
650
+ throw new Error(`PSL top-level name collisions detected:\n${collisions.join('\n')}`);
651
+ }
652
+ }
653
+
654
+ function createReservedNamedTypeNames(
655
+ modelNames: ReadonlyMap<string, TopLevelNameResult>,
656
+ enumNames: ReadonlyMap<string, TopLevelNameResult>,
657
+ ): Set<string> {
658
+ const reservedNames = new Set<string>(PSL_SCALAR_TYPE_NAMES);
659
+
660
+ for (const result of modelNames.values()) {
661
+ reservedNames.add(result.name);
662
+ }
663
+
664
+ for (const result of enumNames.values()) {
665
+ reservedNames.add(result.name);
666
+ }
667
+
668
+ return reservedNames;
669
+ }
670
+
671
+ function seedNamedTypeRegistry(
672
+ schemaIR: SqlSchemaIR,
673
+ typeMap: PslTypeMap,
674
+ enumNameMap: ReadonlyMap<string, string>,
675
+ reservedNames: ReadonlySet<string>,
676
+ ): NamedTypeRegistry {
677
+ type Seed = {
678
+ readonly baseType: string;
679
+ readonly desiredName: string;
680
+ readonly nativeTypeAttribute: PslNativeTypeAttribute;
681
+ };
682
+
683
+ const seeds = new Map<string, Seed>();
684
+
685
+ for (const tableName of Object.keys(schemaIR.tables).sort()) {
686
+ const table = schemaIR.tables[tableName];
687
+ if (!table) {
688
+ continue;
689
+ }
690
+
691
+ for (const columnName of Object.keys(table.columns).sort()) {
692
+ const column = table.columns[columnName];
693
+ if (!column) {
694
+ continue;
695
+ }
696
+
697
+ const resolution = typeMap.resolve(column.nativeType, table.annotations);
698
+ if (
699
+ 'unsupported' in resolution ||
700
+ enumNameMap.has(column.nativeType) ||
701
+ !resolution.nativeTypeAttribute
702
+ ) {
703
+ continue;
704
+ }
705
+
706
+ const signatureKey = createNamedTypeSignatureKey(resolution);
707
+ if (!seeds.has(signatureKey)) {
708
+ seeds.set(signatureKey, {
709
+ baseType: resolution.pslType,
710
+ desiredName: toNamedTypeName(column.name),
711
+ nativeTypeAttribute: resolution.nativeTypeAttribute,
712
+ });
713
+ }
714
+ }
715
+ }
716
+
717
+ const registry: NamedTypeRegistry = {
718
+ entriesByKey: new Map<string, NamedTypeRegistryEntry>(),
719
+ usedNames: new Set<string>(reservedNames),
720
+ };
721
+
722
+ const sortedSeeds = [...seeds.entries()].sort((left, right) => {
723
+ const desiredNameComparison = left[1].desiredName.localeCompare(right[1].desiredName);
724
+ if (desiredNameComparison !== 0) {
725
+ return desiredNameComparison;
726
+ }
727
+ return left[0].localeCompare(right[0]);
728
+ });
729
+
730
+ for (const [signatureKey, seed] of sortedSeeds) {
731
+ const name = createUniqueFieldName(seed.desiredName, registry.usedNames);
732
+ registry.entriesByKey.set(signatureKey, {
733
+ name,
734
+ baseType: seed.baseType,
735
+ nativeTypeAttribute: seed.nativeTypeAttribute,
736
+ });
737
+ registry.usedNames.add(name);
738
+ }
739
+
740
+ return registry;
741
+ }
742
+
743
+ function resolveNamedTypeName(
744
+ registry: NamedTypeRegistry,
745
+ resolution: {
746
+ readonly pslType: string;
747
+ readonly nativeType: string;
748
+ readonly typeParams?: Record<string, unknown>;
749
+ readonly nativeTypeAttribute?: PslNativeTypeAttribute;
750
+ },
751
+ ): string {
752
+ const key = createNamedTypeSignatureKey(resolution);
753
+ const existing = registry.entriesByKey.get(key);
754
+ if (existing) {
755
+ return existing.name;
756
+ }
757
+
758
+ throw new Error(`Named type registry was not seeded for native type "${resolution.nativeType}"`);
759
+ }
760
+
761
+ function createNamedTypeSignatureKey(resolution: {
762
+ readonly pslType: string;
763
+ readonly nativeType: string;
764
+ readonly typeParams?: Record<string, unknown>;
765
+ readonly nativeTypeAttribute?: PslNativeTypeAttribute;
766
+ }): string {
767
+ return JSON.stringify({
768
+ baseType: resolution.pslType,
769
+ nativeTypeAttribute: resolution.nativeTypeAttribute
770
+ ? {
771
+ name: resolution.nativeTypeAttribute.name,
772
+ args: resolution.nativeTypeAttribute.args ?? null,
773
+ }
774
+ : null,
775
+ });
776
+ }
777
+
778
+ function topologicalSort(
779
+ models: PslModel[],
780
+ tables: Record<string, SqlTableIR>,
781
+ modelNameMap: ReadonlyMap<string, string>,
782
+ ): PslModel[] {
783
+ const modelByName = new Map<string, PslModel>();
784
+ for (const model of models) {
785
+ modelByName.set(model.name, model);
786
+ }
787
+
788
+ const deps = new Map<string, Set<string>>();
789
+ const tableToModel = new Map<string, string>();
790
+ for (const tableName of Object.keys(tables)) {
791
+ const modelName = modelNameMap.get(tableName) as string;
792
+ tableToModel.set(tableName, modelName);
793
+ deps.set(modelName, new Set());
794
+ }
795
+
796
+ for (const [tableName, table] of Object.entries(tables)) {
797
+ const modelName = tableToModel.get(tableName) as string;
798
+ for (const fk of table.foreignKeys) {
799
+ const refModelName = tableToModel.get(fk.referencedTable);
800
+ if (refModelName && refModelName !== modelName) {
801
+ (deps.get(modelName) as Set<string>).add(refModelName);
802
+ }
803
+ }
804
+ }
805
+
806
+ const result: PslModel[] = [];
807
+ const visited = new Set<string>();
808
+ const visiting = new Set<string>();
809
+
810
+ const sortedNames = [...deps.keys()].sort();
811
+
812
+ function visit(name: string): void {
813
+ if (visited.has(name)) return;
814
+ if (visiting.has(name)) return;
815
+ visiting.add(name);
816
+
817
+ const sortedDeps = [...(deps.get(name) as Set<string>)].sort();
818
+ for (const dep of sortedDeps) {
819
+ visit(dep);
820
+ }
821
+
822
+ visiting.delete(name);
823
+ visited.add(name);
824
+ result.push(modelByName.get(name) as PslModel);
825
+ }
826
+
827
+ for (const name of sortedNames) {
828
+ visit(name);
829
+ }
830
+
831
+ return result;
832
+ }