ormbridge 0.3.0 → 0.5.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.
Files changed (58) hide show
  1. package/README.md +239 -37
  2. package/dist/cliRunner.js +14 -1
  3. package/dist/cliRunner.js.map +1 -1
  4. package/dist/config.d.ts +3 -0
  5. package/dist/config.js +9 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/emitters/drizzle.d.ts +19 -0
  8. package/dist/emitters/drizzle.js +1337 -0
  9. package/dist/emitters/drizzle.js.map +1 -0
  10. package/dist/emitters/jsonSchema.d.ts +20 -0
  11. package/dist/emitters/jsonSchema.js +503 -0
  12. package/dist/emitters/jsonSchema.js.map +1 -0
  13. package/dist/emitters/sqlDdl.d.ts +29 -0
  14. package/dist/emitters/sqlDdl.js +1741 -0
  15. package/dist/emitters/sqlDdl.js.map +1 -0
  16. package/dist/emitters/sqlalchemy.d.ts +30 -0
  17. package/dist/emitters/sqlalchemy.js +1725 -0
  18. package/dist/emitters/sqlalchemy.js.map +1 -0
  19. package/dist/emitters/typeorm.js +53 -1
  20. package/dist/emitters/typeorm.js.map +1 -1
  21. package/dist/emitters/zod.d.ts +40 -0
  22. package/dist/emitters/zod.js +498 -0
  23. package/dist/emitters/zod.js.map +1 -0
  24. package/dist/formats.d.ts +11 -1
  25. package/dist/formats.js +84 -0
  26. package/dist/formats.js.map +1 -1
  27. package/dist/index.d.ts +18 -0
  28. package/dist/index.js +9 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/io.js +82 -8
  31. package/dist/io.js.map +1 -1
  32. package/dist/parsers/drizzle.d.ts +12 -0
  33. package/dist/parsers/drizzle.js +2040 -0
  34. package/dist/parsers/drizzle.js.map +1 -0
  35. package/dist/parsers/jsonSchema.d.ts +21 -0
  36. package/dist/parsers/jsonSchema.js +1432 -0
  37. package/dist/parsers/jsonSchema.js.map +1 -0
  38. package/dist/parsers/pythonSyntax.d.ts +7 -1
  39. package/dist/parsers/pythonSyntax.js +47 -8
  40. package/dist/parsers/pythonSyntax.js.map +1 -1
  41. package/dist/parsers/sqlDdl.d.ts +18 -0
  42. package/dist/parsers/sqlDdl.js +2302 -0
  43. package/dist/parsers/sqlDdl.js.map +1 -0
  44. package/dist/parsers/sqlSyntax.d.ts +79 -0
  45. package/dist/parsers/sqlSyntax.js +698 -0
  46. package/dist/parsers/sqlSyntax.js.map +1 -0
  47. package/dist/parsers/sqlTypes.d.ts +53 -0
  48. package/dist/parsers/sqlTypes.js +742 -0
  49. package/dist/parsers/sqlTypes.js.map +1 -0
  50. package/dist/parsers/sqlalchemy.d.ts +12 -0
  51. package/dist/parsers/sqlalchemy.js +3191 -0
  52. package/dist/parsers/sqlalchemy.js.map +1 -0
  53. package/dist/parsers/typeorm.js +22 -7
  54. package/dist/parsers/typeorm.js.map +1 -1
  55. package/dist/prismaOnlyConstructs.d.ts +2 -2
  56. package/dist/prismaOnlyConstructs.js +5 -5
  57. package/dist/prismaOnlyConstructs.js.map +1 -1
  58. package/package.json +2 -1
@@ -0,0 +1,2040 @@
1
+ import { basename } from 'node:path';
2
+ import { singularize, toCamelCase, toPascalCase, toSnakeCase, } from '../naming.js';
3
+ import { err, ok } from '../result.js';
4
+ import { evaluateNode, getTypeScriptParser, } from './typescriptSyntax.js';
5
+ /** Table builders that start a table definition, by dialect. */
6
+ const TABLE_FUNCTIONS = {
7
+ pgTable: 'pg',
8
+ mysqlTable: 'mysql',
9
+ sqliteTable: 'sqlite',
10
+ };
11
+ /** Table creators (`pgTableCreator((name) => ...)`), by dialect. */
12
+ const TABLE_CREATORS = {
13
+ pgTableCreator: 'pg',
14
+ mysqlTableCreator: 'mysql',
15
+ sqliteTableCreator: 'sqlite',
16
+ };
17
+ /** Schema builders (`pgSchema('auth')`), by dialect. */
18
+ const SCHEMA_FUNCTIONS = {
19
+ pgSchema: 'pg',
20
+ mysqlSchema: 'mysql',
21
+ };
22
+ /** Table builders of Drizzle dialects this parser does not read. */
23
+ const UNSUPPORTED_DIALECT_TABLES = {
24
+ singlestoreTable: 'SingleStore',
25
+ gelTable: 'Gel',
26
+ mssqlTable: 'SQL Server',
27
+ cockroachTable: 'CockroachDB',
28
+ };
29
+ /** Top-level definitions that exist in the database but have no equivalent in the shared model. */
30
+ const UNSUPPORTED_DEFINITIONS = {
31
+ pgView: 'views',
32
+ pgMaterializedView: 'materialized views',
33
+ mysqlView: 'views',
34
+ sqliteView: 'views',
35
+ pgSequence: 'sequences',
36
+ pgRole: 'roles',
37
+ pgPolicy: 'row level security policies',
38
+ };
39
+ /** PostgreSQL builders that map straight to one IR type. */
40
+ const PG_SIMPLE_TYPES = {
41
+ serial: 'int',
42
+ smallserial: 'int',
43
+ bigserial: 'bigInt',
44
+ integer: 'int',
45
+ smallint: 'int',
46
+ bigint: 'bigInt',
47
+ real: 'float',
48
+ doublePrecision: 'float',
49
+ boolean: 'boolean',
50
+ text: 'text',
51
+ uuid: 'uuid',
52
+ json: 'json',
53
+ jsonb: 'json',
54
+ date: 'date',
55
+ time: 'time',
56
+ timestamp: 'dateTime',
57
+ interval: 'duration',
58
+ inet: 'ipAddress',
59
+ bytea: 'bytes',
60
+ };
61
+ /** PostgreSQL builders that are auto-incrementing, implicitly NOT NULL columns. */
62
+ const PG_SERIAL_TYPES = new Set([
63
+ 'serial',
64
+ 'smallserial',
65
+ 'bigserial',
66
+ ]);
67
+ /** PostgreSQL builders with no IR type, mapped to the SQL type they create. */
68
+ const PG_UNSUPPORTED_TYPES = {
69
+ point: 'point',
70
+ line: 'line',
71
+ macaddr: 'macaddr',
72
+ macaddr8: 'macaddr8',
73
+ cidr: 'cidr',
74
+ geometry: 'geometry',
75
+ vector: 'vector',
76
+ halfvec: 'halfvec',
77
+ sparsevec: 'sparsevec',
78
+ bit: 'bit',
79
+ };
80
+ const MYSQL_SIMPLE_TYPES = {
81
+ int: 'int',
82
+ tinyint: 'int',
83
+ smallint: 'int',
84
+ mediumint: 'int',
85
+ bigint: 'bigInt',
86
+ float: 'float',
87
+ double: 'float',
88
+ real: 'float',
89
+ boolean: 'boolean',
90
+ text: 'text',
91
+ tinytext: 'text',
92
+ mediumtext: 'text',
93
+ longtext: 'text',
94
+ date: 'date',
95
+ datetime: 'dateTime',
96
+ timestamp: 'dateTime',
97
+ time: 'time',
98
+ year: 'int',
99
+ json: 'json',
100
+ };
101
+ const SQLITE_SIMPLE_TYPES = {
102
+ real: 'float',
103
+ };
104
+ const ON_ACTION_MAP = {
105
+ cascade: 'cascade',
106
+ 'set null': 'setNull',
107
+ restrict: 'restrict',
108
+ 'no action': 'noAction',
109
+ 'set default': 'setDefault',
110
+ };
111
+ /** Index access methods Prisma knows, spelled the way the IR stores them. */
112
+ const INDEX_METHODS = {
113
+ btree: 'BTree',
114
+ hash: 'Hash',
115
+ gin: 'Gin',
116
+ gist: 'Gist',
117
+ spgist: 'SpGist',
118
+ brin: 'BRIN',
119
+ };
120
+ const NOW_SQL = /^\(?(now\(\)|current_timestamp(\(\d*\))?|localtimestamp(\(\d*\))?|transaction_timestamp\(\)|unixepoch\(\)|datetime\('now'\)|strftime\(.*'now'.*\)|\(unixepoch\(\)\)|\(datetime\('now'\)\))\)?$/i;
121
+ const UUID_SQL = /^(uuid_generate_v[14]\(\)|gen_random_uuid\(\)|uuid\(\)|newid\(\))$/i;
122
+ const QUOTED_SQL = /^'((?:[^']|'')*)'(::[\w\s]+)?$/;
123
+ /** Wrappers that change nothing about the value an expression evaluates to. */
124
+ const TRANSPARENT_NODES = new Set([
125
+ 'parenthesized_expression',
126
+ 'as_expression',
127
+ 'satisfies_expression',
128
+ 'non_null_expression',
129
+ 'type_assertion',
130
+ ]);
131
+ // ---------------------------------------------------------------------------
132
+ // Public entry point
133
+ // ---------------------------------------------------------------------------
134
+ /** Parses Drizzle ORM schema files into the shared IR using tree-sitter (no Node project required). */
135
+ export async function parseDrizzle(sources, options) {
136
+ const parserResult = await getTypeScriptParser();
137
+ if (!parserResult.ok) {
138
+ return parserResult;
139
+ }
140
+ const parser = parserResult.value;
141
+ const project = {
142
+ files: [],
143
+ declarators: [],
144
+ schemas: new Map(),
145
+ creators: new Map(),
146
+ helpers: new Map(),
147
+ enums: new Map(),
148
+ tables: [],
149
+ relationDefs: [],
150
+ warnings: [],
151
+ };
152
+ // The trees are kept alive until the schema is built: the syntax nodes below point into them.
153
+ const trees = [];
154
+ for (const source of sources) {
155
+ const tree = parser.parse(source.text);
156
+ trees.push(tree);
157
+ const file = describeFile(source.path, tree.rootNode);
158
+ project.files.push(file);
159
+ if (tree.rootNode.hasError) {
160
+ project.warnings.push(`${file.label}: the file contains TypeScript syntax errors; some tables or columns may be missing from the output.`);
161
+ }
162
+ project.declarators.push(...topLevelDeclarators(file, tree.rootNode));
163
+ }
164
+ collectDeclarations(project);
165
+ const schema = buildSchema(project, options);
166
+ if (schema.models.length === 0) {
167
+ const checkedPaths = sources
168
+ .map((source) => source.path)
169
+ .join(', ');
170
+ return err('NO_MODELS_FOUND', `No Drizzle tables were found in: ${checkedPaths}. A table is a top-level \`export const users = pgTable('users', { ... })\` ` +
171
+ `(or mysqlTable, sqliteTable, or a table of a pgSchema/mysqlSchema); relations and enums alone do not make a schema.`);
172
+ }
173
+ return ok(schema);
174
+ }
175
+ // ---------------------------------------------------------------------------
176
+ // Syntax helpers
177
+ // ---------------------------------------------------------------------------
178
+ function describeFile(path, root) {
179
+ const aliases = new Map();
180
+ const namespaces = new Set();
181
+ for (const statement of root.namedChildren) {
182
+ if (statement.type !== 'import_statement') {
183
+ continue;
184
+ }
185
+ const stack = [...statement.namedChildren];
186
+ while (stack.length > 0) {
187
+ const node = stack.pop();
188
+ if (node === undefined) {
189
+ break;
190
+ }
191
+ if (node.type === 'import_specifier') {
192
+ const nameNode = node.childForFieldName('name');
193
+ const aliasNode = node.childForFieldName('alias');
194
+ if (nameNode !== null && aliasNode !== null) {
195
+ aliases.set(aliasNode.text, nameNode.text);
196
+ }
197
+ }
198
+ else if (node.type === 'namespace_import') {
199
+ const nameNode = node.namedChildren[0];
200
+ if (nameNode !== undefined) {
201
+ namespaces.add(nameNode.text);
202
+ }
203
+ }
204
+ else {
205
+ stack.push(...node.namedChildren);
206
+ }
207
+ }
208
+ }
209
+ return { path, label: basename(path), aliases, namespaces };
210
+ }
211
+ /** The variable declarators at the top level of a file, including exported ones. */
212
+ function topLevelDeclarators(file, root) {
213
+ const declarators = [];
214
+ for (const statement of root.namedChildren) {
215
+ const declaration = statement.type === 'export_statement'
216
+ ? (statement.childForFieldName('declaration') ?? statement)
217
+ : statement;
218
+ if (declaration.type !== 'lexical_declaration' &&
219
+ declaration.type !== 'variable_declaration') {
220
+ continue;
221
+ }
222
+ for (const declarator of declaration.namedChildren) {
223
+ if (declarator.type !== 'variable_declarator') {
224
+ continue;
225
+ }
226
+ const nameNode = declarator.childForFieldName('name');
227
+ const valueNode = declarator.childForFieldName('value');
228
+ if (nameNode !== null && valueNode !== null) {
229
+ declarators.push({ file, name: nameNode.text, node: valueNode });
230
+ }
231
+ }
232
+ }
233
+ return declarators;
234
+ }
235
+ function stripWrappers(node) {
236
+ let current = node;
237
+ while (TRANSPARENT_NODES.has(current.type)) {
238
+ const inner = current.namedChildren[0];
239
+ if (inner === undefined) {
240
+ break;
241
+ }
242
+ current = inner;
243
+ }
244
+ return current;
245
+ }
246
+ function isFunctionNode(node) {
247
+ return (node.type === 'arrow_function' ||
248
+ node.type === 'function_expression' ||
249
+ node.type === 'function');
250
+ }
251
+ /** The expression a function returns (the body of an arrow, or the sole `return` of a block). */
252
+ function functionResult(fn) {
253
+ const body = fn.childForFieldName('body');
254
+ if (body === null) {
255
+ return undefined;
256
+ }
257
+ if (body.type === 'statement_block') {
258
+ const returned = body.namedChildren.find((statement) => statement.type === 'return_statement');
259
+ return returned?.namedChildren.find((child) => child.type !== 'comment');
260
+ }
261
+ return stripWrappers(body);
262
+ }
263
+ /** The name of the first parameter of a function (`t` in `(t) => [...]`). */
264
+ function functionParameter(fn) {
265
+ const single = fn.childForFieldName('parameter');
266
+ if (single !== null) {
267
+ return single.text;
268
+ }
269
+ const parameters = fn.childForFieldName('parameters');
270
+ const first = parameters?.namedChildren[0];
271
+ if (first === undefined) {
272
+ return undefined;
273
+ }
274
+ const pattern = first.childForFieldName('pattern');
275
+ return (pattern ?? first).text;
276
+ }
277
+ function argumentsOf(call) {
278
+ const argumentsNode = call.childForFieldName('arguments');
279
+ if (argumentsNode === null) {
280
+ return [];
281
+ }
282
+ // A tagged template (sql`now()`) has the template itself as its arguments node.
283
+ if (argumentsNode.type === 'template_string') {
284
+ return [argumentsNode];
285
+ }
286
+ return argumentsNode.namedChildren.filter((child) => child.type !== 'comment');
287
+ }
288
+ /**
289
+ * Splits `a('x').b().c()` into the first call and the calls chained on it. Returns undefined when the
290
+ * expression is not a call chain at all.
291
+ */
292
+ function unwindChain(node) {
293
+ const links = [];
294
+ let current = stripWrappers(node);
295
+ for (;;) {
296
+ if (current.type !== 'call_expression') {
297
+ return undefined;
298
+ }
299
+ const functionNode = current.childForFieldName('function');
300
+ if (functionNode === null) {
301
+ return undefined;
302
+ }
303
+ const args = argumentsOf(current);
304
+ if (functionNode.type === 'member_expression') {
305
+ const objectNode = functionNode.childForFieldName('object');
306
+ const propertyNode = functionNode.childForFieldName('property');
307
+ const inner = objectNode === null ? null : stripWrappers(objectNode);
308
+ if (inner !== null &&
309
+ propertyNode !== null &&
310
+ inner.type === 'call_expression' &&
311
+ !isTaggedTemplate(inner)) {
312
+ links.unshift({ name: propertyNode.text, args });
313
+ current = inner;
314
+ continue;
315
+ }
316
+ }
317
+ return {
318
+ baseCallee: functionNode.text.replace(/\s+/g, ''),
319
+ baseArgs: args,
320
+ links,
321
+ };
322
+ }
323
+ }
324
+ function isTaggedTemplate(call) {
325
+ return call.childForFieldName('arguments')?.type === 'template_string';
326
+ }
327
+ /** The text of an `sql\`...\`` template (or `sql.raw('...')`), or undefined when it is not one. */
328
+ function sqlText(node) {
329
+ const call = stripWrappers(node);
330
+ if (call.type !== 'call_expression') {
331
+ return undefined;
332
+ }
333
+ const functionNode = call.childForFieldName('function');
334
+ const argumentsNode = call.childForFieldName('arguments');
335
+ if (functionNode === null || argumentsNode === null) {
336
+ return undefined;
337
+ }
338
+ if (functionNode.text === 'sql' && argumentsNode.type === 'template_string') {
339
+ return {
340
+ text: argumentsNode.text.slice(1, -1),
341
+ isDynamic: argumentsNode.namedChildren.some((child) => child.type === 'template_substitution'),
342
+ };
343
+ }
344
+ if (functionNode.text === 'sql.raw') {
345
+ const value = argumentsOf(call)
346
+ .map(evaluateNode)
347
+ .find((candidate) => candidate.kind === 'string');
348
+ if (value !== undefined && value.kind === 'string') {
349
+ return { text: value.value, isDynamic: false };
350
+ }
351
+ }
352
+ return undefined;
353
+ }
354
+ /** Reads `table.column` (with chained `.desc()`, `.asc()`, `.op('...')`) as a column reference. */
355
+ function parseColumnRef(node) {
356
+ let current = stripWrappers(node);
357
+ let sort;
358
+ let ops;
359
+ while (current.type === 'call_expression') {
360
+ const functionNode = current.childForFieldName('function');
361
+ if (functionNode === null || functionNode.type !== 'member_expression') {
362
+ return undefined;
363
+ }
364
+ const propertyName = functionNode.childForFieldName('property')?.text ?? '';
365
+ if (propertyName === 'desc' || propertyName === 'asc') {
366
+ sort = propertyName;
367
+ }
368
+ else if (propertyName === 'op') {
369
+ const operatorClass = argumentsOf(current)
370
+ .map(evaluateNode)
371
+ .find((candidate) => candidate.kind === 'string');
372
+ if (operatorClass !== undefined && operatorClass.kind === 'string') {
373
+ ops = `raw("${operatorClass.value}")`;
374
+ }
375
+ }
376
+ const objectNode = functionNode.childForFieldName('object');
377
+ if (objectNode === null) {
378
+ return undefined;
379
+ }
380
+ current = stripWrappers(objectNode);
381
+ }
382
+ if (current.type !== 'member_expression') {
383
+ return undefined;
384
+ }
385
+ const parts = current.text.replace(/\s+/g, '').split('.');
386
+ const key = parts[parts.length - 1];
387
+ if (parts.length < 2 || key === undefined || key === '') {
388
+ return undefined;
389
+ }
390
+ return {
391
+ owner: parts[parts.length - 2],
392
+ key,
393
+ ...(sort === undefined ? {} : { sort }),
394
+ ...(ops === undefined ? {} : { ops }),
395
+ };
396
+ }
397
+ function columnRefs(node) {
398
+ if (node === undefined) {
399
+ return undefined;
400
+ }
401
+ const array = stripWrappers(node);
402
+ if (array.type !== 'array') {
403
+ return undefined;
404
+ }
405
+ const refs = [];
406
+ for (const item of array.namedChildren) {
407
+ if (item.type === 'comment') {
408
+ continue;
409
+ }
410
+ const ref = parseColumnRef(item);
411
+ if (ref === undefined) {
412
+ return undefined;
413
+ }
414
+ refs.push(ref);
415
+ }
416
+ return refs;
417
+ }
418
+ /** The value node of a property of an object literal, or undefined. */
419
+ function objectProperty(node, name) {
420
+ if (node === undefined) {
421
+ return undefined;
422
+ }
423
+ const object = stripWrappers(node);
424
+ if (object.type !== 'object') {
425
+ return undefined;
426
+ }
427
+ for (const child of object.namedChildren) {
428
+ if (child.type !== 'pair') {
429
+ continue;
430
+ }
431
+ const keyNode = child.childForFieldName('key');
432
+ if (keyNode !== null && unquote(keyNode.text) === name) {
433
+ return child.childForFieldName('value') ?? undefined;
434
+ }
435
+ }
436
+ return undefined;
437
+ }
438
+ function unquote(text) {
439
+ return /^(["'`]).*\1$/s.test(text) ? text.slice(1, -1) : text;
440
+ }
441
+ function stringValue(node) {
442
+ if (node === undefined) {
443
+ return undefined;
444
+ }
445
+ const value = evaluateNode(node);
446
+ return value.kind === 'string' ? value.value : undefined;
447
+ }
448
+ function objectValue(node) {
449
+ if (node === undefined) {
450
+ return undefined;
451
+ }
452
+ const value = evaluateNode(node);
453
+ return value.kind === 'object' ? value : undefined;
454
+ }
455
+ function stringOption(options, key) {
456
+ const value = options?.properties[key];
457
+ return value !== undefined && value.kind === 'string'
458
+ ? value.value
459
+ : undefined;
460
+ }
461
+ function numberOption(options, key) {
462
+ const value = options?.properties[key];
463
+ return value !== undefined && value.kind === 'number'
464
+ ? value.value
465
+ : undefined;
466
+ }
467
+ function boolOption(options, key) {
468
+ const value = options?.properties[key];
469
+ return value !== undefined && value.kind === 'bool' ? value.value : undefined;
470
+ }
471
+ /** Local import aliases resolved to the imported name (`table` -> `pgTable`). */
472
+ function canonicalName(file, name) {
473
+ return file.aliases.get(name) ?? name;
474
+ }
475
+ /** Splits a callee into an optional object (`mySchema`) and a name (`table`), resolving import aliases. */
476
+ function parseCallee(file, callee) {
477
+ const parts = callee.split('.');
478
+ const name = canonicalName(file, parts[parts.length - 1] ?? callee);
479
+ if (parts.length === 1) {
480
+ return { object: undefined, name };
481
+ }
482
+ const object = parts.slice(0, -1).join('.');
483
+ return file.namespaces.has(object)
484
+ ? { object: undefined, name }
485
+ : { object: canonicalName(file, object), name };
486
+ }
487
+ /** Evaluates a value that may be written inline or kept in a top-level constant. */
488
+ function resolveValue(node, project) {
489
+ const value = evaluateNode(node);
490
+ if (value.kind === 'name') {
491
+ const helper = project.helpers.get(value.value);
492
+ if (helper !== undefined) {
493
+ return evaluateNode(helper);
494
+ }
495
+ }
496
+ return value;
497
+ }
498
+ // ---------------------------------------------------------------------------
499
+ // Declarations (first pass over every file)
500
+ // ---------------------------------------------------------------------------
501
+ /** Template literal `prefix_${name}` as a function of the name, or undefined when it has another shape. */
502
+ function creatorTransform(node) {
503
+ if (node === undefined) {
504
+ return undefined;
505
+ }
506
+ const fn = stripWrappers(node);
507
+ if (!isFunctionNode(fn)) {
508
+ return undefined;
509
+ }
510
+ const parameter = functionParameter(fn);
511
+ const result = functionResult(fn);
512
+ if (parameter === undefined || result === undefined) {
513
+ return undefined;
514
+ }
515
+ if (result.type === 'identifier' && result.text === parameter) {
516
+ return (name) => name;
517
+ }
518
+ if (result.type !== 'template_string') {
519
+ return undefined;
520
+ }
521
+ let prefix = '';
522
+ let suffix = '';
523
+ let seenName = false;
524
+ for (const part of result.namedChildren) {
525
+ if (part.type === 'template_substitution') {
526
+ const expression = part.namedChildren[0];
527
+ if (seenName ||
528
+ expression === undefined ||
529
+ expression.text !== parameter) {
530
+ return undefined;
531
+ }
532
+ seenName = true;
533
+ }
534
+ else if (seenName) {
535
+ suffix += part.text;
536
+ }
537
+ else {
538
+ prefix += part.text;
539
+ }
540
+ }
541
+ return seenName
542
+ ? (name) => `${prefix}${name}${suffix}`
543
+ : undefined;
544
+ }
545
+ /** Sorts the top-level declarations into schemas, creators, enums, tables, relations and helpers. */
546
+ function collectDeclarations(project) {
547
+ // Schemas, creators and helpers first: tables and enums can refer to them from any file.
548
+ for (const declarator of project.declarators) {
549
+ const node = stripWrappers(declarator.node);
550
+ if (node.type === 'object') {
551
+ project.helpers.set(declarator.name, node);
552
+ continue;
553
+ }
554
+ if (node.type === 'array') {
555
+ project.helpers.set(declarator.name, node);
556
+ continue;
557
+ }
558
+ if (isFunctionNode(node)) {
559
+ const result = functionResult(node);
560
+ if (result !== undefined) {
561
+ project.helpers.set(declarator.name, result);
562
+ }
563
+ continue;
564
+ }
565
+ const chain = unwindChain(node);
566
+ if (chain === undefined) {
567
+ continue;
568
+ }
569
+ const callee = parseCallee(declarator.file, chain.baseCallee);
570
+ if (callee.object !== undefined) {
571
+ continue;
572
+ }
573
+ const schemaDialect = SCHEMA_FUNCTIONS[callee.name];
574
+ if (schemaDialect !== undefined) {
575
+ const dbName = stringValue(chain.baseArgs[0]);
576
+ if (dbName === undefined) {
577
+ project.warnings.push(`${declarator.file.label}: the schema "${declarator.name}" has a name that is not a string literal; its tables were read without a schema.`);
578
+ }
579
+ else {
580
+ project.schemas.set(declarator.name, {
581
+ dbName,
582
+ dialect: schemaDialect,
583
+ });
584
+ }
585
+ continue;
586
+ }
587
+ const creatorDialect = TABLE_CREATORS[callee.name];
588
+ if (creatorDialect !== undefined) {
589
+ const transform = creatorTransform(chain.baseArgs[0]);
590
+ if (transform === undefined) {
591
+ project.warnings.push(`${declarator.file.label}: the table creator "${declarator.name}" does not map names with a plain template such as \`app_\${name}\`; ` +
592
+ `the tables made with it keep the name they were given, without the prefix.`);
593
+ }
594
+ project.creators.set(declarator.name, {
595
+ dialect: creatorDialect,
596
+ transform,
597
+ });
598
+ }
599
+ }
600
+ for (const declarator of project.declarators) {
601
+ const chain = unwindChain(declarator.node);
602
+ if (chain === undefined) {
603
+ continue;
604
+ }
605
+ collectCall(project, declarator, chain);
606
+ }
607
+ }
608
+ function collectCall(project, declarator, chain) {
609
+ const file = declarator.file;
610
+ const callee = parseCallee(file, chain.baseCallee);
611
+ const schema = callee.object === undefined
612
+ ? undefined
613
+ : project.schemas.get(callee.object);
614
+ const creator = callee.object === undefined ? project.creators.get(callee.name) : undefined;
615
+ const tableDialect = callee.object === undefined
616
+ ? TABLE_FUNCTIONS[callee.name]
617
+ : callee.name === 'table' && schema !== undefined
618
+ ? schema.dialect
619
+ : undefined;
620
+ if (tableDialect !== undefined || creator !== undefined) {
621
+ const dialect = tableDialect ?? creator?.dialect;
622
+ if (dialect === undefined) {
623
+ return;
624
+ }
625
+ for (const link of chain.links) {
626
+ project.warnings.push(`${file.label}: table "${declarator.name}": the chained .${link.name}() call is not represented in the shared model and was ignored.`);
627
+ }
628
+ project.tables.push({
629
+ file,
630
+ varName: declarator.name,
631
+ dialect,
632
+ ...(schema === undefined ? {} : { schema: schema.dbName }),
633
+ ...(creator === undefined ? {} : { creator }),
634
+ nameNode: chain.baseArgs[0],
635
+ columnsNode: chain.baseArgs[1],
636
+ extraNode: chain.baseArgs[2],
637
+ });
638
+ return;
639
+ }
640
+ if (callee.object === undefined && callee.name === 'pgEnum') {
641
+ registerEnum(project, declarator, chain, undefined);
642
+ return;
643
+ }
644
+ if (callee.name === 'enum' && schema !== undefined) {
645
+ registerEnum(project, declarator, chain, schema.dbName);
646
+ return;
647
+ }
648
+ if (callee.object === undefined && callee.name === 'relations') {
649
+ registerRelations(project, declarator, chain);
650
+ return;
651
+ }
652
+ if (callee.object === undefined && callee.name === 'defineRelations') {
653
+ project.warnings.push(`${file.label}: defineRelations (Drizzle relational queries v2) is not supported; "${declarator.name}" was skipped, so the relation names and reverse sides it defines are not read. ` +
654
+ `Foreign keys declared with .references() or foreignKey() are still read.`);
655
+ return;
656
+ }
657
+ const unsupportedDialect = callee.object === undefined
658
+ ? UNSUPPORTED_DIALECT_TABLES[callee.name]
659
+ : undefined;
660
+ if (unsupportedDialect !== undefined) {
661
+ project.warnings.push(`${file.label}: table "${declarator.name}" uses the ${unsupportedDialect} dialect (${callee.name}), which is not supported; it was skipped.`);
662
+ return;
663
+ }
664
+ const unsupportedDefinition = callee.object === undefined
665
+ ? UNSUPPORTED_DEFINITIONS[callee.name]
666
+ : undefined;
667
+ if (unsupportedDefinition !== undefined) {
668
+ project.warnings.push(`${file.label}: "${declarator.name}" defines ${unsupportedDefinition} (${callee.name}), which the shared model cannot represent; it was skipped.`);
669
+ }
670
+ if (callee.object !== undefined &&
671
+ schema !== undefined &&
672
+ UNSUPPORTED_DEFINITIONS[`pg${toPascalCase(callee.name)}`] !== undefined) {
673
+ project.warnings.push(`${file.label}: "${declarator.name}" defines ${UNSUPPORTED_DEFINITIONS[`pg${toPascalCase(callee.name)}`] ?? 'objects'} (${callee.object}.${callee.name}), which the shared model cannot represent; it was skipped.`);
674
+ }
675
+ }
676
+ /** Derives an enum member name from a stored value, for example "in-progress" -> "IN_PROGRESS". */
677
+ function enumMemberName(value) {
678
+ const snake = toSnakeCase(value).toUpperCase();
679
+ if (snake === '') {
680
+ return 'EMPTY';
681
+ }
682
+ return /^[0-9]/.test(snake) ? `V_${snake}` : snake;
683
+ }
684
+ function enumValuesOf(values, unique = true) {
685
+ const used = new Set();
686
+ return values.map((dbValue) => {
687
+ let name = enumMemberName(dbValue);
688
+ while (unique && used.has(name)) {
689
+ name = `${name}_`;
690
+ }
691
+ used.add(name);
692
+ return { name, dbValue };
693
+ });
694
+ }
695
+ function registerEnum(project, declarator, chain, schema) {
696
+ const label = declarator.file.label;
697
+ const dbName = stringValue(chain.baseArgs[0]);
698
+ const valuesNode = chain.baseArgs[1];
699
+ const values = valuesNode === undefined ? undefined : resolveValue(valuesNode, project);
700
+ if (dbName === undefined ||
701
+ values === undefined ||
702
+ values.kind !== 'array' ||
703
+ !values.items.every((item) => item.kind === 'string')) {
704
+ project.warnings.push(`${label}: the enum "${declarator.name}" is not defined with a string literal name and a literal list of strings; it was skipped.`);
705
+ return;
706
+ }
707
+ // Drizzle code conventionally names the constant `statusEnum`; the enum itself is `Status`. The
708
+ // emitter appends the suffix again, so stripping one `Enum` keeps the round trip stable.
709
+ const pascalName = toPascalCase(declarator.name);
710
+ const name = pascalName.length > 'Enum'.length && pascalName.endsWith('Enum')
711
+ ? pascalName.slice(0, -'Enum'.length)
712
+ : pascalName;
713
+ project.enums.set(declarator.name, {
714
+ varName: declarator.name,
715
+ ir: {
716
+ name,
717
+ values: enumValuesOf(values.items.flatMap((item) => item.kind === 'string' ? [item.value] : [])),
718
+ ...(dbName === name ? {} : { dbName }),
719
+ ...(schema === undefined ? {} : { schema }),
720
+ },
721
+ });
722
+ }
723
+ function registerRelations(project, declarator, chain) {
724
+ const file = declarator.file;
725
+ const tableRef = chain.baseArgs[0];
726
+ const callback = chain.baseArgs[1] === undefined
727
+ ? undefined
728
+ : stripWrappers(chain.baseArgs[1]);
729
+ if (tableRef === undefined || callback === undefined) {
730
+ return;
731
+ }
732
+ const tableVar = canonicalName(file, tableRef.text.split('.').pop() ?? tableRef.text);
733
+ const result = isFunctionNode(callback)
734
+ ? functionResult(callback)
735
+ : undefined;
736
+ if (result === undefined || result.type !== 'object') {
737
+ project.warnings.push(`${file.label}: the relations of "${tableVar}" are not defined by a callback returning an object literal; they were skipped.`);
738
+ return;
739
+ }
740
+ const entries = [];
741
+ for (const child of result.namedChildren) {
742
+ if (child.type === 'comment') {
743
+ continue;
744
+ }
745
+ if (child.type !== 'pair') {
746
+ project.warnings.push(`${file.label}: the relations of "${tableVar}" contain a spread or computed entry (${child.text}); it was skipped.`);
747
+ continue;
748
+ }
749
+ const keyNode = child.childForFieldName('key');
750
+ const valueNode = child.childForFieldName('value');
751
+ const call = valueNode === null ? undefined : unwindChain(valueNode);
752
+ const kindName = call?.baseCallee.split('.').pop();
753
+ if (keyNode === null ||
754
+ call === undefined ||
755
+ (kindName !== 'one' && kindName !== 'many')) {
756
+ project.warnings.push(`${file.label}: the relation "${tableVar}.${keyNode === null ? '?' : unquote(keyNode.text)}" is not written as one(...) or many(...); it was skipped.`);
757
+ continue;
758
+ }
759
+ const targetNode = call.baseArgs[0];
760
+ const config = call.baseArgs[1];
761
+ if (targetNode === undefined) {
762
+ continue;
763
+ }
764
+ const relationName = stringValue(objectProperty(config, 'relationName'));
765
+ entries.push({
766
+ key: unquote(keyNode.text),
767
+ kind: kindName,
768
+ targetVar: canonicalName(file, targetNode.text.split('.').pop() ?? targetNode.text),
769
+ fields: columnRefs(objectProperty(config, 'fields')),
770
+ references: columnRefs(objectProperty(config, 'references')),
771
+ relationName,
772
+ });
773
+ }
774
+ project.relationDefs.push({ file, tableVar, entries });
775
+ }
776
+ // ---------------------------------------------------------------------------
777
+ // Tables and columns
778
+ // ---------------------------------------------------------------------------
779
+ function tableLabel(table) {
780
+ const decl = 'decl' in table ? table.decl : table;
781
+ return `${decl.file.label}: table "${decl.varName}"`;
782
+ }
783
+ /** A model name from the variable a table is assigned to: `blogPosts` -> `BlogPost`. */
784
+ function modelNameOf(varName) {
785
+ return toPascalCase(singularize(toSnakeCase(varName)));
786
+ }
787
+ function buildSchema(project, options) {
788
+ const warnings = project.warnings;
789
+ const tables = [];
790
+ const usedModelNames = new Set();
791
+ const usedVars = new Set();
792
+ for (const decl of project.tables) {
793
+ if (usedVars.has(decl.varName)) {
794
+ warnings.push(`${tableLabel(decl)}: another table with the same variable name was already read; this one was skipped.`);
795
+ continue;
796
+ }
797
+ const tableName = stringValue(decl.nameNode);
798
+ if (tableName === undefined) {
799
+ warnings.push(`${tableLabel(decl)}: the table name is not a string literal, so the table was skipped.`);
800
+ continue;
801
+ }
802
+ usedVars.add(decl.varName);
803
+ let modelName = modelNameOf(decl.varName);
804
+ if (usedModelNames.has(modelName)) {
805
+ modelName = toPascalCase(decl.varName);
806
+ }
807
+ if (usedModelNames.has(modelName)) {
808
+ warnings.push(`${tableLabel(decl)}: the model name "${modelName}" is already taken by another table; this table was skipped.`);
809
+ continue;
810
+ }
811
+ usedModelNames.add(modelName);
812
+ let dbName = tableName;
813
+ if (decl.creator !== undefined && decl.creator.transform !== undefined) {
814
+ dbName = decl.creator.transform(tableName);
815
+ }
816
+ tables.push({
817
+ decl,
818
+ modelName,
819
+ dbName,
820
+ columns: [],
821
+ foreignKeys: [],
822
+ indexes: [],
823
+ primaryKey: undefined,
824
+ });
825
+ }
826
+ const enums = [...project.enums.values()].map((declared) => declared.ir);
827
+ for (const table of tables) {
828
+ buildColumns(table, project, enums);
829
+ parseExtras(table, project);
830
+ }
831
+ const byVar = new Map(tables.map((table) => [table.decl.varName, table]));
832
+ const owners = collectOwners(tables, byVar, project);
833
+ linkReverseSides(tables, byVar, owners, project);
834
+ const models = tables.map((table) => finalizeModel(table, byVar, owners, warnings, options));
835
+ addStubModels(models, warnings, options);
836
+ return { models, enums, warnings };
837
+ }
838
+ /** Reads the `{ key: column, ... }` object of a table, expanding spreads of shared column sets. */
839
+ function columnEntries(node, table, project, seen = new Set()) {
840
+ if (node === undefined) {
841
+ return [];
842
+ }
843
+ const object = stripWrappers(node);
844
+ const label = tableLabel(table);
845
+ if (object.type === 'identifier') {
846
+ const helper = project.helpers.get(object.text);
847
+ if (helper === undefined || seen.has(object.text)) {
848
+ project.warnings.push(`${label}: the columns are not an object literal (${object.text}); the table has no columns.`);
849
+ return [];
850
+ }
851
+ return columnEntries(helper, table, project, new Set([...seen, object.text]));
852
+ }
853
+ if (object.type !== 'object') {
854
+ project.warnings.push(`${label}: the columns are not an object literal; the table has no columns.`);
855
+ return [];
856
+ }
857
+ const entries = [];
858
+ for (const child of object.namedChildren) {
859
+ if (child.type === 'comment') {
860
+ continue;
861
+ }
862
+ if (child.type === 'pair') {
863
+ const keyNode = child.childForFieldName('key');
864
+ const valueNode = child.childForFieldName('value');
865
+ if (keyNode !== null && valueNode !== null) {
866
+ entries.push({ key: unquote(keyNode.text), value: valueNode });
867
+ }
868
+ }
869
+ else if (child.type === 'spread_element') {
870
+ const spread = child.namedChildren[0];
871
+ const target = spread === undefined
872
+ ? undefined
873
+ : spread.type === 'call_expression'
874
+ ? (spread.childForFieldName('function') ?? undefined)
875
+ : spread;
876
+ const name = target?.text;
877
+ const helper = name === undefined ? undefined : project.helpers.get(name);
878
+ if (name === undefined || helper === undefined || seen.has(name)) {
879
+ project.warnings.push(`${label}: the spread "${child.text}" cannot be resolved to a column object defined in the input; its columns were skipped.`);
880
+ continue;
881
+ }
882
+ entries.push(...columnEntries(helper, table, project, new Set([...seen, name])));
883
+ }
884
+ else {
885
+ project.warnings.push(`${label}: the entry "${child.text}" is not a "name: column" pair and was skipped.`);
886
+ }
887
+ }
888
+ return entries;
889
+ }
890
+ function buildColumns(table, project, enums) {
891
+ const seenKeys = new Set();
892
+ for (const entry of columnEntries(table.decl.columnsNode, table, project)) {
893
+ if (seenKeys.has(entry.key)) {
894
+ continue;
895
+ }
896
+ seenKeys.add(entry.key);
897
+ const column = buildColumn(table, entry.key, entry.value, project, enums);
898
+ if (column !== undefined) {
899
+ table.columns.push(column);
900
+ }
901
+ }
902
+ }
903
+ /** Resolves the builder call of a column, following one level of user-written column helpers. */
904
+ function resolveChain(table, node, project) {
905
+ const chain = unwindChain(node);
906
+ if (chain === undefined) {
907
+ return undefined;
908
+ }
909
+ const callee = parseCallee(table.decl.file, chain.baseCallee);
910
+ const helper = callee.object === undefined ? project.helpers.get(callee.name) : undefined;
911
+ if (helper === undefined || BUILTIN_BUILDERS.has(callee.name)) {
912
+ return { chain, fallbackName: undefined };
913
+ }
914
+ const inner = unwindChain(helper);
915
+ if (inner === undefined) {
916
+ return { chain, fallbackName: undefined };
917
+ }
918
+ return {
919
+ chain: {
920
+ baseCallee: inner.baseCallee,
921
+ baseArgs: inner.baseArgs,
922
+ links: [...inner.links, ...chain.links],
923
+ },
924
+ fallbackName: stringValue(chain.baseArgs[0]),
925
+ };
926
+ }
927
+ /** Every builder name this parser knows; a user helper never shadows one of them. */
928
+ const BUILTIN_BUILDERS = new Set([
929
+ ...Object.keys(PG_SIMPLE_TYPES),
930
+ ...Object.keys(PG_UNSUPPORTED_TYPES),
931
+ ...Object.keys(MYSQL_SIMPLE_TYPES),
932
+ ...Object.keys(SQLITE_SIMPLE_TYPES),
933
+ 'varchar',
934
+ 'char',
935
+ 'numeric',
936
+ 'decimal',
937
+ 'binary',
938
+ 'varbinary',
939
+ 'mysqlEnum',
940
+ 'integer',
941
+ 'text',
942
+ 'blob',
943
+ ]);
944
+ function buildColumn(table, key, node, project, enums) {
945
+ const label = `${tableLabel(table)}, column "${key}"`;
946
+ const resolved = resolveChain(table, node, project);
947
+ if (resolved === undefined) {
948
+ project.warnings.push(`${label}: the column is not written as a builder call such as integer('name'); it was skipped.`);
949
+ return undefined;
950
+ }
951
+ const { chain, fallbackName } = resolved;
952
+ const callee = parseCallee(table.decl.file, chain.baseCallee);
953
+ const builder = callee.name;
954
+ // The first argument is the column name; builders also accept the options object first.
955
+ const firstArg = chain.baseArgs[0];
956
+ const firstValue = firstArg === undefined ? undefined : evaluateNode(firstArg);
957
+ const config = firstValue?.kind === 'object' ? firstValue : objectValue(chain.baseArgs[1]);
958
+ let columnName = key;
959
+ if (firstValue?.kind === 'string') {
960
+ columnName = firstValue.value;
961
+ }
962
+ else if (firstValue !== undefined && firstValue.kind !== 'object') {
963
+ if (fallbackName !== undefined) {
964
+ columnName = fallbackName;
965
+ }
966
+ else {
967
+ project.warnings.push(`${label}: the column name is not a string literal; the property name "${key}" was used.`);
968
+ }
969
+ }
970
+ const field = {
971
+ name: key,
972
+ columnName,
973
+ type: 'string',
974
+ isPrimaryKey: false,
975
+ isUnique: false,
976
+ isNullable: true,
977
+ isAutoUpdated: false,
978
+ };
979
+ let isNotNull = false;
980
+ const enumDecl = callee.object === undefined ? project.enums.get(builder) : undefined;
981
+ let resolvedType;
982
+ if (enumDecl !== undefined) {
983
+ resolvedType = { type: 'string' };
984
+ field.enumName = enumDecl.ir.name;
985
+ }
986
+ else if (builder === 'mysqlEnum' && table.decl.dialect === 'mysql') {
987
+ const values = chain.baseArgs[1] === undefined
988
+ ? undefined
989
+ : resolveValue(chain.baseArgs[1], project);
990
+ if (values?.kind === 'array') {
991
+ const inline = inlineEnum(table, key, values.items, enums, project);
992
+ field.enumName = inline.name;
993
+ resolvedType = { type: 'string' };
994
+ }
995
+ else {
996
+ project.warnings.push(`${label}: mysqlEnum is not given a literal list of values; the column was read as a string.`);
997
+ resolvedType = { type: 'string' };
998
+ }
999
+ }
1000
+ else {
1001
+ resolvedType = resolveType(table.decl.dialect, builder, config);
1002
+ if (resolvedType === undefined) {
1003
+ resolvedType = {
1004
+ type: 'unsupported',
1005
+ unsupportedType: builder,
1006
+ note: `the column builder "${chain.baseCallee}" is not a ${dialectName(table.decl.dialect)} column type this parser knows; ` +
1007
+ `the column was kept as an unsupported "${builder}" column.`,
1008
+ };
1009
+ }
1010
+ }
1011
+ field.type = resolvedType.type;
1012
+ if (resolvedType.maxLength !== undefined) {
1013
+ field.maxLength = resolvedType.maxLength;
1014
+ }
1015
+ if (resolvedType.maxDigits !== undefined) {
1016
+ field.maxDigits = resolvedType.maxDigits;
1017
+ }
1018
+ if (resolvedType.decimalPlaces !== undefined) {
1019
+ field.decimalPlaces = resolvedType.decimalPlaces;
1020
+ }
1021
+ if (resolvedType.unsupportedType !== undefined) {
1022
+ field.unsupportedType = resolvedType.unsupportedType;
1023
+ if (enumDecl === undefined && resolvedType.note === undefined) {
1024
+ project.warnings.push(`${label}: the column type "${resolvedType.unsupportedType}" has no equivalent in the shared model and was kept as an unsupported column.`);
1025
+ }
1026
+ }
1027
+ if (resolvedType.note !== undefined) {
1028
+ project.warnings.push(`${label}: ${resolvedType.note}`);
1029
+ }
1030
+ if (resolvedType.autoIncrement === true) {
1031
+ field.default = { kind: 'autoIncrement' };
1032
+ }
1033
+ if (resolvedType.notNull === true) {
1034
+ isNotNull = true;
1035
+ }
1036
+ if (resolvedType.unique === true) {
1037
+ field.isUnique = true;
1038
+ }
1039
+ if (config !== undefined && boolOption(config, 'unsigned') === true) {
1040
+ project.warnings.push(`${label}: unsigned integers are not represented in the shared model; the column was read as a signed integer.`);
1041
+ }
1042
+ let defaultNode;
1043
+ let hasDefaultFn = false;
1044
+ for (const link of chain.links) {
1045
+ switch (link.name) {
1046
+ case 'notNull':
1047
+ isNotNull = true;
1048
+ break;
1049
+ case 'primaryKey': {
1050
+ field.isPrimaryKey = true;
1051
+ isNotNull = true;
1052
+ if (boolOption(objectValue(link.args[0]), 'autoIncrement') === true) {
1053
+ field.default = { kind: 'autoIncrement' };
1054
+ }
1055
+ break;
1056
+ }
1057
+ case 'autoincrement':
1058
+ case 'generatedAlwaysAsIdentity':
1059
+ case 'generatedByDefaultAsIdentity':
1060
+ field.default = { kind: 'autoIncrement' };
1061
+ break;
1062
+ case 'unique': {
1063
+ field.isUnique = true;
1064
+ const constraintName = stringValue(link.args[0]);
1065
+ if (constraintName !== undefined) {
1066
+ field.uniqueName = constraintName;
1067
+ }
1068
+ if (link.args.some((arg) => arg.type === 'object')) {
1069
+ project.warnings.push(`${label}: the unique constraint options (NULLS NOT DISTINCT) are not represented in the shared model and were ignored.`);
1070
+ }
1071
+ break;
1072
+ }
1073
+ case 'default':
1074
+ defaultNode = link.args[0];
1075
+ break;
1076
+ case 'defaultNow':
1077
+ field.default = { kind: 'now' };
1078
+ break;
1079
+ case 'defaultRandom':
1080
+ field.default = { kind: 'uuid' };
1081
+ break;
1082
+ case '$defaultFn':
1083
+ case '$default':
1084
+ hasDefaultFn = true;
1085
+ applyClientDefault(field, link.args[0], label, project);
1086
+ break;
1087
+ case '$onUpdate':
1088
+ case '$onUpdateFn':
1089
+ applyOnUpdate(field, label, project);
1090
+ break;
1091
+ case 'onUpdateNow':
1092
+ field.isAutoUpdated = true;
1093
+ break;
1094
+ case 'array':
1095
+ field.arrayDepth = (field.arrayDepth ?? 0) + 1;
1096
+ break;
1097
+ case 'references':
1098
+ addReference(table, key, link.args, label, project);
1099
+ break;
1100
+ case 'generatedAlwaysAs':
1101
+ applyGenerated(field, link.args, label, project);
1102
+ break;
1103
+ case '$type':
1104
+ case 'notnull':
1105
+ break;
1106
+ default:
1107
+ project.warnings.push(`${label}: the .${link.name}() modifier is not represented in the shared model and was ignored.`);
1108
+ break;
1109
+ }
1110
+ }
1111
+ if (defaultNode !== undefined) {
1112
+ if (hasDefaultFn) {
1113
+ project.warnings.push(`${label}: both .default() and a client-side default are set; the .default() value is used.`);
1114
+ }
1115
+ const converted = convertDefault(defaultNode, field, label, project, enumDecl?.ir ?? enums.find((item) => item.name === field.enumName));
1116
+ if (converted !== undefined) {
1117
+ field.default = converted;
1118
+ }
1119
+ }
1120
+ if (field.isAutoUpdated && field.default?.kind === 'now') {
1121
+ // An auto-updated column is stamped on every write, so the other readers do not carry a separate "now" default.
1122
+ delete field.default;
1123
+ }
1124
+ if (field.isPrimaryKey) {
1125
+ isNotNull = true;
1126
+ field.isUnique = false;
1127
+ }
1128
+ field.isNullable = !isNotNull;
1129
+ return { key, field, isNotNull };
1130
+ }
1131
+ function dialectName(dialect) {
1132
+ switch (dialect) {
1133
+ case 'pg':
1134
+ return 'PostgreSQL';
1135
+ case 'mysql':
1136
+ return 'MySQL';
1137
+ default:
1138
+ return 'SQLite';
1139
+ }
1140
+ }
1141
+ /** A MySQL `mysqlEnum` column becomes an enum named after the table and the column. */
1142
+ function inlineEnum(table, key, items, enums, project) {
1143
+ const values = items.flatMap((item) => item.kind === 'string' ? [item.value] : []);
1144
+ if (values.length !== items.length) {
1145
+ project.warnings.push(`${tableLabel(table)}, column "${key}": some mysqlEnum values are not string literals and were skipped.`);
1146
+ }
1147
+ let name = `${table.modelName}${toPascalCase(key)}`;
1148
+ while (enums.some((item) => item.name === name)) {
1149
+ name = `${name}Enum`;
1150
+ }
1151
+ const created = {
1152
+ name,
1153
+ values: enumValuesOf(values),
1154
+ ...(table.decl.schema === undefined ? {} : { schema: table.decl.schema }),
1155
+ };
1156
+ enums.push(created);
1157
+ return created;
1158
+ }
1159
+ /** Maps a column builder to an IR type. Returns undefined for a builder the dialect does not have. */
1160
+ function resolveType(dialect, builder, config) {
1161
+ const length = numberOption(config, 'length');
1162
+ const precision = numberOption(config, 'precision');
1163
+ const scale = numberOption(config, 'scale');
1164
+ const mode = stringOption(config, 'mode');
1165
+ const decimalType = () => ({
1166
+ type: 'decimal',
1167
+ ...(precision === undefined ? {} : { maxDigits: precision }),
1168
+ ...(scale === undefined ? {} : { decimalPlaces: scale }),
1169
+ });
1170
+ if (dialect === 'pg') {
1171
+ const simple = PG_SIMPLE_TYPES[builder];
1172
+ if (simple !== undefined) {
1173
+ const serial = PG_SERIAL_TYPES.has(builder);
1174
+ return {
1175
+ type: simple,
1176
+ ...(serial ? { autoIncrement: true, notNull: true } : {}),
1177
+ };
1178
+ }
1179
+ switch (builder) {
1180
+ case 'varchar':
1181
+ return {
1182
+ type: 'string',
1183
+ ...(length === undefined ? {} : { maxLength: length }),
1184
+ };
1185
+ case 'char':
1186
+ return { type: 'string', maxLength: length ?? 1 };
1187
+ case 'numeric':
1188
+ case 'decimal':
1189
+ return decimalType();
1190
+ default:
1191
+ break;
1192
+ }
1193
+ const unsupported = PG_UNSUPPORTED_TYPES[builder];
1194
+ if (unsupported !== undefined) {
1195
+ const dimensions = numberOption(config, 'dimensions');
1196
+ return {
1197
+ type: 'unsupported',
1198
+ unsupportedType: dimensions === undefined
1199
+ ? unsupported
1200
+ : `${unsupported}(${dimensions})`,
1201
+ };
1202
+ }
1203
+ return undefined;
1204
+ }
1205
+ if (dialect === 'mysql') {
1206
+ const simple = MYSQL_SIMPLE_TYPES[builder];
1207
+ if (simple !== undefined) {
1208
+ return { type: simple };
1209
+ }
1210
+ switch (builder) {
1211
+ case 'serial':
1212
+ return {
1213
+ type: 'bigInt',
1214
+ autoIncrement: true,
1215
+ notNull: true,
1216
+ unique: true,
1217
+ };
1218
+ case 'varchar':
1219
+ case 'char':
1220
+ return {
1221
+ type: 'string',
1222
+ ...(length === undefined
1223
+ ? builder === 'char'
1224
+ ? { maxLength: 1 }
1225
+ : {}
1226
+ : { maxLength: length }),
1227
+ };
1228
+ case 'decimal':
1229
+ case 'numeric':
1230
+ return decimalType();
1231
+ case 'binary':
1232
+ case 'varbinary':
1233
+ return {
1234
+ type: 'bytes',
1235
+ ...(length === undefined ? {} : { maxLength: length }),
1236
+ };
1237
+ default:
1238
+ return undefined;
1239
+ }
1240
+ }
1241
+ // SQLite
1242
+ const simple = SQLITE_SIMPLE_TYPES[builder];
1243
+ if (simple !== undefined) {
1244
+ return { type: simple };
1245
+ }
1246
+ switch (builder) {
1247
+ case 'integer':
1248
+ if (mode === 'timestamp' || mode === 'timestamp_ms') {
1249
+ return {
1250
+ type: 'dateTime',
1251
+ note: `integer({ mode: '${mode}' }) stores Unix time as an integer; it was read as a date-time column.`,
1252
+ };
1253
+ }
1254
+ return { type: mode === 'boolean' ? 'boolean' : 'int' };
1255
+ case 'text':
1256
+ if (mode === 'json') {
1257
+ return { type: 'json' };
1258
+ }
1259
+ return length === undefined
1260
+ ? { type: 'text' }
1261
+ : { type: 'string', maxLength: length };
1262
+ case 'blob':
1263
+ if (mode === 'json') {
1264
+ return { type: 'json' };
1265
+ }
1266
+ return { type: mode === 'bigint' ? 'bigInt' : 'bytes' };
1267
+ case 'numeric':
1268
+ return { type: 'decimal' };
1269
+ default:
1270
+ return undefined;
1271
+ }
1272
+ }
1273
+ // ---------------------------------------------------------------------------
1274
+ // Defaults
1275
+ // ---------------------------------------------------------------------------
1276
+ function describeNode(node) {
1277
+ const text = (node?.text ?? '').replace(/\s+/g, ' ');
1278
+ return text.length > 60 ? `${text.slice(0, 57)}...` : text;
1279
+ }
1280
+ function applyClientDefault(field, fn, label, project) {
1281
+ const source = describeNode(fn);
1282
+ const result = fn === undefined || !isFunctionNode(stripWrappers(fn))
1283
+ ? undefined
1284
+ : functionResult(stripWrappers(fn));
1285
+ const body = result?.text ?? '';
1286
+ if (field.type === 'uuid' &&
1287
+ /randomUUID\s*\(|\buuid(v4)?\s*\(|\bv4\s*\(/i.test(body)) {
1288
+ field.default = { kind: 'uuid' };
1289
+ project.warnings.push(`${label}: $defaultFn(${source}) is generated in application code; it was read as a generated UUID default, which the target generates instead.`);
1290
+ return;
1291
+ }
1292
+ if ((field.type === 'dateTime' || field.type === 'date') &&
1293
+ /^(new Date\(\)|Date\.now\(\)|sql`now\(\)`)$/.test(body)) {
1294
+ field.default = { kind: 'now' };
1295
+ project.warnings.push(`${label}: $defaultFn(${source}) is evaluated in application code; it was read as a "now" default, which the target applies in the database instead.`);
1296
+ return;
1297
+ }
1298
+ project.warnings.push(`${label}: $defaultFn(${source}) is a client-side default computed in application code; it has no equivalent in the shared model and was dropped.`);
1299
+ }
1300
+ function applyOnUpdate(field, label, project) {
1301
+ if (field.type === 'dateTime' ||
1302
+ field.type === 'date' ||
1303
+ field.type === 'time') {
1304
+ field.isAutoUpdated = true;
1305
+ return;
1306
+ }
1307
+ project.warnings.push(`${label}: $onUpdate on a ${field.type} column is not represented in the shared model (only date-time columns refresh on update); it was ignored.`);
1308
+ }
1309
+ function applyGenerated(field, args, label, project) {
1310
+ const expressionNode = args[0];
1311
+ const expression = expressionNode === undefined
1312
+ ? undefined
1313
+ : isFunctionNode(stripWrappers(expressionNode))
1314
+ ? functionResult(stripWrappers(expressionNode))
1315
+ : expressionNode;
1316
+ const sql = expression === undefined ? undefined : sqlText(expression);
1317
+ if (sql === undefined) {
1318
+ project.warnings.push(`${label}: the generated column expression could not be read statically; the column was read as an ordinary column.`);
1319
+ return;
1320
+ }
1321
+ const mode = stringOption(objectValue(args[1]), 'mode');
1322
+ field.generated = {
1323
+ expression: sql.text.trim(),
1324
+ isStored: mode !== 'virtual',
1325
+ };
1326
+ if (sql.isDynamic) {
1327
+ project.warnings.push(`${label}: the generated column expression refers to other columns through \${...}; the expression text is kept as written.`);
1328
+ }
1329
+ }
1330
+ function jsonText(value) {
1331
+ switch (value.kind) {
1332
+ case 'string':
1333
+ return JSON.stringify(value.value);
1334
+ case 'number':
1335
+ return String(value.value);
1336
+ case 'bool':
1337
+ return value.value ? 'true' : 'false';
1338
+ case 'null':
1339
+ return 'null';
1340
+ case 'array': {
1341
+ const items = value.items.map(jsonText);
1342
+ return items.every((item) => item !== undefined)
1343
+ ? `[${items.join(',')}]`
1344
+ : undefined;
1345
+ }
1346
+ case 'object': {
1347
+ const parts = [];
1348
+ for (const [key, item] of Object.entries(value.properties)) {
1349
+ const text = jsonText(item);
1350
+ if (text === undefined) {
1351
+ return undefined;
1352
+ }
1353
+ parts.push(`${JSON.stringify(key)}:${text}`);
1354
+ }
1355
+ return `{${parts.join(',')}}`;
1356
+ }
1357
+ default:
1358
+ return undefined;
1359
+ }
1360
+ }
1361
+ function convertDefault(node, field, label, project, enumeration) {
1362
+ const sql = sqlText(node);
1363
+ if (sql !== undefined) {
1364
+ if (sql.isDynamic) {
1365
+ project.warnings.push(`${label}: the default sql\`${sql.text}\` refers to other values through \${...} and cannot be read statically; it was dropped.`);
1366
+ return undefined;
1367
+ }
1368
+ return convertSqlDefault(sql.text, field, enumeration);
1369
+ }
1370
+ const value = resolveValue(stripWrappers(node), project);
1371
+ switch (value.kind) {
1372
+ case 'null':
1373
+ return undefined;
1374
+ case 'bool':
1375
+ return { kind: 'literal', value: value.value };
1376
+ case 'number':
1377
+ return { kind: 'literal', value: value.value };
1378
+ case 'string': {
1379
+ const member = enumeration?.values.find((candidate) => candidate.dbValue === value.value);
1380
+ if (member !== undefined) {
1381
+ return { kind: 'enumValue', value: member.name };
1382
+ }
1383
+ if (/^-?\d+(\.\d+)?$/.test(value.value) &&
1384
+ (field.type === 'decimal' ||
1385
+ field.type === 'int' ||
1386
+ field.type === 'bigInt' ||
1387
+ field.type === 'float')) {
1388
+ return { kind: 'literal', value: Number(value.value) };
1389
+ }
1390
+ return { kind: 'literal', value: value.value };
1391
+ }
1392
+ case 'object':
1393
+ case 'array': {
1394
+ const text = jsonText(value);
1395
+ if (text !== undefined &&
1396
+ field.type === 'json' &&
1397
+ field.arrayDepth === undefined) {
1398
+ return { kind: 'literal', value: text };
1399
+ }
1400
+ break;
1401
+ }
1402
+ case 'other': {
1403
+ const bigint = /^(-?\d+)n$/.exec(value.text);
1404
+ if (bigint !== null) {
1405
+ return { kind: 'literal', value: Number(bigint[1]) };
1406
+ }
1407
+ break;
1408
+ }
1409
+ default:
1410
+ break;
1411
+ }
1412
+ project.warnings.push(`${label}: the default value (${describeNode(node)}) cannot be evaluated statically and was dropped.`);
1413
+ return undefined;
1414
+ }
1415
+ function convertSqlDefault(sql, field, enumeration) {
1416
+ const trimmed = sql.trim();
1417
+ if (NOW_SQL.test(trimmed)) {
1418
+ return { kind: 'now' };
1419
+ }
1420
+ if (UUID_SQL.test(trimmed)) {
1421
+ return { kind: 'uuid' };
1422
+ }
1423
+ if (/^null$/i.test(trimmed)) {
1424
+ return undefined;
1425
+ }
1426
+ if (/^(true|false)$/i.test(trimmed)) {
1427
+ return { kind: 'literal', value: trimmed.toLowerCase() === 'true' };
1428
+ }
1429
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
1430
+ return field.type === 'string' || field.type === 'text'
1431
+ ? { kind: 'literal', value: trimmed }
1432
+ : { kind: 'literal', value: Number(trimmed) };
1433
+ }
1434
+ const quoted = QUOTED_SQL.exec(trimmed);
1435
+ if (quoted !== null) {
1436
+ const text = (quoted[1] ?? '').replace(/''/g, "'");
1437
+ const member = enumeration?.values.find((candidate) => candidate.dbValue === text);
1438
+ if (member !== undefined) {
1439
+ return { kind: 'enumValue', value: member.name };
1440
+ }
1441
+ return { kind: 'literal', value: text };
1442
+ }
1443
+ return { kind: 'dbExpression', expression: trimmed };
1444
+ }
1445
+ // ---------------------------------------------------------------------------
1446
+ // Foreign keys, indexes and keys declared in the table callback
1447
+ // ---------------------------------------------------------------------------
1448
+ function mapAction(value, what, label, project) {
1449
+ if (value === undefined) {
1450
+ return undefined;
1451
+ }
1452
+ const mapped = ON_ACTION_MAP[value.toLowerCase()];
1453
+ if (mapped === undefined) {
1454
+ project.warnings.push(`${label}: ${what} "${value}" has no equivalent and was ignored.`);
1455
+ }
1456
+ return mapped;
1457
+ }
1458
+ /** Reads `.references(() => users.id, { onDelete: 'cascade' })` into a foreign key on the column. */
1459
+ function addReference(table, key, args, label, project) {
1460
+ const targetFn = args[0] === undefined ? undefined : stripWrappers(args[0]);
1461
+ const target = targetFn === undefined || !isFunctionNode(targetFn)
1462
+ ? undefined
1463
+ : functionResult(targetFn);
1464
+ const ref = target === undefined ? undefined : parseColumnRef(target);
1465
+ if (ref === undefined || ref.owner === undefined) {
1466
+ project.warnings.push(`${label}: the target of .references(${describeNode(args[0])}) is not written as () => table.column; the foreign key was skipped.`);
1467
+ return;
1468
+ }
1469
+ const config = objectValue(args[1]);
1470
+ table.foreignKeys.push({
1471
+ columns: [key],
1472
+ targetVar: canonicalName(table.decl.file, ref.owner),
1473
+ targetColumns: [ref.key],
1474
+ onDelete: mapAction(stringOption(config, 'onDelete'), 'onDelete', label, project),
1475
+ onUpdate: mapAction(stringOption(config, 'onUpdate'), 'onUpdate', label, project),
1476
+ name: undefined,
1477
+ });
1478
+ }
1479
+ function parseExtras(table, project) {
1480
+ const extra = table.decl.extraNode === undefined
1481
+ ? undefined
1482
+ : stripWrappers(table.decl.extraNode);
1483
+ if (extra === undefined) {
1484
+ return;
1485
+ }
1486
+ const label = tableLabel(table);
1487
+ const result = isFunctionNode(extra)
1488
+ ? functionResult(extra)
1489
+ : undefined;
1490
+ const container = result === undefined ? undefined : stripWrappers(result);
1491
+ if (container === undefined ||
1492
+ (container.type !== 'array' && container.type !== 'object')) {
1493
+ project.warnings.push(`${label}: the indexes and constraints are not returned from the table callback as an array or object literal; they were skipped.`);
1494
+ return;
1495
+ }
1496
+ const items = [];
1497
+ for (const child of container.namedChildren) {
1498
+ if (child.type === 'comment') {
1499
+ continue;
1500
+ }
1501
+ if (container.type === 'array') {
1502
+ items.push(child);
1503
+ }
1504
+ else if (child.type === 'pair') {
1505
+ const value = child.childForFieldName('value');
1506
+ if (value !== null) {
1507
+ items.push(value);
1508
+ }
1509
+ }
1510
+ else {
1511
+ project.warnings.push(`${label}: the table callback entry "${describeNode(child)}" is a spread or computed entry and was skipped.`);
1512
+ }
1513
+ }
1514
+ for (const item of items) {
1515
+ parseExtraItem(table, item, project);
1516
+ }
1517
+ }
1518
+ function localKeys(table, refs, label, project) {
1519
+ const known = new Set(table.columns.map((column) => column.key));
1520
+ const missing = refs
1521
+ .filter((ref) => !known.has(ref.key))
1522
+ .map((ref) => ref.key);
1523
+ if (missing.length > 0) {
1524
+ project.warnings.push(`${label}: refers to "${missing.join('", "')}", which is not a column of the table; it was skipped.`);
1525
+ return undefined;
1526
+ }
1527
+ return refs.map((ref) => ref.key);
1528
+ }
1529
+ function parseExtraItem(table, item, project) {
1530
+ const label = tableLabel(table);
1531
+ const chain = unwindChain(item);
1532
+ if (chain === undefined) {
1533
+ project.warnings.push(`${label}: the table callback entry "${describeNode(item)}" is not a call such as index('name').on(...) and was skipped.`);
1534
+ return;
1535
+ }
1536
+ const callee = parseCallee(table.decl.file, chain.baseCallee);
1537
+ switch (callee.name) {
1538
+ case 'index':
1539
+ case 'uniqueIndex':
1540
+ case 'unique':
1541
+ parseIndex(table, callee.name, chain, project);
1542
+ return;
1543
+ case 'primaryKey': {
1544
+ const options = chain.baseArgs[0];
1545
+ const refs = columnRefs(objectProperty(options, 'columns'));
1546
+ const keys = refs === undefined
1547
+ ? undefined
1548
+ : localKeys(table, refs, `${label}: the primary key`, project);
1549
+ if (keys !== undefined) {
1550
+ table.primaryKey = {
1551
+ columns: keys,
1552
+ name: stringValue(objectProperty(options, 'name')),
1553
+ };
1554
+ }
1555
+ else if (refs === undefined) {
1556
+ project.warnings.push(`${label}: the primary key columns are not a literal list of table.column references; the primary key was skipped.`);
1557
+ }
1558
+ return;
1559
+ }
1560
+ case 'foreignKey': {
1561
+ const options = chain.baseArgs[0];
1562
+ const local = columnRefs(objectProperty(options, 'columns'));
1563
+ const foreign = columnRefs(objectProperty(options, 'foreignColumns'));
1564
+ const keys = local === undefined
1565
+ ? undefined
1566
+ : localKeys(table, local, `${label}: the foreign key`, project);
1567
+ const targetOwner = foreign?.[0]?.owner;
1568
+ if (foreign === undefined ||
1569
+ foreign.length !== local?.length ||
1570
+ targetOwner === undefined ||
1571
+ foreign.some((ref) => ref.owner !== targetOwner)) {
1572
+ if (keys !== undefined || local === undefined) {
1573
+ project.warnings.push(`${label}: a foreignKey() is not written with literal columns and foreignColumns lists of the same table; it was skipped.`);
1574
+ }
1575
+ return;
1576
+ }
1577
+ if (keys === undefined) {
1578
+ return;
1579
+ }
1580
+ const lookup = (name) => stringValue(chain.links.find((link) => link.name === name)?.args[0]);
1581
+ table.foreignKeys.push({
1582
+ columns: keys,
1583
+ targetVar: canonicalName(table.decl.file, targetOwner),
1584
+ targetColumns: foreign.map((ref) => ref.key),
1585
+ onDelete: mapAction(lookup('onDelete'), 'onDelete', label, project),
1586
+ onUpdate: mapAction(lookup('onUpdate'), 'onUpdate', label, project),
1587
+ name: stringValue(objectProperty(options, 'name')),
1588
+ });
1589
+ return;
1590
+ }
1591
+ case 'check':
1592
+ project.warnings.push(`${label}: the check constraint ${describeNode(chain.baseArgs[0])} has no equivalent in the shared model and was dropped.`);
1593
+ return;
1594
+ default:
1595
+ project.warnings.push(`${label}: the table callback entry ${callee.name}(...) is not represented in the shared model and was skipped.`);
1596
+ }
1597
+ }
1598
+ function parseIndex(table, kind, chain, project) {
1599
+ const label = tableLabel(table);
1600
+ const name = stringValue(chain.baseArgs[0]);
1601
+ const display = name === undefined ? '' : ` "${name}"`;
1602
+ const what = `the ${kind === 'index' ? 'index' : 'unique constraint'}${display}`;
1603
+ let refs;
1604
+ let method;
1605
+ let isPartial = false;
1606
+ for (const link of chain.links) {
1607
+ if (link.name === 'on' || link.name === 'using') {
1608
+ const columnArgs = link.name === 'using' ? link.args.slice(1) : link.args;
1609
+ if (link.name === 'using') {
1610
+ method = stringValue(link.args[0]);
1611
+ }
1612
+ const parsed = columnArgs.map(parseColumnRef);
1613
+ if (parsed.some((ref) => ref === undefined)) {
1614
+ project.warnings.push(`${label}: ${what} is built on an expression (${describeNode(columnArgs[0])}) rather than plain columns; it was skipped.`);
1615
+ return;
1616
+ }
1617
+ refs = parsed.flatMap((ref) => ref === undefined ? [] : [ref]);
1618
+ }
1619
+ else if (link.name === 'where') {
1620
+ isPartial = true;
1621
+ }
1622
+ else if (link.name === 'nullsNotDistinct') {
1623
+ project.warnings.push(`${label}: ${what} is NULLS NOT DISTINCT, which is not represented in the shared model and was ignored.`);
1624
+ }
1625
+ }
1626
+ if (refs === undefined || refs.length === 0) {
1627
+ project.warnings.push(`${label}: ${what} has no .on(...) columns; it was skipped.`);
1628
+ return;
1629
+ }
1630
+ const keys = localKeys(table, refs, `${label}: ${what}`, project);
1631
+ if (keys === undefined) {
1632
+ return;
1633
+ }
1634
+ const isUnique = kind !== 'index';
1635
+ if (isPartial) {
1636
+ if (isUnique) {
1637
+ project.warnings.push(`${label}: ${what} has a where condition (a partial index), which the shared model cannot represent; it was dropped because a full unique index would reject rows the source accepts.`);
1638
+ return;
1639
+ }
1640
+ project.warnings.push(`${label}: ${what} has a where condition (a partial index), which is not preserved; it was read as a full index.`);
1641
+ }
1642
+ let indexMethod;
1643
+ if (method !== undefined) {
1644
+ indexMethod = INDEX_METHODS[method.toLowerCase()];
1645
+ if (indexMethod === undefined) {
1646
+ project.warnings.push(`${label}: ${what} uses the "${method}" access method, which is not represented in the shared model; the default method is used.`);
1647
+ }
1648
+ }
1649
+ const fieldOptions = {};
1650
+ for (const ref of refs) {
1651
+ if (ref.sort !== undefined || ref.ops !== undefined) {
1652
+ fieldOptions[ref.key] = {
1653
+ ...(ref.sort === undefined ? {} : { sort: ref.sort }),
1654
+ ...(ref.ops === undefined ? {} : { ops: ref.ops }),
1655
+ };
1656
+ }
1657
+ }
1658
+ table.indexes.push({
1659
+ columns: keys,
1660
+ isUnique,
1661
+ name,
1662
+ method: indexMethod,
1663
+ fieldOptions,
1664
+ isPartial,
1665
+ });
1666
+ }
1667
+ // ---------------------------------------------------------------------------
1668
+ // Relations
1669
+ // ---------------------------------------------------------------------------
1670
+ function sameList(first, second) {
1671
+ return (first.length === second.length &&
1672
+ first.every((item, index) => item === second[index]));
1673
+ }
1674
+ /** The relation name for a foreign key declared only through `.references()`: `authorId` -> `author`. */
1675
+ function derivedRelationName(table, foreignKey) {
1676
+ const key = foreignKey.columns[0] ?? '';
1677
+ const stripped = key.replace(/(_id|Id|ID)$/, '');
1678
+ const taken = new Set(table.columns
1679
+ .filter((column) => !foreignKey.columns.includes(column.key))
1680
+ .map((column) => column.key));
1681
+ if (foreignKey.columns.length > 1) {
1682
+ return toCamelCase(modelNameOf(foreignKey.targetVar));
1683
+ }
1684
+ return stripped === '' || stripped === key || taken.has(stripped)
1685
+ ? key
1686
+ : stripped;
1687
+ }
1688
+ /**
1689
+ * Builds the owning side of every relation: a foreign key, named by the `one(...)` entry of the
1690
+ * table's `relations()` when there is one, otherwise by the column it is declared on.
1691
+ */
1692
+ function collectOwners(tables, byVar, project) {
1693
+ const owners = [];
1694
+ for (const table of tables) {
1695
+ const label = tableLabel(table);
1696
+ const used = new Set();
1697
+ const defs = project.relationDefs.filter((def) => def.tableVar === table.decl.varName);
1698
+ for (const def of defs) {
1699
+ for (const entry of def.entries) {
1700
+ if (entry.kind !== 'one' || entry.fields === undefined) {
1701
+ continue;
1702
+ }
1703
+ const keys = entry.fields.map((ref) => ref.key);
1704
+ const references = (entry.references ?? []).map((ref) => ref.key);
1705
+ const missing = keys.filter((key) => !table.columns.some((column) => column.key === key));
1706
+ if (keys.length === 0 || missing.length > 0) {
1707
+ project.warnings.push(`${label}: the relation "${entry.key}" uses fields (${keys.join(', ')}) that are not columns of the table; it was skipped.`);
1708
+ continue;
1709
+ }
1710
+ let foreignKey = table.foreignKeys.find((candidate) => !used.has(candidate) &&
1711
+ sameList(candidate.columns, keys) &&
1712
+ candidate.targetVar === entry.targetVar);
1713
+ if (foreignKey === undefined) {
1714
+ // A relation without a database constraint (Drizzle relations are an application-level feature).
1715
+ foreignKey = {
1716
+ columns: keys,
1717
+ targetVar: entry.targetVar,
1718
+ targetColumns: references.length > 0
1719
+ ? references
1720
+ : primaryKeyOf(byVar.get(entry.targetVar)),
1721
+ onDelete: undefined,
1722
+ onUpdate: undefined,
1723
+ name: undefined,
1724
+ };
1725
+ table.foreignKeys.push(foreignKey);
1726
+ }
1727
+ used.add(foreignKey);
1728
+ owners.push({
1729
+ table,
1730
+ name: entry.key,
1731
+ foreignKey,
1732
+ relationName: entry.relationName,
1733
+ reverseName: undefined,
1734
+ isDeclared: true,
1735
+ });
1736
+ }
1737
+ }
1738
+ for (const foreignKey of table.foreignKeys) {
1739
+ if (!used.has(foreignKey)) {
1740
+ owners.push({
1741
+ table,
1742
+ name: derivedRelationName(table, foreignKey),
1743
+ foreignKey,
1744
+ relationName: undefined,
1745
+ reverseName: undefined,
1746
+ isDeclared: false,
1747
+ });
1748
+ }
1749
+ }
1750
+ }
1751
+ return owners;
1752
+ }
1753
+ function primaryKeyOf(table) {
1754
+ if (table === undefined) {
1755
+ return ['id'];
1756
+ }
1757
+ if (table.primaryKey !== undefined) {
1758
+ return table.primaryKey.columns;
1759
+ }
1760
+ const keys = table.columns
1761
+ .filter((column) => column.field.isPrimaryKey)
1762
+ .map((column) => column.key);
1763
+ return keys.length > 0 ? keys : ['id'];
1764
+ }
1765
+ /** Links the `many(...)` and field-less `one(...)` entries to the owning side they mirror. */
1766
+ function linkReverseSides(tables, byVar, owners, project) {
1767
+ for (const def of project.relationDefs) {
1768
+ const table = byVar.get(def.tableVar);
1769
+ if (table === undefined) {
1770
+ project.warnings.push(`${def.file.label}: relations() are defined for "${def.tableVar}", which is not a table in the input; they were skipped.`);
1771
+ continue;
1772
+ }
1773
+ for (const entry of def.entries) {
1774
+ if (entry.kind === 'one' && entry.fields !== undefined) {
1775
+ continue;
1776
+ }
1777
+ const location = `${tableLabel(table)}: the relation "${entry.key}"`;
1778
+ const candidates = owners.filter((owner) => owner.table.decl.varName === entry.targetVar &&
1779
+ owner.foreignKey.targetVar === table.decl.varName &&
1780
+ owner.reverseName === undefined &&
1781
+ (entry.relationName === undefined ||
1782
+ owner.relationName === entry.relationName));
1783
+ if (!byVar.has(entry.targetVar)) {
1784
+ project.warnings.push(`${location} points at "${entry.targetVar}", which is not a table in the input; the reverse side was skipped.`);
1785
+ continue;
1786
+ }
1787
+ // Drizzle only knows the relations written as one(...); a bare foreign key is a fallback.
1788
+ const declared = candidates.filter((owner) => owner.isDeclared);
1789
+ const matching = declared.length > 0 ? declared : candidates;
1790
+ const owner = matching[0];
1791
+ if (owner === undefined) {
1792
+ project.warnings.push(`${location} has no matching one(${table.decl.varName}, { fields, references }) on "${entry.targetVar}"` +
1793
+ `${entry.relationName === undefined ? '' : ` with relationName "${entry.relationName}"`}; the reverse side was not linked.`);
1794
+ continue;
1795
+ }
1796
+ if (matching.length > 1) {
1797
+ project.warnings.push(`${location} matches several relations of "${entry.targetVar}" (${matching.map((candidate) => candidate.name).join(', ')}); add a relationName to tell them apart. It was linked to "${owner.name}".`);
1798
+ }
1799
+ owner.reverseName = entry.key;
1800
+ }
1801
+ }
1802
+ }
1803
+ // ---------------------------------------------------------------------------
1804
+ // Models
1805
+ // ---------------------------------------------------------------------------
1806
+ function finalizeModel(table, byVar, owners, warnings, options) {
1807
+ const label = tableLabel(table);
1808
+ const fields = table.columns.map((column) => column.field);
1809
+ const fieldByKey = new Map(fields.map((field) => [field.name, field]));
1810
+ const tableOwners = owners.filter((owner) => owner.table === table);
1811
+ // Which keys form the primary key, and which single columns are unique.
1812
+ const pkKeys = table.primaryKey !== undefined
1813
+ ? table.primaryKey.columns
1814
+ : fields
1815
+ .filter((field) => field.isPrimaryKey)
1816
+ .map((field) => field.name);
1817
+ const uniqueKeys = new Set(fields
1818
+ .filter((field) => field.isUnique)
1819
+ .map((field) => field.name));
1820
+ for (const index of table.indexes) {
1821
+ const only = index.columns[0];
1822
+ if (index.isUnique &&
1823
+ index.columns.length === 1 &&
1824
+ only !== undefined &&
1825
+ !index.isPartial) {
1826
+ uniqueKeys.add(only);
1827
+ }
1828
+ }
1829
+ const relations = [];
1830
+ const compositeForeignKeys = [];
1831
+ /** Column key -> the relation that replaced it. */
1832
+ const renamed = new Map();
1833
+ const mergedKeys = new Set();
1834
+ const takenNames = new Set();
1835
+ for (const owner of tableOwners) {
1836
+ const foreignKey = owner.foreignKey;
1837
+ const target = byVar.get(foreignKey.targetVar);
1838
+ const targetModel = target === undefined
1839
+ ? modelNameOf(foreignKey.targetVar)
1840
+ : target.modelName;
1841
+ const targetPk = primaryKeyOf(target);
1842
+ const columns = foreignKey.columns.flatMap((key) => {
1843
+ const column = table.columns.find((candidate) => candidate.key === key);
1844
+ return column === undefined ? [] : [column];
1845
+ });
1846
+ if (foreignKey.columns.length > 1) {
1847
+ const references = foreignKey.targetColumns;
1848
+ compositeForeignKeys.push({
1849
+ name: owner.name,
1850
+ targetModel,
1851
+ fields: foreignKey.columns,
1852
+ references,
1853
+ kind: 'foreignKey',
1854
+ isNullable: columns.some((column) => !column.isNotNull),
1855
+ onDelete: foreignKey.onDelete ?? 'noAction',
1856
+ ...(foreignKey.onUpdate === undefined
1857
+ ? {}
1858
+ : { onUpdate: foreignKey.onUpdate }),
1859
+ ...(owner.reverseName === undefined
1860
+ ? {}
1861
+ : { relatedName: owner.reverseName }),
1862
+ ...(foreignKey.name === undefined
1863
+ ? {}
1864
+ : { constraintName: foreignKey.name }),
1865
+ });
1866
+ continue;
1867
+ }
1868
+ const backing = columns[0];
1869
+ if (backing === undefined || mergedKeys.has(backing.key)) {
1870
+ warnings.push(`${label}: the foreign key on "${foreignKey.columns.join(', ')}" ${backing === undefined ? 'refers to a column that does not exist' : 'repeats a column that already has a foreign key'}; it was skipped.`);
1871
+ continue;
1872
+ }
1873
+ let name = owner.name;
1874
+ if (takenNames.has(name)) {
1875
+ name = backing.key;
1876
+ }
1877
+ takenNames.add(name);
1878
+ mergedKeys.add(backing.key);
1879
+ renamed.set(backing.key, name);
1880
+ const isPrimary = pkKeys.length === 1 && pkKeys[0] === backing.key;
1881
+ const referenced = foreignKey.targetColumns[0];
1882
+ const isOneToOne = isPrimary || uniqueKeys.has(backing.key);
1883
+ relations.push({
1884
+ name,
1885
+ kind: isOneToOne ? 'oneToOne' : 'foreignKey',
1886
+ targetModel,
1887
+ columnName: backing.field.columnName,
1888
+ isNullable: !backing.isNotNull && !isPrimary,
1889
+ onDelete: foreignKey.onDelete ?? 'noAction',
1890
+ ...(owner.reverseName === undefined
1891
+ ? {}
1892
+ : { relatedName: owner.reverseName }),
1893
+ ...(referenced === undefined ||
1894
+ target === undefined ||
1895
+ sameList(targetPk, [referenced])
1896
+ ? {}
1897
+ : { toField: referenced }),
1898
+ ...(isPrimary ? { isPrimaryKey: true } : {}),
1899
+ ...(foreignKey.onUpdate === undefined
1900
+ ? {}
1901
+ : { onUpdate: foreignKey.onUpdate }),
1902
+ ...(foreignKey.name === undefined
1903
+ ? {}
1904
+ : { constraintName: foreignKey.name }),
1905
+ });
1906
+ }
1907
+ const finalFields = fields.filter((field) => !mergedKeys.has(field.name));
1908
+ // Indexes, with columns that became relations renamed.
1909
+ const known = new Set([
1910
+ ...finalFields.map((field) => field.name),
1911
+ ...relations.map((relation) => relation.name),
1912
+ ]);
1913
+ const indexes = [];
1914
+ for (const raw of table.indexes) {
1915
+ const names = raw.columns.map((key) => renamed.get(key) ?? key);
1916
+ if (names.some((name) => !known.has(name))) {
1917
+ continue;
1918
+ }
1919
+ const single = names.length === 1
1920
+ ? finalFields.find((field) => field.name === names[0])
1921
+ : undefined;
1922
+ const onRelation = names.length === 1 &&
1923
+ relations.some((relation) => relation.name === names[0]);
1924
+ if (raw.isUnique && onRelation && !raw.isPartial) {
1925
+ // A unique foreign key is a one-to-one relation, which already implies the constraint.
1926
+ continue;
1927
+ }
1928
+ if (raw.isUnique &&
1929
+ raw.name === undefined &&
1930
+ single !== undefined &&
1931
+ !single.isPrimaryKey &&
1932
+ !raw.isPartial) {
1933
+ single.isUnique = true;
1934
+ continue;
1935
+ }
1936
+ const fieldOptions = {};
1937
+ for (const [key, entry] of Object.entries(raw.fieldOptions)) {
1938
+ fieldOptions[renamed.get(key) ?? key] = entry;
1939
+ }
1940
+ indexes.push({
1941
+ fields: names,
1942
+ isUnique: raw.isUnique,
1943
+ ...(raw.name === undefined ? {} : { name: raw.name }),
1944
+ ...(raw.method === undefined ? {} : { method: raw.method }),
1945
+ ...(Object.keys(fieldOptions).length === 0 ? {} : { fieldOptions }),
1946
+ });
1947
+ }
1948
+ // Primary key.
1949
+ let compositePrimaryKey;
1950
+ if (pkKeys.length === 0) {
1951
+ warnings.push(`${label}: the table has no primary key; most ORMs need one, so add .primaryKey() to a column or a primaryKey({ columns: [...] }) constraint.`);
1952
+ }
1953
+ else if (pkKeys.length > 1) {
1954
+ compositePrimaryKey = pkKeys.map((key) => renamed.get(key) ?? key);
1955
+ for (const key of pkKeys) {
1956
+ const field = fieldByKey.get(key);
1957
+ if (field !== undefined) {
1958
+ field.isPrimaryKey = false;
1959
+ field.isUnique = false;
1960
+ }
1961
+ }
1962
+ }
1963
+ else if (table.primaryKey !== undefined) {
1964
+ const field = finalFields.find((candidate) => candidate.name === table.primaryKey?.columns[0]);
1965
+ if (field !== undefined) {
1966
+ field.isPrimaryKey = true;
1967
+ field.isNullable = false;
1968
+ }
1969
+ }
1970
+ for (const relation of relations) {
1971
+ if (compositePrimaryKey !== undefined) {
1972
+ delete relation.isPrimaryKey;
1973
+ }
1974
+ }
1975
+ return {
1976
+ name: table.modelName,
1977
+ tableName: table.dbName,
1978
+ appLabel: options.appLabel,
1979
+ fields: finalFields,
1980
+ relations,
1981
+ indexes,
1982
+ ...(compositePrimaryKey === undefined ? {} : { compositePrimaryKey }),
1983
+ ...(table.primaryKey?.name !== undefined &&
1984
+ compositePrimaryKey !== undefined
1985
+ ? { primaryKeyName: table.primaryKey.name }
1986
+ : {}),
1987
+ ...(table.decl.schema === undefined ? {} : { schema: table.decl.schema }),
1988
+ ...(compositeForeignKeys.length === 0 ? {} : { compositeForeignKeys }),
1989
+ };
1990
+ }
1991
+ function addStubModels(models, warnings, options) {
1992
+ const known = new Set(models.map((model) => model.name));
1993
+ const stubs = [];
1994
+ const targets = [];
1995
+ for (const model of models) {
1996
+ for (const relation of model.relations) {
1997
+ targets.push({
1998
+ from: model.name,
1999
+ via: relation.name,
2000
+ target: relation.targetModel,
2001
+ });
2002
+ }
2003
+ for (const composite of model.compositeForeignKeys ?? []) {
2004
+ targets.push({
2005
+ from: model.name,
2006
+ via: composite.name,
2007
+ target: composite.targetModel,
2008
+ });
2009
+ }
2010
+ }
2011
+ for (const { from, via, target } of targets) {
2012
+ if (known.has(target)) {
2013
+ continue;
2014
+ }
2015
+ known.add(target);
2016
+ stubs.push({
2017
+ name: target,
2018
+ tableName: toSnakeCase(target),
2019
+ appLabel: options.appLabel,
2020
+ fields: [
2021
+ {
2022
+ name: 'id',
2023
+ columnName: 'id',
2024
+ type: 'int',
2025
+ isPrimaryKey: true,
2026
+ isUnique: false,
2027
+ isNullable: false,
2028
+ isAutoUpdated: false,
2029
+ default: { kind: 'autoIncrement' },
2030
+ },
2031
+ ],
2032
+ relations: [],
2033
+ indexes: [],
2034
+ });
2035
+ warnings.push(`${from}.${via} references "${target}", which is not a table in the input. ` +
2036
+ `A stub model with an auto-increment id was generated; replace it with the real definition.`);
2037
+ }
2038
+ models.push(...stubs);
2039
+ }
2040
+ //# sourceMappingURL=drizzle.js.map