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,2302 @@
1
+ import { basename } from 'node:path';
2
+ import { singularize, toPascalCase, toSnakeCase } from '../naming.js';
3
+ import { describeThrown, err, ok } from '../result.js';
4
+ import { Cursor, detectDialect, MAX_SOURCE_CHARACTERS, sourceOf, splitStatements, splitTopLevel, tokenize, } from './sqlSyntax.js';
5
+ import { defaultExpression, interpretDefault, mapType, parseType, } from './sqlTypes.js';
6
+ const DEFAULT_SCHEMAS = new Set(['public', 'dbo', 'main']);
7
+ const MAX_WARNINGS = 400;
8
+ /** An enum with more members than this takes ALTER TYPE ... ADD VALUE ... BEFORE / AFTER at the end. */
9
+ const MAX_POSITIONED_ENUM_VALUES = 10_000;
10
+ function warn(ctx, message) {
11
+ if (ctx.warnings.length < MAX_WARNINGS) {
12
+ ctx.warnings.push(message);
13
+ }
14
+ else {
15
+ ctx.suppressedWarnings += 1;
16
+ }
17
+ }
18
+ function recordSkipped(ctx, file, kind, line) {
19
+ const entry = ctx.skipped.get(kind);
20
+ if (entry === undefined) {
21
+ ctx.skipped.set(kind, { count: 1, first: `${file.name}:${line}` });
22
+ }
23
+ else {
24
+ entry.count += 1;
25
+ }
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // Public entry point
29
+ // ---------------------------------------------------------------------------
30
+ /**
31
+ * Reads SQL DDL (PostgreSQL, MySQL / MariaDB, SQLite and SQL Server) into the shared model. Never throws:
32
+ * input it cannot read becomes a warning, or an error result when no table was found at all.
33
+ */
34
+ export function parseSqlDdl(sources, options) {
35
+ try {
36
+ return parseSources(sources, options);
37
+ }
38
+ catch (thrown) {
39
+ return err('PARSE_FAILED', `The SQL input could not be converted: ${describeThrown(thrown)}`);
40
+ }
41
+ }
42
+ function parseSources(sources, options) {
43
+ const totalCharacters = sources.reduce((sum, source) => sum + source.text.length, 0);
44
+ if (totalCharacters > MAX_SOURCE_CHARACTERS) {
45
+ return err('PARSE_FAILED', `The SQL input is too large (${totalCharacters} characters; the limit is ${MAX_SOURCE_CHARACTERS}). Split it into smaller files.`);
46
+ }
47
+ const ctx = {
48
+ tables: [],
49
+ tableByKey: new Map(),
50
+ tablesByName: new Map(),
51
+ enums: [],
52
+ enumsByName: new Map(),
53
+ warnings: [],
54
+ suppressedWarnings: 0,
55
+ skipped: new Map(),
56
+ };
57
+ for (const source of sources) {
58
+ const dialect = options.dialect ?? detectDialect(source.text);
59
+ const file = {
60
+ name: basename(source.path),
61
+ text: source.text,
62
+ dialect,
63
+ };
64
+ const scanned = tokenize(source.text, dialect);
65
+ if (scanned.truncated) {
66
+ warn(ctx, `${file.name}: the file has too many tokens; everything after the first ${scanned.tokens.length} was ignored.`);
67
+ }
68
+ for (const statement of splitStatements(scanned.tokens)) {
69
+ try {
70
+ handleStatement(ctx, file, statement);
71
+ }
72
+ catch (thrown) {
73
+ warn(ctx, `${file.name}:${statement[0]?.line ?? 0}: a statement could not be read (${describeThrown(thrown)}) and was skipped.`);
74
+ }
75
+ }
76
+ }
77
+ for (const [kind, entry] of ctx.skipped) {
78
+ warn(ctx, `Skipped ${entry.count} ${kind} statement${entry.count === 1 ? '' : 's'} (first at ${entry.first}): ${describeSkipReason(kind)}`);
79
+ }
80
+ const liveTables = ctx.tables.filter((table) => !table.dropped);
81
+ if (liveTables.length === 0) {
82
+ return err('NO_MODELS_FOUND', 'No CREATE TABLE statements were found in the SQL input.');
83
+ }
84
+ const schema = buildSchema(ctx, liveTables, options);
85
+ if (ctx.suppressedWarnings > 0) {
86
+ schema.warnings.push(`${ctx.suppressedWarnings} more warnings were not shown.`);
87
+ }
88
+ return ok(schema);
89
+ }
90
+ function describeSkipReason(kind) {
91
+ if (kind === 'data') {
92
+ return 'row data is not part of the schema.';
93
+ }
94
+ if (/^CREATE (OR REPLACE )?(MATERIALIZED )?VIEW/.test(kind)) {
95
+ return 'views are not read.';
96
+ }
97
+ if (kind.startsWith('ALTER TABLE')) {
98
+ return 'the change is not replayed, so the result may differ from the final database.';
99
+ }
100
+ return 'it has no equivalent in the shared model.';
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Statements
104
+ // ---------------------------------------------------------------------------
105
+ /** Statements that never matter to the schema and are skipped without a warning. */
106
+ const QUIET_STATEMENTS = new Set([
107
+ 'SET',
108
+ 'USE',
109
+ 'GRANT',
110
+ 'REVOKE',
111
+ 'BEGIN',
112
+ 'START',
113
+ 'COMMIT',
114
+ 'END',
115
+ 'ROLLBACK',
116
+ 'SAVEPOINT',
117
+ 'RELEASE',
118
+ 'PRAGMA',
119
+ 'LOCK',
120
+ 'UNLOCK',
121
+ 'COMMENT',
122
+ 'ANALYZE',
123
+ 'VACUUM',
124
+ 'REINDEX',
125
+ 'CLUSTER',
126
+ 'RESET',
127
+ 'PRINT',
128
+ 'DECLARE',
129
+ 'GO',
130
+ 'SHOW',
131
+ 'DO',
132
+ 'EXEC',
133
+ 'EXECUTE',
134
+ 'CHECKPOINT',
135
+ 'DISCARD',
136
+ ]);
137
+ /** Data statements, reported together as one warning. */
138
+ const DATA_STATEMENTS = new Set([
139
+ 'INSERT',
140
+ 'UPDATE',
141
+ 'DELETE',
142
+ 'SELECT',
143
+ 'COPY',
144
+ 'TRUNCATE',
145
+ 'MERGE',
146
+ 'REPLACE',
147
+ 'WITH',
148
+ 'VALUES',
149
+ ]);
150
+ /** CREATE statements with no schema meaning for this reader and no value in warning about. */
151
+ const QUIET_CREATES = new Set([
152
+ 'EXTENSION',
153
+ 'SCHEMA',
154
+ 'DATABASE',
155
+ 'ROLE',
156
+ 'USER',
157
+ 'TABLESPACE',
158
+ 'LANGUAGE',
159
+ 'COLLATION',
160
+ 'SERVER',
161
+ 'LOGIN',
162
+ 'CAST',
163
+ 'OPERATOR',
164
+ 'AGGREGATE',
165
+ 'PUBLICATION',
166
+ 'SUBSCRIPTION',
167
+ 'POLICY',
168
+ 'RULE',
169
+ ]);
170
+ function handleStatement(ctx, file, tokens) {
171
+ const cursor = new Cursor(tokens);
172
+ const head = cursor.peek();
173
+ const line = head.line;
174
+ if (head.kind !== 'word') {
175
+ recordSkipped(ctx, file, 'unrecognised', line);
176
+ return;
177
+ }
178
+ if (QUIET_STATEMENTS.has(head.up)) {
179
+ return;
180
+ }
181
+ if (DATA_STATEMENTS.has(head.up)) {
182
+ recordSkipped(ctx, file, 'data', line);
183
+ return;
184
+ }
185
+ switch (head.up) {
186
+ case 'CREATE':
187
+ handleCreate(ctx, file, cursor);
188
+ return;
189
+ case 'ALTER':
190
+ handleAlter(ctx, file, cursor);
191
+ return;
192
+ case 'DROP':
193
+ handleDrop(ctx, file, cursor);
194
+ return;
195
+ default:
196
+ recordSkipped(ctx, file, head.up, line);
197
+ }
198
+ }
199
+ function handleCreate(ctx, file, cursor) {
200
+ const line = cursor.peek().line;
201
+ cursor.next();
202
+ let isTemporary = false;
203
+ let modifiers = '';
204
+ for (;;) {
205
+ const word = cursor.peek();
206
+ if (word.kind !== 'word') {
207
+ break;
208
+ }
209
+ if (word.up === 'OR' && cursor.isKeyword('REPLACE', 1)) {
210
+ modifiers += 'OR REPLACE ';
211
+ cursor.next();
212
+ cursor.next();
213
+ }
214
+ else if (word.up === 'TEMP' || word.up === 'TEMPORARY') {
215
+ isTemporary = true;
216
+ cursor.next();
217
+ }
218
+ else if (word.up === 'GLOBAL' ||
219
+ word.up === 'LOCAL' ||
220
+ word.up === 'UNLOGGED' ||
221
+ word.up === 'MATERIALIZED') {
222
+ if (word.up === 'MATERIALIZED') {
223
+ modifiers += 'MATERIALIZED ';
224
+ }
225
+ cursor.next();
226
+ }
227
+ else {
228
+ break;
229
+ }
230
+ }
231
+ const kind = cursor.peek();
232
+ if (kind.kind !== 'word') {
233
+ recordSkipped(ctx, file, 'unrecognised', line);
234
+ return;
235
+ }
236
+ if (kind.up === 'TABLE') {
237
+ if (isTemporary) {
238
+ recordSkipped(ctx, file, 'CREATE TEMPORARY TABLE', line);
239
+ return;
240
+ }
241
+ createTable(ctx, file, cursor, line);
242
+ return;
243
+ }
244
+ if (kind.up === 'INDEX' ||
245
+ kind.up === 'UNIQUE' ||
246
+ kind.up === 'FULLTEXT' ||
247
+ kind.up === 'SPATIAL' ||
248
+ kind.up === 'CLUSTERED' ||
249
+ kind.up === 'NONCLUSTERED' ||
250
+ kind.up === 'COLUMNSTORE' ||
251
+ kind.up === 'BITMAP') {
252
+ createIndex(ctx, file, cursor, line);
253
+ return;
254
+ }
255
+ if (kind.up === 'TYPE') {
256
+ createType(ctx, file, cursor, line);
257
+ return;
258
+ }
259
+ if (QUIET_CREATES.has(kind.up)) {
260
+ return;
261
+ }
262
+ recordSkipped(ctx, file, `CREATE ${modifiers}${kind.up}`, line);
263
+ }
264
+ function parseQualifiedName(cursor) {
265
+ const parts = [];
266
+ for (;;) {
267
+ const token = cursor.peek();
268
+ if (token.kind !== 'word' && token.kind !== 'ident') {
269
+ break;
270
+ }
271
+ parts.push(token.value);
272
+ cursor.next();
273
+ if (cursor.isPunct('.')) {
274
+ cursor.next();
275
+ // SQL Server allows an omitted schema: db..table.
276
+ while (cursor.isPunct('.')) {
277
+ cursor.next();
278
+ }
279
+ continue;
280
+ }
281
+ break;
282
+ }
283
+ const name = parts[parts.length - 1];
284
+ if (name === undefined) {
285
+ return undefined;
286
+ }
287
+ const schema = parts[parts.length - 2];
288
+ return schema === undefined ? { name } : { schema, name };
289
+ }
290
+ /** MySQL names a database where other systems name a schema, so the qualifier is not kept. */
291
+ function normalizedSchema(name, dialect) {
292
+ return dialect === 'mysql' ? undefined : name.schema;
293
+ }
294
+ function tableKey(schema, name) {
295
+ return `${(schema ?? '').toLowerCase()}.${name.toLowerCase()}`;
296
+ }
297
+ const CONSTRAINT_START_WORDS = new Set([
298
+ 'NOT',
299
+ 'NULL',
300
+ 'DEFAULT',
301
+ 'PRIMARY',
302
+ 'UNIQUE',
303
+ 'REFERENCES',
304
+ 'CHECK',
305
+ 'CONSTRAINT',
306
+ 'COLLATE',
307
+ 'GENERATED',
308
+ 'AS',
309
+ 'AUTOINCREMENT',
310
+ 'AUTO_INCREMENT',
311
+ 'IDENTITY',
312
+ 'COMMENT',
313
+ 'ON',
314
+ ]);
315
+ function createTable(ctx, file, cursor, line) {
316
+ cursor.next();
317
+ const ifNotExists = cursor.acceptKeywords('IF', 'NOT', 'EXISTS');
318
+ const name = parseQualifiedName(cursor);
319
+ if (name === undefined) {
320
+ recordSkipped(ctx, file, 'unrecognised', line);
321
+ return;
322
+ }
323
+ const label = name.name;
324
+ if (!cursor.isPunct('(')) {
325
+ recordSkipped(ctx, file, cursor.isKeyword('AS')
326
+ ? 'CREATE TABLE ... AS SELECT'
327
+ : cursor.isKeyword('PARTITION')
328
+ ? 'CREATE TABLE ... PARTITION OF'
329
+ : 'CREATE TABLE without a column list', line);
330
+ return;
331
+ }
332
+ const group = cursor.readGroup() ?? [];
333
+ const table = {
334
+ ...(normalizedSchema(name, file.dialect) === undefined
335
+ ? {}
336
+ : { schema: normalizedSchema(name, file.dialect) }),
337
+ name: name.name,
338
+ columns: [],
339
+ columnIndex: new Map(),
340
+ uniques: [],
341
+ references: [],
342
+ indexes: [],
343
+ checks: [],
344
+ withoutRowid: false,
345
+ dialect: file.dialect,
346
+ line,
347
+ file: file.name,
348
+ dropped: false,
349
+ };
350
+ // Table options after the column list.
351
+ while (!cursor.atEnd) {
352
+ const token = cursor.next();
353
+ if (token.kind !== 'word') {
354
+ continue;
355
+ }
356
+ if (token.up === 'WITHOUT' && cursor.isKeyword('ROWID')) {
357
+ table.withoutRowid = true;
358
+ }
359
+ else if (token.up === 'INHERITS' && cursor.isPunct('(')) {
360
+ warn(ctx, `table "${label}": INHERITS is not read; the columns of the parent table are not copied.`);
361
+ }
362
+ else if (token.up === 'PARTITION' && cursor.isKeyword('BY')) {
363
+ warn(ctx, `table "${label}": the partitioning clause is not read; the table is read as an ordinary table.`);
364
+ }
365
+ }
366
+ for (const element of splitTopLevel(group)) {
367
+ const parser = new Cursor(element);
368
+ if (isConstraintStart(parser, file.dialect)) {
369
+ if (parser.isKeyword('LIKE')) {
370
+ warn(ctx, `table "${label}": LIKE copies columns from another table and is not read.`);
371
+ continue;
372
+ }
373
+ applyConstraint(ctx, table, parseTableConstraint(ctx, file, parser, label));
374
+ continue;
375
+ }
376
+ const column = parseColumn(ctx, file, parser, table, label);
377
+ if (column === undefined) {
378
+ warn(ctx, `table "${label}": ${file.name}:${element[0]?.line ?? line}: a column definition could not be read and was skipped.`);
379
+ }
380
+ }
381
+ registerTable(ctx, table, ifNotExists);
382
+ }
383
+ function registerTable(ctx, table, ifNotExists) {
384
+ const key = tableKey(table.schema, table.name);
385
+ const existing = ctx.tableByKey.get(key);
386
+ if (existing !== undefined && !existing.dropped) {
387
+ if (ifNotExists) {
388
+ return;
389
+ }
390
+ warn(ctx, `table "${table.name}" is created twice (${existing.file}:${existing.line} and ${table.file}:${table.line}); the later definition is used.`);
391
+ existing.dropped = true;
392
+ }
393
+ ctx.tables.push(table);
394
+ ctx.tableByKey.set(key, table);
395
+ const byName = ctx.tablesByName.get(table.name.toLowerCase()) ?? [];
396
+ byName.push(table);
397
+ ctx.tablesByName.set(table.name.toLowerCase(), byName);
398
+ }
399
+ function isConstraintStart(cursor, dialect) {
400
+ const head = cursor.peek();
401
+ if (head.kind !== 'word') {
402
+ return false;
403
+ }
404
+ const next = cursor.peek(1);
405
+ switch (head.up) {
406
+ case 'CONSTRAINT':
407
+ return true;
408
+ case 'PRIMARY':
409
+ case 'FOREIGN':
410
+ return cursor.isKeyword('KEY', 1);
411
+ case 'UNIQUE':
412
+ return (cursor.isPunct('(', 1) ||
413
+ cursor.isKeyword('KEY', 1) ||
414
+ cursor.isKeyword('INDEX', 1) ||
415
+ cursor.isKeyword('CLUSTERED', 1) ||
416
+ cursor.isKeyword('NONCLUSTERED', 1) ||
417
+ ((next.kind === 'word' || next.kind === 'ident') &&
418
+ cursor.isPunct('(', 2)));
419
+ case 'CHECK':
420
+ return cursor.isPunct('(', 1);
421
+ case 'EXCLUDE':
422
+ return cursor.isKeyword('USING', 1) || cursor.isPunct('(', 1);
423
+ case 'LIKE':
424
+ return dialect === 'postgresql';
425
+ case 'PERIOD':
426
+ return cursor.isKeyword('FOR', 1);
427
+ case 'KEY':
428
+ case 'INDEX':
429
+ case 'FULLTEXT':
430
+ case 'SPATIAL':
431
+ return dialect === 'mysql';
432
+ default:
433
+ return false;
434
+ }
435
+ }
436
+ /** Reads the column names of `( a, b DESC, c(10) )`. */
437
+ function readColumnList(cursor) {
438
+ const group = cursor.readGroup();
439
+ if (group === undefined) {
440
+ return [];
441
+ }
442
+ const names = [];
443
+ for (const element of splitTopLevel(group)) {
444
+ const first = element[0];
445
+ if (first !== undefined &&
446
+ (first.kind === 'word' || first.kind === 'ident')) {
447
+ names.push(first.value);
448
+ }
449
+ }
450
+ return names;
451
+ }
452
+ function readIndexElements(group) {
453
+ const elements = [];
454
+ for (const element of splitTopLevel(group)) {
455
+ const first = element[0];
456
+ if (first === undefined) {
457
+ continue;
458
+ }
459
+ const second = element[1];
460
+ const isColumnToken = first.kind === 'ident' ||
461
+ (first.kind === 'word' &&
462
+ !(second?.kind === 'punct' &&
463
+ second.text === '(' &&
464
+ element.length > 4));
465
+ const isPrefixLength = second?.kind === 'punct' &&
466
+ second.text === '(' &&
467
+ element[2]?.kind === 'number' &&
468
+ element[3]?.kind === 'punct' &&
469
+ element[3].text === ')';
470
+ if (!isColumnToken ||
471
+ (second?.kind === 'punct' && second.text === '(' && !isPrefixLength) ||
472
+ (second?.kind === 'punct' && second.text === '.')) {
473
+ elements.push({ isExpression: true, descending: false });
474
+ continue;
475
+ }
476
+ const parsed = {
477
+ column: first.value,
478
+ isExpression: false,
479
+ descending: false,
480
+ };
481
+ let at = 1;
482
+ if (isPrefixLength) {
483
+ parsed.length = Number(element[2]?.value ?? '0');
484
+ at = 4;
485
+ }
486
+ while (at < element.length) {
487
+ const token = element[at];
488
+ if (token === undefined) {
489
+ break;
490
+ }
491
+ if (token.kind === 'word' && token.up === 'DESC') {
492
+ parsed.descending = true;
493
+ }
494
+ else if (token.kind === 'word' && token.up === 'COLLATE') {
495
+ at += 1;
496
+ }
497
+ else if (token.kind === 'word' && token.up === 'NULLS') {
498
+ at += 1;
499
+ }
500
+ else if (token.kind === 'word' &&
501
+ token.up !== 'ASC' &&
502
+ parsed.ops === undefined) {
503
+ parsed.ops = token.value;
504
+ }
505
+ else if (token.kind === 'punct' && token.text === '(') {
506
+ let depth = 0;
507
+ while (at < element.length) {
508
+ const inner = element[at];
509
+ if (inner?.kind === 'punct' && inner.text === '(') {
510
+ depth += 1;
511
+ }
512
+ else if (inner?.kind === 'punct' && inner.text === ')') {
513
+ depth -= 1;
514
+ if (depth === 0) {
515
+ break;
516
+ }
517
+ }
518
+ at += 1;
519
+ }
520
+ }
521
+ at += 1;
522
+ }
523
+ elements.push(parsed);
524
+ }
525
+ return elements;
526
+ }
527
+ const ACTIONS = {
528
+ CASCADE: 'cascade',
529
+ RESTRICT: 'restrict',
530
+ };
531
+ /** Reads `REFERENCES t (cols) MATCH x ON DELETE a ON UPDATE b DEFERRABLE ...` after the keyword. */
532
+ function parseReferences(cursor, columns, name, line) {
533
+ const target = parseQualifiedName(cursor);
534
+ if (target === undefined) {
535
+ return undefined;
536
+ }
537
+ const refColumns = readColumnList(cursor);
538
+ const reference = {
539
+ ...(name === undefined ? {} : { name }),
540
+ columns,
541
+ ...(target.schema === undefined ? {} : { refSchema: target.schema }),
542
+ refTable: target.name,
543
+ refColumns,
544
+ line,
545
+ };
546
+ for (;;) {
547
+ if (cursor.isKeyword('MATCH')) {
548
+ cursor.next();
549
+ cursor.next();
550
+ }
551
+ else if (cursor.isKeyword('ON') &&
552
+ (cursor.isKeyword('DELETE', 1) || cursor.isKeyword('UPDATE', 1)) &&
553
+ (cursor.isKeyword('CASCADE', 2) ||
554
+ cursor.isKeyword('RESTRICT', 2) ||
555
+ cursor.isKeyword('SET', 2) ||
556
+ cursor.isKeyword('NO', 2))) {
557
+ cursor.next();
558
+ const which = cursor.next().up;
559
+ let action;
560
+ if (cursor.acceptKeywords('SET', 'NULL')) {
561
+ action = 'setNull';
562
+ }
563
+ else if (cursor.acceptKeywords('SET', 'DEFAULT')) {
564
+ action = 'setDefault';
565
+ }
566
+ else if (cursor.acceptKeywords('NO', 'ACTION')) {
567
+ action = 'noAction';
568
+ }
569
+ else {
570
+ action = ACTIONS[cursor.next().up] ?? 'noAction';
571
+ }
572
+ if (which === 'DELETE') {
573
+ reference.onDelete = action;
574
+ }
575
+ else {
576
+ reference.onUpdate = action;
577
+ }
578
+ }
579
+ else if (cursor.isKeyword('NOT') &&
580
+ (cursor.isKeyword('DEFERRABLE', 1) || cursor.isKeyword('ENFORCED', 1))) {
581
+ cursor.next();
582
+ cursor.next();
583
+ }
584
+ else if (cursor.isKeyword('DEFERRABLE') ||
585
+ cursor.isKeyword('ENFORCED') ||
586
+ (cursor.isKeyword('INITIALLY') &&
587
+ (cursor.isKeyword('DEFERRED', 1) || cursor.isKeyword('IMMEDIATE', 1)))) {
588
+ cursor.next();
589
+ if (cursor.isKeyword('DEFERRED') || cursor.isKeyword('IMMEDIATE')) {
590
+ cursor.next();
591
+ }
592
+ }
593
+ else {
594
+ break;
595
+ }
596
+ }
597
+ return reference;
598
+ }
599
+ /** Reads a table constraint, or one of the MySQL `KEY` / `FULLTEXT` index forms. */
600
+ function parseTableConstraint(ctx, file, cursor, label) {
601
+ const line = cursor.peek().line;
602
+ let name;
603
+ if (cursor.acceptKeyword('CONSTRAINT')) {
604
+ const candidate = cursor.peek();
605
+ if ((candidate.kind === 'word' &&
606
+ !['PRIMARY', 'UNIQUE', 'FOREIGN', 'CHECK', 'DEFAULT'].includes(candidate.up)) ||
607
+ candidate.kind === 'ident') {
608
+ name = cursor.next().value;
609
+ }
610
+ }
611
+ const head = cursor.peek();
612
+ if (head.kind !== 'word') {
613
+ return { kind: 'ignored' };
614
+ }
615
+ const skipUsing = () => {
616
+ if (cursor.isKeyword('USING') && !cursor.isPunct('(', 1)) {
617
+ cursor.next();
618
+ cursor.next();
619
+ }
620
+ };
621
+ switch (head.up) {
622
+ case 'PRIMARY': {
623
+ cursor.next();
624
+ cursor.next();
625
+ cursor.acceptKeyword('CLUSTERED');
626
+ cursor.acceptKeyword('NONCLUSTERED');
627
+ skipUsing();
628
+ const columns = readColumnList(cursor);
629
+ return {
630
+ kind: 'primaryKey',
631
+ columns,
632
+ ...(name === undefined ? {} : { name }),
633
+ };
634
+ }
635
+ case 'UNIQUE': {
636
+ cursor.next();
637
+ cursor.acceptKeyword('KEY');
638
+ cursor.acceptKeyword('INDEX');
639
+ cursor.acceptKeyword('CLUSTERED');
640
+ cursor.acceptKeyword('NONCLUSTERED');
641
+ const maybeName = cursor.peek();
642
+ if ((maybeName.kind === 'word' || maybeName.kind === 'ident') &&
643
+ !cursor.isPunct('(')) {
644
+ if (name === undefined) {
645
+ name = maybeName.value;
646
+ }
647
+ cursor.next();
648
+ }
649
+ skipUsing();
650
+ const columns = readColumnList(cursor);
651
+ return {
652
+ kind: 'unique',
653
+ columns,
654
+ ...(name === undefined ? {} : { name }),
655
+ };
656
+ }
657
+ case 'FOREIGN': {
658
+ cursor.next();
659
+ cursor.next();
660
+ const maybeName = cursor.peek();
661
+ if ((maybeName.kind === 'word' || maybeName.kind === 'ident') &&
662
+ !cursor.isPunct('(')) {
663
+ if (name === undefined) {
664
+ name = maybeName.value;
665
+ }
666
+ cursor.next();
667
+ }
668
+ const columns = readColumnList(cursor);
669
+ if (!cursor.acceptKeyword('REFERENCES')) {
670
+ warn(ctx, `table "${label}": ${file.name}:${line}: a FOREIGN KEY without REFERENCES was skipped.`);
671
+ return { kind: 'ignored' };
672
+ }
673
+ const reference = parseReferences(cursor, columns, name, line);
674
+ return reference === undefined
675
+ ? { kind: 'ignored' }
676
+ : { kind: 'reference', reference };
677
+ }
678
+ case 'CHECK': {
679
+ cursor.next();
680
+ const group = cursor.readGroup() ?? [];
681
+ return {
682
+ kind: 'check',
683
+ check: {
684
+ ...(name === undefined ? {} : { name }),
685
+ tokens: group,
686
+ text: sourceOf(file.text, group),
687
+ },
688
+ };
689
+ }
690
+ case 'DEFAULT': {
691
+ // SQL Server: ALTER TABLE t ADD CONSTRAINT DF_x DEFAULT (0) FOR col
692
+ cursor.next();
693
+ const expression = [];
694
+ while (!cursor.atEnd && !cursor.isKeyword('FOR')) {
695
+ expression.push(cursor.next());
696
+ }
697
+ cursor.acceptKeyword('FOR');
698
+ const column = cursor.peek();
699
+ if (column.kind === 'word' || column.kind === 'ident') {
700
+ return {
701
+ kind: 'default',
702
+ column: column.value,
703
+ tokens: expression,
704
+ text: defaultExpression(expression, file.text),
705
+ };
706
+ }
707
+ return { kind: 'ignored' };
708
+ }
709
+ case 'KEY':
710
+ case 'INDEX':
711
+ case 'FULLTEXT':
712
+ case 'SPATIAL': {
713
+ cursor.next();
714
+ let kind;
715
+ if (head.up === 'FULLTEXT') {
716
+ kind = 'fulltext';
717
+ }
718
+ else if (head.up === 'SPATIAL') {
719
+ kind = 'spatial';
720
+ }
721
+ cursor.acceptKeyword('KEY');
722
+ cursor.acceptKeyword('INDEX');
723
+ const maybeName = cursor.peek();
724
+ let indexName = name;
725
+ if ((maybeName.kind === 'word' || maybeName.kind === 'ident') &&
726
+ !cursor.isPunct('(') &&
727
+ !(maybeName.kind === 'word' && maybeName.up === 'USING')) {
728
+ indexName = maybeName.value;
729
+ cursor.next();
730
+ }
731
+ skipUsing();
732
+ const group = cursor.readGroup() ?? [];
733
+ return {
734
+ kind: 'index',
735
+ index: {
736
+ ...(indexName === undefined ? {} : { name: indexName }),
737
+ elements: readIndexElements(group),
738
+ isUnique: false,
739
+ ...(kind === undefined ? {} : { kind }),
740
+ isPartial: false,
741
+ line,
742
+ },
743
+ };
744
+ }
745
+ case 'EXCLUDE':
746
+ warn(ctx, `table "${label}": an EXCLUDE constraint has no equivalent in the shared model and was skipped.`);
747
+ return { kind: 'ignored' };
748
+ default:
749
+ return { kind: 'ignored' };
750
+ }
751
+ }
752
+ function applyConstraint(ctx, table, result) {
753
+ switch (result.kind) {
754
+ case 'primaryKey':
755
+ if (result.columns.length === 0) {
756
+ return;
757
+ }
758
+ table.primaryKey = {
759
+ columns: result.columns,
760
+ ...(result.name === undefined ? {} : { name: result.name }),
761
+ };
762
+ return;
763
+ case 'unique':
764
+ if (result.columns.length > 0) {
765
+ table.uniques.push({
766
+ columns: result.columns,
767
+ ...(result.name === undefined ? {} : { name: result.name }),
768
+ });
769
+ }
770
+ return;
771
+ case 'reference':
772
+ table.references.push(result.reference);
773
+ return;
774
+ case 'check':
775
+ table.checks.push(result.check);
776
+ return;
777
+ case 'index':
778
+ table.indexes.push(result.index);
779
+ return;
780
+ case 'default': {
781
+ const column = findColumn(table, result.column);
782
+ if (column === undefined) {
783
+ warn(ctx, `table "${table.name}": a DEFAULT for the unknown column "${result.column}" was ignored.`);
784
+ return;
785
+ }
786
+ column.defaultTokens = result.tokens;
787
+ column.defaultText = result.text;
788
+ return;
789
+ }
790
+ default:
791
+ return;
792
+ }
793
+ }
794
+ /** Words that end a DEFAULT expression when they appear outside parentheses. */
795
+ const DEFAULT_STOP_WORDS = new Set([
796
+ 'NOT',
797
+ 'NULL',
798
+ 'PRIMARY',
799
+ 'UNIQUE',
800
+ 'REFERENCES',
801
+ 'CHECK',
802
+ 'CONSTRAINT',
803
+ 'COLLATE',
804
+ 'GENERATED',
805
+ 'COMMENT',
806
+ 'ON',
807
+ 'AUTO_INCREMENT',
808
+ 'AUTOINCREMENT',
809
+ 'IDENTITY',
810
+ ]);
811
+ function readDefault(cursor) {
812
+ const tokens = [];
813
+ let depth = 0;
814
+ while (!cursor.atEnd) {
815
+ const token = cursor.peek();
816
+ if (depth === 0 &&
817
+ tokens.length > 0 &&
818
+ token.kind === 'word' &&
819
+ DEFAULT_STOP_WORDS.has(token.up)) {
820
+ break;
821
+ }
822
+ if (token.kind === 'punct') {
823
+ if (token.text === '(') {
824
+ depth += 1;
825
+ }
826
+ else if (token.text === ')') {
827
+ depth = Math.max(0, depth - 1);
828
+ }
829
+ }
830
+ tokens.push(cursor.next());
831
+ }
832
+ return tokens;
833
+ }
834
+ /**
835
+ * Reads a column definition and adds it, with the constraints written inline, to the table. Returns
836
+ * undefined when the element does not start with a name.
837
+ */
838
+ function parseColumn(ctx, file, cursor, table, label) {
839
+ const nameToken = cursor.peek();
840
+ if (nameToken.kind !== 'word' && nameToken.kind !== 'ident') {
841
+ return undefined;
842
+ }
843
+ cursor.next();
844
+ const dialect = file.dialect;
845
+ const afterName = cursor.peek();
846
+ const hasNoType = cursor.atEnd ||
847
+ (afterName.kind === 'word' && CONSTRAINT_START_WORDS.has(afterName.up));
848
+ const type = hasNoType
849
+ ? {
850
+ name: '',
851
+ args: [],
852
+ arrayDepth: 0,
853
+ unsigned: false,
854
+ text: '',
855
+ userName: '',
856
+ }
857
+ : parseType(cursor, file.text, dialect);
858
+ const column = {
859
+ name: nameToken.value,
860
+ type,
861
+ notNull: undefined,
862
+ autoIncrement: false,
863
+ onUpdateNow: false,
864
+ line: nameToken.line,
865
+ };
866
+ let pendingName;
867
+ let isPrimaryKey = false;
868
+ while (!cursor.atEnd) {
869
+ const token = cursor.peek();
870
+ if (token.kind !== 'word') {
871
+ // Stray punctuation, strings or a parenthesised option: skip it.
872
+ if (token.kind === 'punct' && token.text === '(') {
873
+ cursor.readGroup();
874
+ }
875
+ else {
876
+ cursor.next();
877
+ }
878
+ continue;
879
+ }
880
+ switch (token.up) {
881
+ case 'CONSTRAINT': {
882
+ cursor.next();
883
+ const candidate = cursor.peek();
884
+ if (candidate.kind === 'word' || candidate.kind === 'ident') {
885
+ if (!(candidate.kind === 'word' &&
886
+ [
887
+ 'NOT',
888
+ 'NULL',
889
+ 'PRIMARY',
890
+ 'UNIQUE',
891
+ 'REFERENCES',
892
+ 'CHECK',
893
+ 'DEFAULT',
894
+ ].includes(candidate.up))) {
895
+ pendingName = candidate.value;
896
+ cursor.next();
897
+ }
898
+ }
899
+ continue;
900
+ }
901
+ case 'NOT':
902
+ cursor.next();
903
+ if (cursor.acceptKeyword('NULL')) {
904
+ column.notNull = true;
905
+ }
906
+ continue;
907
+ case 'NULL':
908
+ cursor.next();
909
+ column.notNull = false;
910
+ continue;
911
+ case 'DEFAULT': {
912
+ cursor.next();
913
+ const tokens = readDefault(cursor);
914
+ column.defaultTokens = tokens;
915
+ column.defaultText = defaultExpression(tokens, file.text);
916
+ continue;
917
+ }
918
+ case 'PRIMARY':
919
+ cursor.next();
920
+ cursor.acceptKeyword('KEY');
921
+ cursor.acceptKeyword('ASC');
922
+ cursor.acceptKeyword('DESC');
923
+ cursor.acceptKeyword('CLUSTERED');
924
+ cursor.acceptKeyword('NONCLUSTERED');
925
+ if (cursor.isKeyword('ON') && cursor.isKeyword('CONFLICT', 1)) {
926
+ cursor.next();
927
+ cursor.next();
928
+ cursor.next();
929
+ }
930
+ isPrimaryKey = true;
931
+ if (pendingName !== undefined && table.primaryKey === undefined) {
932
+ table.primaryKey = { columns: [], name: pendingName };
933
+ }
934
+ pendingName = undefined;
935
+ continue;
936
+ case 'KEY':
937
+ cursor.next();
938
+ isPrimaryKey = true;
939
+ continue;
940
+ case 'UNIQUE': {
941
+ cursor.next();
942
+ cursor.acceptKeyword('KEY');
943
+ cursor.acceptKeyword('INDEX');
944
+ cursor.acceptKeyword('CLUSTERED');
945
+ cursor.acceptKeyword('NONCLUSTERED');
946
+ if (cursor.isKeyword('ON') && cursor.isKeyword('CONFLICT', 1)) {
947
+ cursor.next();
948
+ cursor.next();
949
+ cursor.next();
950
+ }
951
+ table.uniques.push({
952
+ columns: [column.name],
953
+ ...(pendingName === undefined ? {} : { name: pendingName }),
954
+ });
955
+ pendingName = undefined;
956
+ continue;
957
+ }
958
+ case 'REFERENCES': {
959
+ cursor.next();
960
+ const reference = parseReferences(cursor, [column.name], pendingName, nameToken.line);
961
+ if (reference !== undefined) {
962
+ table.references.push(reference);
963
+ }
964
+ pendingName = undefined;
965
+ continue;
966
+ }
967
+ case 'CHECK': {
968
+ cursor.next();
969
+ const group = cursor.readGroup();
970
+ if (group !== undefined) {
971
+ table.checks.push({
972
+ ...(pendingName === undefined ? {} : { name: pendingName }),
973
+ tokens: group,
974
+ text: sourceOf(file.text, group),
975
+ });
976
+ }
977
+ pendingName = undefined;
978
+ continue;
979
+ }
980
+ case 'AUTO_INCREMENT':
981
+ case 'AUTOINCREMENT':
982
+ cursor.next();
983
+ column.autoIncrement = true;
984
+ continue;
985
+ case 'IDENTITY':
986
+ cursor.next();
987
+ column.autoIncrement = true;
988
+ cursor.readGroup();
989
+ continue;
990
+ case 'GENERATED': {
991
+ cursor.next();
992
+ if (!cursor.acceptKeyword('ALWAYS')) {
993
+ cursor.acceptKeywords('BY', 'DEFAULT');
994
+ }
995
+ if (cursor.acceptKeyword('AS')) {
996
+ if (cursor.acceptKeyword('IDENTITY')) {
997
+ column.autoIncrement = true;
998
+ cursor.readGroup();
999
+ }
1000
+ else {
1001
+ readGeneratedExpression(cursor, file, column);
1002
+ }
1003
+ }
1004
+ continue;
1005
+ }
1006
+ case 'AS':
1007
+ cursor.next();
1008
+ readGeneratedExpression(cursor, file, column);
1009
+ continue;
1010
+ case 'COLLATE':
1011
+ cursor.next();
1012
+ cursor.next();
1013
+ continue;
1014
+ case 'CHARACTER':
1015
+ cursor.next();
1016
+ cursor.acceptKeyword('SET');
1017
+ cursor.next();
1018
+ continue;
1019
+ case 'CHARSET':
1020
+ cursor.next();
1021
+ cursor.next();
1022
+ continue;
1023
+ case 'COMMENT':
1024
+ cursor.next();
1025
+ cursor.next();
1026
+ continue;
1027
+ case 'ON':
1028
+ cursor.next();
1029
+ if (cursor.acceptKeyword('UPDATE')) {
1030
+ const target = cursor.next();
1031
+ if (target.kind === 'word' &&
1032
+ [
1033
+ 'CURRENT_TIMESTAMP',
1034
+ 'NOW',
1035
+ 'LOCALTIMESTAMP',
1036
+ 'LOCALTIME',
1037
+ ].includes(target.up)) {
1038
+ column.onUpdateNow = true;
1039
+ }
1040
+ cursor.readGroup();
1041
+ }
1042
+ else {
1043
+ cursor.next();
1044
+ }
1045
+ continue;
1046
+ case 'STORAGE':
1047
+ case 'COMPRESSION':
1048
+ cursor.next();
1049
+ cursor.next();
1050
+ continue;
1051
+ default:
1052
+ cursor.next();
1053
+ }
1054
+ }
1055
+ if (isPrimaryKey) {
1056
+ if (table.primaryKey === undefined) {
1057
+ table.primaryKey = { columns: [column.name] };
1058
+ }
1059
+ else if (table.primaryKey.columns.length === 0) {
1060
+ table.primaryKey.columns = [column.name];
1061
+ }
1062
+ else {
1063
+ warn(ctx, `table "${label}": more than one PRIMARY KEY is declared; "${column.name}" is ignored as a key.`);
1064
+ }
1065
+ }
1066
+ table.columns.push(column);
1067
+ if (!table.columnIndex.has(column.name.toLowerCase())) {
1068
+ table.columnIndex.set(column.name.toLowerCase(), column);
1069
+ }
1070
+ return column;
1071
+ }
1072
+ function readGeneratedExpression(cursor, file, column) {
1073
+ const group = cursor.readGroup();
1074
+ if (group === undefined) {
1075
+ return;
1076
+ }
1077
+ let isStored = false;
1078
+ if (cursor.acceptKeyword('STORED') || cursor.acceptKeyword('PERSISTED')) {
1079
+ isStored = true;
1080
+ }
1081
+ else {
1082
+ cursor.acceptKeyword('VIRTUAL');
1083
+ }
1084
+ column.generated = { expression: sourceOf(file.text, group), isStored };
1085
+ }
1086
+ function createIndex(ctx, file, cursor, line) {
1087
+ let isUnique = false;
1088
+ let kind;
1089
+ let clustered;
1090
+ for (;;) {
1091
+ if (cursor.acceptKeyword('UNIQUE')) {
1092
+ isUnique = true;
1093
+ }
1094
+ else if (cursor.acceptKeyword('FULLTEXT')) {
1095
+ kind = 'fulltext';
1096
+ }
1097
+ else if (cursor.acceptKeyword('SPATIAL')) {
1098
+ kind = 'spatial';
1099
+ }
1100
+ else if (cursor.acceptKeyword('CLUSTERED')) {
1101
+ clustered = true;
1102
+ }
1103
+ else if (cursor.acceptKeyword('NONCLUSTERED')) {
1104
+ clustered = false;
1105
+ }
1106
+ else if (cursor.acceptKeyword('COLUMNSTORE') ||
1107
+ cursor.acceptKeyword('BITMAP')) {
1108
+ continue;
1109
+ }
1110
+ else {
1111
+ break;
1112
+ }
1113
+ }
1114
+ if (!cursor.acceptKeyword('INDEX')) {
1115
+ recordSkipped(ctx, file, 'unrecognised', line);
1116
+ return;
1117
+ }
1118
+ cursor.acceptKeyword('CONCURRENTLY');
1119
+ cursor.acceptKeywords('IF', 'NOT', 'EXISTS');
1120
+ let name;
1121
+ if (!cursor.isKeyword('ON')) {
1122
+ const parsed = parseQualifiedName(cursor);
1123
+ name = parsed?.name;
1124
+ }
1125
+ let method;
1126
+ // MySQL: CREATE INDEX name USING BTREE ON table (...)
1127
+ if (cursor.isKeyword('USING') && cursor.isKeyword('ON', 2)) {
1128
+ cursor.next();
1129
+ method = cursor.next().value.toLowerCase();
1130
+ }
1131
+ if (!cursor.acceptKeyword('ON')) {
1132
+ recordSkipped(ctx, file, 'unrecognised', line);
1133
+ return;
1134
+ }
1135
+ cursor.acceptKeyword('ONLY');
1136
+ const target = parseQualifiedName(cursor);
1137
+ if (target === undefined) {
1138
+ return;
1139
+ }
1140
+ if (cursor.isKeyword('USING') && !cursor.isPunct('(', 1)) {
1141
+ cursor.next();
1142
+ method = cursor.next().value.toLowerCase();
1143
+ }
1144
+ const group = cursor.readGroup();
1145
+ if (group === undefined) {
1146
+ recordSkipped(ctx, file, 'unrecognised', line);
1147
+ return;
1148
+ }
1149
+ // MySQL: USING after the column list.
1150
+ if (cursor.isKeyword('USING') && !cursor.isPunct('(', 1)) {
1151
+ cursor.next();
1152
+ method = cursor.next().value.toLowerCase();
1153
+ }
1154
+ let isPartial = false;
1155
+ while (!cursor.atEnd) {
1156
+ if (cursor.isKeyword('WHERE')) {
1157
+ isPartial = true;
1158
+ break;
1159
+ }
1160
+ if (cursor.isKeyword('INCLUDE') || cursor.isKeyword('WITH')) {
1161
+ cursor.next();
1162
+ cursor.readGroup();
1163
+ }
1164
+ else {
1165
+ cursor.next();
1166
+ }
1167
+ }
1168
+ const table = lookupTable(ctx, target, file.dialect);
1169
+ if (table === undefined) {
1170
+ warn(ctx, `${file.name}:${line}: the index${name === undefined ? '' : ` "${name}"`} is on the unknown table "${target.name}" and was skipped.`);
1171
+ return;
1172
+ }
1173
+ table.indexes.push({
1174
+ ...(name === undefined ? {} : { name }),
1175
+ elements: readIndexElements(group),
1176
+ isUnique,
1177
+ ...(kind === undefined ? {} : { kind }),
1178
+ ...(method === undefined || method === 'btree' ? {} : { method }),
1179
+ ...(clustered === undefined ? {} : { clustered }),
1180
+ isPartial,
1181
+ line,
1182
+ });
1183
+ }
1184
+ function lookupTable(ctx, name, dialect) {
1185
+ const schema = normalizedSchema(name, dialect);
1186
+ const exact = ctx.tableByKey.get(tableKey(schema, name.name));
1187
+ if (exact !== undefined && !exact.dropped) {
1188
+ return exact;
1189
+ }
1190
+ // An unqualified name finds a table in any schema when there is only one candidate.
1191
+ if (schema === undefined) {
1192
+ let only;
1193
+ for (const candidate of ctx.tablesByName.get(name.name.toLowerCase()) ??
1194
+ []) {
1195
+ if (candidate.dropped) {
1196
+ continue;
1197
+ }
1198
+ if (only !== undefined) {
1199
+ // Ambiguous: the name exists in several schemas.
1200
+ return undefined;
1201
+ }
1202
+ only = candidate;
1203
+ }
1204
+ return only;
1205
+ }
1206
+ else {
1207
+ const fallback = ctx.tableByKey.get(tableKey(undefined, name.name));
1208
+ if (fallback !== undefined &&
1209
+ !fallback.dropped &&
1210
+ DEFAULT_SCHEMAS.has(schema.toLowerCase())) {
1211
+ return fallback;
1212
+ }
1213
+ }
1214
+ return undefined;
1215
+ }
1216
+ function createType(ctx, file, cursor, line) {
1217
+ cursor.next();
1218
+ const name = parseQualifiedName(cursor);
1219
+ if (name === undefined || !cursor.acceptKeyword('AS')) {
1220
+ recordSkipped(ctx, file, 'CREATE TYPE', line);
1221
+ return;
1222
+ }
1223
+ if (!cursor.acceptKeyword('ENUM')) {
1224
+ recordSkipped(ctx, file, 'CREATE TYPE (not an enum)', line);
1225
+ return;
1226
+ }
1227
+ const group = cursor.readGroup() ?? [];
1228
+ const values = group
1229
+ .filter((token) => token.kind === 'string')
1230
+ .map((token) => token.value);
1231
+ const created = {
1232
+ ...(name.schema === undefined ? {} : { schema: name.schema }),
1233
+ name: name.name,
1234
+ values,
1235
+ seen: new Set(values),
1236
+ dropped: false,
1237
+ };
1238
+ ctx.enums.push(created);
1239
+ ctx.enumsByName.set(name.name.toLowerCase(), created);
1240
+ }
1241
+ function findEnum(ctx, name) {
1242
+ const found = ctx.enumsByName.get((typeof name === 'string' ? name : name.name).toLowerCase());
1243
+ return found === undefined || found.dropped ? undefined : found;
1244
+ }
1245
+ function handleAlter(ctx, file, cursor) {
1246
+ const line = cursor.peek().line;
1247
+ cursor.next();
1248
+ if (cursor.isKeyword('TYPE')) {
1249
+ cursor.next();
1250
+ const name = parseQualifiedName(cursor);
1251
+ const target = name === undefined ? undefined : findEnum(ctx, name);
1252
+ if (target !== undefined && cursor.acceptKeywords('ADD', 'VALUE')) {
1253
+ cursor.acceptKeywords('IF', 'NOT', 'EXISTS');
1254
+ const value = cursor.next();
1255
+ if (value.kind === 'string' && !target.seen.has(value.value)) {
1256
+ let position = target.values.length;
1257
+ if (cursor.isKeyword('BEFORE') || cursor.isKeyword('AFTER')) {
1258
+ const after = cursor.next().up === 'AFTER';
1259
+ const anchor = cursor.next();
1260
+ // Looking the anchor up is linear; a huge enum is appended to instead.
1261
+ const anchorAt = target.values.length > MAX_POSITIONED_ENUM_VALUES
1262
+ ? -1
1263
+ : target.values.indexOf(anchor.value);
1264
+ if (anchorAt !== -1) {
1265
+ position = after ? anchorAt + 1 : anchorAt;
1266
+ }
1267
+ }
1268
+ target.values.splice(position, 0, value.value);
1269
+ target.seen.add(value.value);
1270
+ }
1271
+ }
1272
+ return;
1273
+ }
1274
+ if (!cursor.acceptKeyword('TABLE')) {
1275
+ // ALTER SEQUENCE, ALTER VIEW, ALTER DATABASE and similar change nothing the model holds.
1276
+ return;
1277
+ }
1278
+ cursor.acceptKeywords('IF', 'EXISTS');
1279
+ cursor.acceptKeyword('ONLY');
1280
+ const name = parseQualifiedName(cursor);
1281
+ if (name === undefined) {
1282
+ return;
1283
+ }
1284
+ if (cursor.isKeyword('WITH') &&
1285
+ (cursor.isKeyword('CHECK', 1) || cursor.isKeyword('NOCHECK', 1))) {
1286
+ cursor.next();
1287
+ cursor.next();
1288
+ }
1289
+ const rest = [];
1290
+ while (!cursor.atEnd) {
1291
+ rest.push(cursor.next());
1292
+ }
1293
+ const table = lookupTable(ctx, name, file.dialect);
1294
+ if (table === undefined) {
1295
+ warn(ctx, `${file.name}:${line}: ALTER TABLE on the unknown table "${name.name}" was skipped.`);
1296
+ return;
1297
+ }
1298
+ for (const action of splitTopLevel(rest)) {
1299
+ handleAlterAction(ctx, file, table, new Cursor(action), line);
1300
+ }
1301
+ }
1302
+ const QUIET_ALTER_ACTIONS = new Set([
1303
+ 'OWNER',
1304
+ 'ENABLE',
1305
+ 'DISABLE',
1306
+ 'FORCE',
1307
+ 'NO',
1308
+ 'CLUSTER',
1309
+ 'SET',
1310
+ 'RESET',
1311
+ 'REPLICA',
1312
+ 'CHECK',
1313
+ 'NOCHECK',
1314
+ 'ATTACH',
1315
+ 'DETACH',
1316
+ 'AUTO_INCREMENT',
1317
+ 'ENGINE',
1318
+ 'ROW_FORMAT',
1319
+ 'COMMENT',
1320
+ 'CHARACTER',
1321
+ 'COLLATE',
1322
+ 'CONVERT',
1323
+ 'ALGORITHM',
1324
+ 'LOCK',
1325
+ 'TRIGGER',
1326
+ 'VALIDATE',
1327
+ 'INHERIT',
1328
+ 'OF',
1329
+ 'NOT',
1330
+ 'UNION',
1331
+ 'WITH',
1332
+ ]);
1333
+ function handleAlterAction(ctx, file, table, cursor, line) {
1334
+ const head = cursor.peek();
1335
+ if (head.kind !== 'word') {
1336
+ return;
1337
+ }
1338
+ if (head.up === 'ADD') {
1339
+ cursor.next();
1340
+ cursor.acceptKeyword('COLUMN');
1341
+ cursor.acceptKeywords('IF', 'NOT', 'EXISTS');
1342
+ if (isConstraintStart(cursor, file.dialect)) {
1343
+ applyConstraint(ctx, table, parseTableConstraint(ctx, file, cursor, table.name));
1344
+ return;
1345
+ }
1346
+ if (cursor.isKeyword('GENERATED') || cursor.isPunct('(') || cursor.atEnd) {
1347
+ return;
1348
+ }
1349
+ const column = parseColumn(ctx, file, cursor, table, table.name);
1350
+ if (column === undefined) {
1351
+ warn(ctx, `${file.name}:${line}: ALTER TABLE "${table.name}" ADD could not be read and was skipped.`);
1352
+ }
1353
+ return;
1354
+ }
1355
+ if (head.up === 'ALTER') {
1356
+ cursor.next();
1357
+ cursor.acceptKeyword('COLUMN');
1358
+ const columnToken = cursor.next();
1359
+ const column = findColumn(table, columnToken.value);
1360
+ if (column === undefined) {
1361
+ return;
1362
+ }
1363
+ if (cursor.acceptKeywords('SET', 'NOT', 'NULL')) {
1364
+ column.notNull = true;
1365
+ }
1366
+ else if (cursor.acceptKeywords('DROP', 'NOT', 'NULL')) {
1367
+ column.notNull = false;
1368
+ }
1369
+ else if (cursor.acceptKeywords('SET', 'DEFAULT')) {
1370
+ const tokens = readDefault(cursor);
1371
+ column.defaultTokens = tokens;
1372
+ column.defaultText = defaultExpression(tokens, file.text);
1373
+ }
1374
+ else if (cursor.acceptKeywords('DROP', 'DEFAULT')) {
1375
+ delete column.defaultTokens;
1376
+ delete column.defaultText;
1377
+ }
1378
+ else if (cursor.acceptKeyword('ADD') && cursor.isKeyword('GENERATED')) {
1379
+ column.autoIncrement = true;
1380
+ }
1381
+ else {
1382
+ recordSkipped(ctx, file, 'ALTER TABLE ... ALTER COLUMN (type or option change)', line);
1383
+ }
1384
+ return;
1385
+ }
1386
+ if (QUIET_ALTER_ACTIONS.has(head.up)) {
1387
+ return;
1388
+ }
1389
+ recordSkipped(ctx, file, `ALTER TABLE ... ${head.up}`, line);
1390
+ }
1391
+ function handleDrop(ctx, file, cursor) {
1392
+ cursor.next();
1393
+ const kind = cursor.next();
1394
+ if (kind.up === 'TABLE') {
1395
+ cursor.acceptKeywords('IF', 'EXISTS');
1396
+ for (;;) {
1397
+ const name = parseQualifiedName(cursor);
1398
+ if (name === undefined) {
1399
+ break;
1400
+ }
1401
+ const table = lookupTable(ctx, name, file.dialect);
1402
+ if (table !== undefined) {
1403
+ table.dropped = true;
1404
+ ctx.tableByKey.delete(tableKey(table.schema, table.name));
1405
+ }
1406
+ if (!cursor.acceptPunct(',')) {
1407
+ break;
1408
+ }
1409
+ }
1410
+ }
1411
+ else if (kind.up === 'TYPE') {
1412
+ cursor.acceptKeywords('IF', 'EXISTS');
1413
+ const name = parseQualifiedName(cursor);
1414
+ const target = name === undefined ? undefined : findEnum(ctx, name);
1415
+ if (target !== undefined) {
1416
+ target.dropped = true;
1417
+ }
1418
+ }
1419
+ }
1420
+ function sanitizeIdentifier(value) {
1421
+ let result = value.replace(/[^A-Za-z0-9_]+/g, '_');
1422
+ result = result.replace(/^_+(?=.)/, (match) => value.startsWith('_') ? match : '');
1423
+ if (result === '' || result === '_') {
1424
+ result = 'column';
1425
+ }
1426
+ if (/^\d/.test(result)) {
1427
+ result = `c_${result}`;
1428
+ }
1429
+ return result;
1430
+ }
1431
+ /** The next suffix to try per base name, per set of used names, so many clashes stay linear. */
1432
+ const NEXT_SUFFIX = new WeakMap();
1433
+ function nextFree(base, used, separator) {
1434
+ let counters = NEXT_SUFFIX.get(used);
1435
+ if (counters === undefined) {
1436
+ counters = new Map();
1437
+ NEXT_SUFFIX.set(used, counters);
1438
+ }
1439
+ let suffix = counters.get(base) ?? 2;
1440
+ for (;;) {
1441
+ const candidate = `${base}${separator}${suffix}`;
1442
+ suffix += 1;
1443
+ if (!used.has(candidate)) {
1444
+ counters.set(base, suffix);
1445
+ used.add(candidate);
1446
+ return candidate;
1447
+ }
1448
+ }
1449
+ }
1450
+ function uniqueName(base, used) {
1451
+ if (!used.has(base)) {
1452
+ used.add(base);
1453
+ return base;
1454
+ }
1455
+ return nextFree(base, used, '_');
1456
+ }
1457
+ function pluralize(snake) {
1458
+ const parts = snake.split('_');
1459
+ const word = parts.pop() ?? '';
1460
+ const lower = word.toLowerCase();
1461
+ let plural;
1462
+ if (['data', 'news', 'series', 'species', 'media', 'metadata'].includes(lower)) {
1463
+ plural = `${word}_set`;
1464
+ }
1465
+ else if (/[^aeiou]y$/.test(lower)) {
1466
+ plural = `${word.slice(0, -1)}ies`;
1467
+ }
1468
+ else if (/(s|x|z|ch|sh)$/.test(lower)) {
1469
+ plural = `${word}es`;
1470
+ }
1471
+ else {
1472
+ plural = `${word}s`;
1473
+ }
1474
+ parts.push(plural);
1475
+ return parts.join('_');
1476
+ }
1477
+ function modelNameFor(table, taken) {
1478
+ let base = toPascalCase(singularize(toSnakeCase(table.name)));
1479
+ if (base === '') {
1480
+ base = 'Model';
1481
+ }
1482
+ if (/^\d/.test(base)) {
1483
+ base = `T${base}`;
1484
+ }
1485
+ if (taken.has(base) && table.schema !== undefined) {
1486
+ base = `${toPascalCase(table.schema)}${base}`;
1487
+ }
1488
+ if (!taken.has(base)) {
1489
+ taken.add(base);
1490
+ return base;
1491
+ }
1492
+ return nextFree(base, taken, '');
1493
+ }
1494
+ function enumMembers(values) {
1495
+ const used = new Set();
1496
+ return values.map((value, position) => {
1497
+ let base = toSnakeCase(value).toUpperCase();
1498
+ if (base === '') {
1499
+ base = `VALUE_${position + 1}`;
1500
+ }
1501
+ if (/^\d/.test(base)) {
1502
+ base = `V_${base}`;
1503
+ }
1504
+ return { name: uniqueName(base, used), dbValue: value };
1505
+ });
1506
+ }
1507
+ function findColumn(table, name) {
1508
+ return table.columnIndex.get(name.toLowerCase());
1509
+ }
1510
+ /** Reads `col IN ('a', 'b')`, `col = 'a' OR col = 'b'` and PostgreSQL's `col = ANY (ARRAY[...])` checks. */
1511
+ function extractEnumCheck(check, table) {
1512
+ const kept = [];
1513
+ const tokens = check.tokens;
1514
+ for (let at = 0; at < tokens.length; at += 1) {
1515
+ const token = tokens[at];
1516
+ if (token === undefined) {
1517
+ continue;
1518
+ }
1519
+ if (token.kind === 'punct' && token.text === '::') {
1520
+ // Drop the cast: ::text, ::character varying(20), ::text[]
1521
+ at += 1;
1522
+ while (at < tokens.length) {
1523
+ const castPart = tokens[at];
1524
+ if (castPart?.kind === 'word' &&
1525
+ [
1526
+ 'TEXT',
1527
+ 'VARCHAR',
1528
+ 'CHARACTER',
1529
+ 'VARYING',
1530
+ 'CHAR',
1531
+ 'BPCHAR',
1532
+ 'NAME',
1533
+ ].includes(castPart.up)) {
1534
+ at += 1;
1535
+ }
1536
+ else if (castPart?.kind === 'punct' && castPart.text === '[') {
1537
+ at += 1;
1538
+ }
1539
+ else if (castPart?.kind === 'punct' && castPart.text === ']') {
1540
+ at += 1;
1541
+ }
1542
+ else {
1543
+ break;
1544
+ }
1545
+ }
1546
+ at -= 1;
1547
+ continue;
1548
+ }
1549
+ kept.push(token);
1550
+ }
1551
+ let column;
1552
+ const values = [];
1553
+ const seenValues = new Set();
1554
+ let sawMembership = false;
1555
+ for (const token of kept) {
1556
+ if (token.kind === 'string') {
1557
+ if (!seenValues.has(token.value)) {
1558
+ seenValues.add(token.value);
1559
+ values.push(token.value);
1560
+ }
1561
+ }
1562
+ else if (token.kind === 'word' || token.kind === 'ident') {
1563
+ const upper = token.value.toUpperCase();
1564
+ if (token.kind === 'word' && (upper === 'IN' || upper === 'ANY')) {
1565
+ sawMembership = true;
1566
+ }
1567
+ else if (token.kind === 'word' &&
1568
+ (upper === 'OR' || upper === 'ARRAY')) {
1569
+ continue;
1570
+ }
1571
+ else {
1572
+ const candidate = findColumn(table, token.value);
1573
+ if (candidate === undefined) {
1574
+ return undefined;
1575
+ }
1576
+ if (column !== undefined && column !== candidate) {
1577
+ return undefined;
1578
+ }
1579
+ column = candidate;
1580
+ }
1581
+ }
1582
+ else if (token.kind === 'number') {
1583
+ return undefined;
1584
+ }
1585
+ else if (token.kind === 'punct' &&
1586
+ !['(', ')', ',', '=', '[', ']'].includes(token.text)) {
1587
+ return undefined;
1588
+ }
1589
+ else if (token.kind === 'punct' && token.text === '=') {
1590
+ sawMembership = true;
1591
+ }
1592
+ }
1593
+ if (column === undefined || !sawMembership || values.length === 0) {
1594
+ return undefined;
1595
+ }
1596
+ return { column, values };
1597
+ }
1598
+ function referenceCoversExactly(columns, candidate) {
1599
+ if (columns.length !== candidate.length) {
1600
+ return false;
1601
+ }
1602
+ const wanted = new Set(candidate.map((name) => name.toLowerCase()));
1603
+ return columns.every((name) => wanted.has(name.toLowerCase()));
1604
+ }
1605
+ function targetKeyColumns(target) {
1606
+ const names = target.primaryKey?.columns ?? [];
1607
+ const found = [];
1608
+ for (const name of names) {
1609
+ const column = findColumn(target, name);
1610
+ if (column !== undefined) {
1611
+ found.push(column);
1612
+ }
1613
+ }
1614
+ return found;
1615
+ }
1616
+ function buildSchema(ctx, tables, options) {
1617
+ const warnings = {
1618
+ push: (message) => warn(ctx, message),
1619
+ };
1620
+ const modelNames = new Set();
1621
+ const builds = new Map();
1622
+ for (const table of tables) {
1623
+ builds.set(table, {
1624
+ table,
1625
+ name: modelNameFor(table, modelNames),
1626
+ used: new Set(),
1627
+ fieldNames: new Map(),
1628
+ consumed: new Set(),
1629
+ references: [],
1630
+ single: new Map(),
1631
+ composite: [],
1632
+ relationNames: new Map(),
1633
+ enumByColumn: new Map(),
1634
+ fields: [],
1635
+ relations: [],
1636
+ compositeForeignKeys: [],
1637
+ indexes: [],
1638
+ isCollapsed: false,
1639
+ });
1640
+ }
1641
+ // Enums: CREATE TYPE ... AS ENUM first, then inline and CHECK-derived ones.
1642
+ const enums = [];
1643
+ const enumByType = new Map();
1644
+ const enumNames = new Set(modelNames);
1645
+ for (const raw of ctx.enums) {
1646
+ if (raw.dropped) {
1647
+ continue;
1648
+ }
1649
+ const name = uniqueName(toPascalCase(raw.name) || 'Enum', enumNames);
1650
+ const created = {
1651
+ name,
1652
+ values: enumMembers(raw.values),
1653
+ ...(name === raw.name ? {} : { dbName: raw.name }),
1654
+ ...(raw.schema === undefined || DEFAULT_SCHEMAS.has(raw.schema)
1655
+ ? {}
1656
+ : { schema: raw.schema }),
1657
+ };
1658
+ enums.push(created);
1659
+ enumByType.set(raw.name.toLowerCase(), created);
1660
+ }
1661
+ for (const table of tables) {
1662
+ const build = builds.get(table);
1663
+ for (const column of table.columns) {
1664
+ if (column.type.enumValues !== undefined) {
1665
+ const created = {
1666
+ name: uniqueName(toPascalCase(`${singularize(toSnakeCase(table.name))}_${column.name}`) || 'Enum', enumNames),
1667
+ values: enumMembers(column.type.enumValues),
1668
+ };
1669
+ enums.push(created);
1670
+ build.enumByColumn.set(column.name.toLowerCase(), created);
1671
+ }
1672
+ }
1673
+ let droppedChecks = 0;
1674
+ for (const check of table.checks) {
1675
+ const found = extractEnumCheck(check, table);
1676
+ if (found !== undefined) {
1677
+ const mapped = mapType(found.column.type, table.dialect);
1678
+ const key = found.column.name.toLowerCase();
1679
+ if ((mapped.type === 'string' || mapped.type === 'text') &&
1680
+ !build.enumByColumn.has(key) &&
1681
+ found.column.type.enumValues === undefined &&
1682
+ found.column.type.arrayDepth === 0) {
1683
+ const created = {
1684
+ name: uniqueName(toPascalCase(`${singularize(toSnakeCase(table.name))}_${found.column.name}`) || 'Enum', enumNames),
1685
+ values: enumMembers(found.values),
1686
+ };
1687
+ enums.push(created);
1688
+ build.enumByColumn.set(key, created);
1689
+ continue;
1690
+ }
1691
+ }
1692
+ droppedChecks += 1;
1693
+ }
1694
+ if (droppedChecks > 0) {
1695
+ warnings.push(`table "${table.name}": ${droppedChecks} CHECK constraint${droppedChecks === 1 ? ' has' : 's have'} no equivalent in the shared model and ${droppedChecks === 1 ? 'was' : 'were'} dropped.`);
1696
+ }
1697
+ }
1698
+ // Foreign keys: which target does each one point at, and is it usable?
1699
+ const referencedTables = new Set();
1700
+ for (const table of tables) {
1701
+ const build = builds.get(table);
1702
+ for (const raw of table.references) {
1703
+ const target = lookupTable(ctx, {
1704
+ ...(raw.refSchema === undefined ? {} : { schema: raw.refSchema }),
1705
+ name: raw.refTable,
1706
+ }, table.dialect);
1707
+ if (target === undefined || builds.get(target) === undefined) {
1708
+ warnings.push(`table "${table.name}": the foreign key on (${raw.columns.join(', ')}) points at the unknown table "${raw.refSchema === undefined ? '' : `${raw.refSchema}.`}${raw.refTable}"; the column stays an ordinary field.`);
1709
+ continue;
1710
+ }
1711
+ const columns = [];
1712
+ for (const name of raw.columns) {
1713
+ const found = findColumn(table, name);
1714
+ if (found !== undefined) {
1715
+ columns.push(found);
1716
+ }
1717
+ }
1718
+ let targetColumns = [];
1719
+ if (raw.refColumns.length === 0) {
1720
+ targetColumns = targetKeyColumns(target);
1721
+ }
1722
+ else {
1723
+ for (const name of raw.refColumns) {
1724
+ const found = findColumn(target, name);
1725
+ if (found !== undefined) {
1726
+ targetColumns.push(found);
1727
+ }
1728
+ }
1729
+ }
1730
+ if (columns.length === 0 ||
1731
+ columns.length !== raw.columns.length ||
1732
+ targetColumns.length === 0 ||
1733
+ targetColumns.length !== columns.length ||
1734
+ (raw.refColumns.length > 0 &&
1735
+ raw.refColumns.length !== targetColumns.length)) {
1736
+ warnings.push(`table "${table.name}": the foreign key on (${raw.columns.join(', ')}) does not match the columns of "${target.name}"; it was skipped.`);
1737
+ continue;
1738
+ }
1739
+ build.references.push({ raw, target, columns, targetColumns });
1740
+ referencedTables.add(target);
1741
+ }
1742
+ }
1743
+ // Join tables: exactly two foreign-key columns that together are the primary key.
1744
+ const collapsed = new Map();
1745
+ for (const table of tables) {
1746
+ const build = builds.get(table);
1747
+ const key = table.primaryKey?.columns ?? [];
1748
+ if (table.columns.length !== 2 ||
1749
+ key.length !== 2 ||
1750
+ build.references.length !== 2 ||
1751
+ table.uniques.length > 0 ||
1752
+ table.indexes.length > 0 ||
1753
+ table.checks.length > 0 ||
1754
+ referencedTables.has(table)) {
1755
+ continue;
1756
+ }
1757
+ const [first, second] = build.references;
1758
+ if (first === undefined || second === undefined) {
1759
+ continue;
1760
+ }
1761
+ const columnNames = table.columns.map((column) => column.name);
1762
+ if (first.columns.length !== 1 ||
1763
+ second.columns.length !== 1 ||
1764
+ !referenceCoversExactly(key, columnNames) ||
1765
+ !referenceCoversExactly([first.columns[0]?.name ?? '', second.columns[0]?.name ?? ''], columnNames)) {
1766
+ continue;
1767
+ }
1768
+ const isKeyTarget = (reference) => {
1769
+ const expected = targetKeyColumns(reference.target);
1770
+ return (expected.length === 1 &&
1771
+ expected[0]?.name.toLowerCase() ===
1772
+ reference.targetColumns[0]?.name.toLowerCase());
1773
+ };
1774
+ if (!isKeyTarget(first) ||
1775
+ !isKeyTarget(second) ||
1776
+ first.target === table ||
1777
+ second.target === table) {
1778
+ continue;
1779
+ }
1780
+ // Keep the order of the primary key, so the first key column owns the relation by default.
1781
+ const ordered = [first, second].sort((left, right) => key.findIndex((name) => name.toLowerCase() === left.columns[0]?.name.toLowerCase()) -
1782
+ key.findIndex((name) => name.toLowerCase() === right.columns[0]?.name.toLowerCase()));
1783
+ const left = ordered[0];
1784
+ const right = ordered[1];
1785
+ if (left === undefined || right === undefined) {
1786
+ continue;
1787
+ }
1788
+ collapsed.set(table, { first: left, second: right });
1789
+ build.isCollapsed = true;
1790
+ }
1791
+ // A join table whose target is itself a join table stays a model.
1792
+ for (const [table, pair] of [...collapsed]) {
1793
+ if (collapsed.has(pair.first.target) || collapsed.has(pair.second.target)) {
1794
+ collapsed.delete(table);
1795
+ builds.get(table).isCollapsed = false;
1796
+ }
1797
+ }
1798
+ const keptTables = tables.filter((table) => !collapsed.has(table));
1799
+ // Drop references to collapsed tables (cannot happen: referenced tables are never collapsed).
1800
+ // Column names, then relation names.
1801
+ for (const table of keptTables) {
1802
+ const build = builds.get(table);
1803
+ const usedColumns = new Set();
1804
+ // Single-column foreign keys replace their column; composite ones leave the columns as fields.
1805
+ const consumedBy = new Map();
1806
+ for (const reference of build.references) {
1807
+ if (reference.columns.length !== 1) {
1808
+ continue;
1809
+ }
1810
+ const column = reference.columns[0];
1811
+ const key = column.name.toLowerCase();
1812
+ if (consumedBy.has(key)) {
1813
+ warnings.push(`table "${table.name}": the column "${column.name}" has more than one foreign key; only the first is read as a relation.`);
1814
+ continue;
1815
+ }
1816
+ consumedBy.set(key, reference);
1817
+ build.single.set(key, reference);
1818
+ build.consumed.add(key);
1819
+ }
1820
+ for (const column of table.columns) {
1821
+ const key = column.name.toLowerCase();
1822
+ if (build.consumed.has(key)) {
1823
+ continue;
1824
+ }
1825
+ const fieldName = uniqueName(sanitizeIdentifier(column.name), usedColumns);
1826
+ build.fieldNames.set(key, fieldName);
1827
+ build.used.add(fieldName);
1828
+ }
1829
+ for (const reference of build.references) {
1830
+ if (reference.columns.length === 1) {
1831
+ if (build.single.get(reference.columns[0]?.name.toLowerCase() ?? '') !==
1832
+ reference) {
1833
+ continue;
1834
+ }
1835
+ const column = reference.columns[0];
1836
+ let stripped = stripKeySuffix(column.name);
1837
+ if (stripped === column.name) {
1838
+ // order_no -> orders: no key suffix, but the column starts with the name of its target.
1839
+ const targetName = toSnakeCase(builds.get(reference.target).name);
1840
+ if (column.name.toLowerCase().startsWith(`${targetName}_`)) {
1841
+ stripped = targetName;
1842
+ }
1843
+ }
1844
+ const base = sanitizeIdentifier(stripped === ''
1845
+ ? toSnakeCase(builds.get(reference.target).name)
1846
+ : stripped);
1847
+ const name = uniqueName(base, build.used);
1848
+ build.relationNames.set(reference, name);
1849
+ build.fieldNames.set(column.name.toLowerCase(), name);
1850
+ }
1851
+ else {
1852
+ const unconsumed = reference.columns.every((column) => !build.consumed.has(column.name.toLowerCase()));
1853
+ if (!unconsumed) {
1854
+ warnings.push(`table "${table.name}": the composite foreign key on (${reference.columns.map((column) => column.name).join(', ')}) shares a column with a single-column foreign key and was skipped.`);
1855
+ continue;
1856
+ }
1857
+ const name = uniqueName(sanitizeIdentifier(toSnakeCase(builds.get(reference.target).name)), build.used);
1858
+ build.composite.push(reference);
1859
+ build.relationNames.set(reference, name);
1860
+ }
1861
+ }
1862
+ }
1863
+ const manyToMany = [];
1864
+ for (const [table, pair] of collapsed) {
1865
+ const first = builds.get(pair.first.target);
1866
+ const second = builds.get(pair.second.target);
1867
+ const joinName = table.name.toLowerCase();
1868
+ const prefixOf = (build) => joinName.startsWith(`${build.table.name.toLowerCase()}_`) &&
1869
+ joinName.length > build.table.name.length + 1;
1870
+ let owner = first;
1871
+ let other = second;
1872
+ if (!prefixOf(first) && prefixOf(second)) {
1873
+ owner = second;
1874
+ other = first;
1875
+ }
1876
+ let relationName;
1877
+ let relatedBase;
1878
+ if (first === second) {
1879
+ // A self-referencing join table (follower_id, followee_id): the columns name both directions.
1880
+ const firstColumn = stripKeySuffix(pair.first.columns[0]?.name ?? '');
1881
+ const secondColumn = stripKeySuffix(pair.second.columns[0]?.name ?? '');
1882
+ relationName = pluralize(sanitizeIdentifier(secondColumn === '' ? toSnakeCase(other.name) : secondColumn));
1883
+ relatedBase =
1884
+ firstColumn === ''
1885
+ ? undefined
1886
+ : pluralize(sanitizeIdentifier(firstColumn));
1887
+ }
1888
+ else if (prefixOf(owner)) {
1889
+ relationName = sanitizeIdentifier(table.name.slice(owner.table.name.length + 1));
1890
+ }
1891
+ else {
1892
+ relationName = pluralize(toSnakeCase(other.name));
1893
+ warnings.push(`table "${table.name}": read as the many-to-many relation "${owner.name}.${relationName}"; writers name the join table "${owner.table.name}_${relationName}".`);
1894
+ }
1895
+ manyToMany.push({
1896
+ owner,
1897
+ other,
1898
+ name: uniqueName(relationName, owner.used),
1899
+ relatedBase,
1900
+ });
1901
+ }
1902
+ for (const entry of manyToMany) {
1903
+ entry.relatedName = uniqueName(entry.relatedBase ?? pluralize(toSnakeCase(entry.owner.name)), entry.other.used);
1904
+ }
1905
+ // Reverse accessor names of foreign keys.
1906
+ const relatedNames = new Map();
1907
+ for (const table of keptTables) {
1908
+ const build = builds.get(table);
1909
+ for (const reference of build.references) {
1910
+ const name = build.relationNames.get(reference);
1911
+ if (name === undefined) {
1912
+ continue;
1913
+ }
1914
+ const targetBuild = builds.get(reference.target);
1915
+ const sources = namedPerTarget(build).get(reference.target) ?? 0;
1916
+ const isUnique = isOneToOne(table, reference);
1917
+ const source = toSnakeCase(build.name);
1918
+ const base = isUnique ? source : pluralize(source);
1919
+ const preferred = sources > 1 ? `${name}_${base}` : base;
1920
+ relatedNames.set(reference, uniqueName(preferred, targetBuild.used));
1921
+ }
1922
+ }
1923
+ // Models.
1924
+ const models = [];
1925
+ const modelSchemaOf = (table) => table.schema !== undefined &&
1926
+ !DEFAULT_SCHEMAS.has(table.schema.toLowerCase())
1927
+ ? table.schema
1928
+ : undefined;
1929
+ for (const table of keptTables) {
1930
+ const build = builds.get(table);
1931
+ buildFields(build, enumByType, warnings);
1932
+ const primaryKey = table.primaryKey?.columns ?? [];
1933
+ const primaryKeyNames = new Set(primaryKey.map((name) => name.toLowerCase()));
1934
+ for (const reference of build.references) {
1935
+ const name = build.relationNames.get(reference);
1936
+ if (name === undefined) {
1937
+ continue;
1938
+ }
1939
+ const targetBuild = builds.get(reference.target);
1940
+ const targetKey = targetKeyColumns(reference.target);
1941
+ const oneToOne = isOneToOne(table, reference);
1942
+ const nullable = reference.columns.some((column) => column.notNull !== true &&
1943
+ !primaryKeyNames.has(column.name.toLowerCase()));
1944
+ const onDelete = reference.raw.onDelete ?? 'noAction';
1945
+ const onUpdate = reference.raw.onUpdate === undefined ||
1946
+ reference.raw.onUpdate === 'noAction'
1947
+ ? undefined
1948
+ : reference.raw.onUpdate;
1949
+ const relatedName = relatedNames.get(reference);
1950
+ if (reference.columns.length === 1) {
1951
+ const column = reference.columns[0];
1952
+ const referenced = reference.targetColumns[0];
1953
+ const pointsAtKey = targetKey.length === 1 &&
1954
+ targetKey[0]?.name.toLowerCase() === referenced.name.toLowerCase();
1955
+ const isKey = primaryKey.length === 1 &&
1956
+ primaryKey[0]?.toLowerCase() === column.name.toLowerCase();
1957
+ build.relations.push({
1958
+ name,
1959
+ kind: oneToOne ? 'oneToOne' : 'foreignKey',
1960
+ targetModel: targetBuild.name,
1961
+ columnName: column.name,
1962
+ isNullable: nullable && !isKey,
1963
+ onDelete,
1964
+ ...(relatedName === undefined ? {} : { relatedName }),
1965
+ ...(pointsAtKey
1966
+ ? {}
1967
+ : {
1968
+ toField: targetBuild.fieldNames.get(referenced.name.toLowerCase()) ??
1969
+ sanitizeIdentifier(referenced.name),
1970
+ }),
1971
+ ...(isKey ? { isPrimaryKey: true } : {}),
1972
+ ...(onUpdate === undefined ? {} : { onUpdate }),
1973
+ ...(reference.raw.name === undefined
1974
+ ? {}
1975
+ : { constraintName: reference.raw.name }),
1976
+ });
1977
+ }
1978
+ else {
1979
+ build.compositeForeignKeys.push({
1980
+ name,
1981
+ targetModel: targetBuild.name,
1982
+ fields: reference.columns.map((column) => build.fieldNames.get(column.name.toLowerCase()) ??
1983
+ sanitizeIdentifier(column.name)),
1984
+ references: reference.targetColumns.map((column) => targetBuild.fieldNames.get(column.name.toLowerCase()) ??
1985
+ sanitizeIdentifier(column.name)),
1986
+ kind: oneToOne ? 'oneToOne' : 'foreignKey',
1987
+ isNullable: nullable,
1988
+ onDelete,
1989
+ ...(onUpdate === undefined ? {} : { onUpdate }),
1990
+ ...(relatedName === undefined ? {} : { relatedName }),
1991
+ ...(reference.raw.name === undefined
1992
+ ? {}
1993
+ : { constraintName: reference.raw.name }),
1994
+ });
1995
+ }
1996
+ }
1997
+ for (const entry of manyToMany) {
1998
+ if (entry.owner === build) {
1999
+ build.relations.push({
2000
+ name: entry.name,
2001
+ kind: 'manyToMany',
2002
+ targetModel: entry.other.name,
2003
+ columnName: '',
2004
+ isNullable: false,
2005
+ onDelete: 'cascade',
2006
+ ...(entry.relatedName === undefined
2007
+ ? {}
2008
+ : { relatedName: entry.relatedName }),
2009
+ });
2010
+ }
2011
+ }
2012
+ buildIndexes(build, warnings);
2013
+ const keyNames = primaryKey.map((name) => build.fieldNames.get(name.toLowerCase()) ?? sanitizeIdentifier(name));
2014
+ const model = {
2015
+ name: build.name,
2016
+ tableName: table.name,
2017
+ appLabel: options.appLabel,
2018
+ fields: build.fields,
2019
+ relations: build.relations,
2020
+ indexes: build.indexes,
2021
+ ...(keyNames.length > 1 ? { compositePrimaryKey: keyNames } : {}),
2022
+ ...(modelSchemaOf(table) === undefined
2023
+ ? {}
2024
+ : { schema: modelSchemaOf(table) }),
2025
+ ...(table.primaryKey?.name === undefined
2026
+ ? {}
2027
+ : { primaryKeyName: table.primaryKey.name }),
2028
+ ...(build.compositeForeignKeys.length === 0
2029
+ ? {}
2030
+ : { compositeForeignKeys: build.compositeForeignKeys }),
2031
+ };
2032
+ if (primaryKey.length === 0) {
2033
+ warnings.push(`table "${table.name}": the table has no primary key; most ORMs need one.`);
2034
+ }
2035
+ models.push(model);
2036
+ }
2037
+ return { models, enums, warnings: ctx.warnings };
2038
+ }
2039
+ /** A set of column names as a comparable key. */
2040
+ function columnSetKey(names) {
2041
+ return names
2042
+ .map((name) => name.toLowerCase())
2043
+ .sort()
2044
+ .join('\u0000');
2045
+ }
2046
+ const UNIQUE_COLUMN_SETS = new WeakMap();
2047
+ /** The column sets of a table that hold unique values: the primary key, unique constraints and unique indexes. */
2048
+ function uniqueColumnSets(table) {
2049
+ const cached = UNIQUE_COLUMN_SETS.get(table);
2050
+ if (cached !== undefined) {
2051
+ return cached;
2052
+ }
2053
+ const sets = new Set();
2054
+ if (table.primaryKey !== undefined) {
2055
+ sets.add(columnSetKey(table.primaryKey.columns));
2056
+ }
2057
+ for (const unique of table.uniques) {
2058
+ sets.add(columnSetKey(unique.columns));
2059
+ }
2060
+ for (const index of table.indexes) {
2061
+ if (index.isUnique &&
2062
+ !index.isPartial &&
2063
+ index.kind === undefined &&
2064
+ index.elements.every((element) => element.column !== undefined)) {
2065
+ sets.add(columnSetKey(index.elements.map((element) => element.column)));
2066
+ }
2067
+ }
2068
+ UNIQUE_COLUMN_SETS.set(table, sets);
2069
+ return sets;
2070
+ }
2071
+ /** A foreign key is one-to-one when its columns are the primary key or carry a unique constraint or index. */
2072
+ const NAMED_PER_TARGET = new WeakMap();
2073
+ /** How many named relations of a model point at each target table. */
2074
+ function namedPerTarget(build) {
2075
+ const cached = NAMED_PER_TARGET.get(build);
2076
+ if (cached !== undefined) {
2077
+ return cached;
2078
+ }
2079
+ const counts = new Map();
2080
+ for (const reference of build.references) {
2081
+ if (build.relationNames.has(reference)) {
2082
+ counts.set(reference.target, (counts.get(reference.target) ?? 0) + 1);
2083
+ }
2084
+ }
2085
+ NAMED_PER_TARGET.set(build, counts);
2086
+ return counts;
2087
+ }
2088
+ function isOneToOne(table, reference) {
2089
+ return uniqueColumnSets(table).has(columnSetKey(reference.columns.map((column) => column.name)));
2090
+ }
2091
+ function stripKeySuffix(column) {
2092
+ const match = /^(.*?)(?:_(?:id|uuid|fk|pk|ref|key)|(?<=[a-z0-9])(?:Id|ID|Uuid))$/.exec(column);
2093
+ if (match === null) {
2094
+ return /^(?:id|uuid)$/i.test(column) ? '' : column;
2095
+ }
2096
+ return match[1] ?? '';
2097
+ }
2098
+ function buildFields(build, enumByType, warnings) {
2099
+ const table = build.table;
2100
+ const keyColumns = new Set((table.primaryKey?.columns ?? []).map((name) => name.toLowerCase()));
2101
+ const singleKey = keyColumns.size === 1;
2102
+ const uniqueNames = new Map();
2103
+ for (const unique of table.uniques) {
2104
+ if (unique.columns.length === 1) {
2105
+ uniqueNames.set(unique.columns[0]?.toLowerCase() ?? '', unique.name);
2106
+ }
2107
+ }
2108
+ for (const column of table.columns) {
2109
+ const key = column.name.toLowerCase();
2110
+ if (build.consumed.has(key)) {
2111
+ continue;
2112
+ }
2113
+ const fieldName = build.fieldNames.get(key) ?? sanitizeIdentifier(column.name);
2114
+ if (column.type.name === '' && table.dialect !== 'sqlite') {
2115
+ warnings.push(`table "${table.name}", column "${column.name}": the column has no declared type (a computed column?) and was skipped.`);
2116
+ build.fieldNames.delete(key);
2117
+ continue;
2118
+ }
2119
+ const mapped = mapType(column.type, table.dialect);
2120
+ let type = mapped.type;
2121
+ let enumType = build.enumByColumn.get(key);
2122
+ let unsupportedType;
2123
+ if (enumType === undefined && mapped.isUserType === true) {
2124
+ enumType = enumByType.get(column.type.userName.toLowerCase());
2125
+ }
2126
+ if (enumType !== undefined) {
2127
+ type = 'string';
2128
+ }
2129
+ else if (mapped.unsupported === true) {
2130
+ unsupportedType =
2131
+ table.dialect === 'sqlserver' && column.type.name === 'timestamp'
2132
+ ? 'rowversion'
2133
+ : column.type.text;
2134
+ if (mapped.isUserType === true) {
2135
+ warnings.push(`table "${table.name}", column "${column.name}": the type "${column.type.text}" is unknown; it is kept as an unsupported column.`);
2136
+ }
2137
+ }
2138
+ if (column.type.setValues !== undefined) {
2139
+ warnings.push(`table "${table.name}", column "${column.name}": a SET column is read as text; the member list is dropped.`);
2140
+ }
2141
+ const inKey = keyColumns.has(key);
2142
+ const isPrimaryKey = singleKey && inKey;
2143
+ const isInteger = type === 'int' || type === 'bigInt';
2144
+ const implicitRowId = table.dialect === 'sqlite' &&
2145
+ !table.withoutRowid &&
2146
+ isPrimaryKey &&
2147
+ column.type.name === 'integer' &&
2148
+ column.type.arrayDepth === 0;
2149
+ const autoIncrement = isInteger &&
2150
+ column.type.arrayDepth === 0 &&
2151
+ (column.autoIncrement || mapped.autoIncrement === true || implicitRowId);
2152
+ const nullable = column.notNull !== true && !inKey && !autoIncrement;
2153
+ let columnDefault;
2154
+ if (column.generated === undefined) {
2155
+ if (column.defaultTokens !== undefined) {
2156
+ const interpreted = interpretDefault(column.defaultTokens, {
2157
+ type,
2158
+ ...(enumType === undefined ? {} : { enumType }),
2159
+ isArray: column.type.arrayDepth > 0,
2160
+ }, table.dialect);
2161
+ if (interpreted === undefined) {
2162
+ const expression = column.defaultText ?? '';
2163
+ if (expression !== '') {
2164
+ columnDefault = { kind: 'dbExpression', expression };
2165
+ }
2166
+ }
2167
+ else if (interpreted !== null) {
2168
+ columnDefault = interpreted;
2169
+ }
2170
+ }
2171
+ if (autoIncrement && columnDefault === undefined) {
2172
+ columnDefault = { kind: 'autoIncrement' };
2173
+ }
2174
+ }
2175
+ const field = {
2176
+ name: fieldName,
2177
+ columnName: column.name,
2178
+ type,
2179
+ isPrimaryKey,
2180
+ isUnique: !isPrimaryKey && uniqueNames.has(key),
2181
+ isNullable: nullable,
2182
+ isAutoUpdated: column.onUpdateNow,
2183
+ ...(columnDefault === undefined ? {} : { default: columnDefault }),
2184
+ ...(mapped.maxLength === undefined
2185
+ ? {}
2186
+ : { maxLength: mapped.maxLength }),
2187
+ ...(mapped.maxDigits === undefined
2188
+ ? {}
2189
+ : { maxDigits: mapped.maxDigits }),
2190
+ ...(mapped.decimalPlaces === undefined
2191
+ ? {}
2192
+ : { decimalPlaces: mapped.decimalPlaces }),
2193
+ ...(enumType === undefined ? {} : { enumName: enumType.name }),
2194
+ ...(column.type.arrayDepth > 0
2195
+ ? { arrayDepth: column.type.arrayDepth }
2196
+ : {}),
2197
+ ...(mapped.rangeOf === undefined ? {} : { rangeOf: mapped.rangeOf }),
2198
+ ...(column.generated === undefined
2199
+ ? {}
2200
+ : { generated: column.generated }),
2201
+ ...(unsupportedType === undefined ? {} : { unsupportedType }),
2202
+ ...(!isPrimaryKey && uniqueNames.get(key) !== undefined
2203
+ ? { uniqueName: uniqueNames.get(key) }
2204
+ : {}),
2205
+ };
2206
+ build.fields.push(field);
2207
+ }
2208
+ }
2209
+ function buildIndexes(build, warnings) {
2210
+ const table = build.table;
2211
+ const nameOf = (column) => build.fieldNames.get(column.toLowerCase());
2212
+ for (const unique of table.uniques) {
2213
+ if (unique.columns.length === 1) {
2214
+ continue;
2215
+ }
2216
+ const names = unique.columns.map((column) => nameOf(column) ?? '');
2217
+ if (names.some((name) => name === '')) {
2218
+ warnings.push(`table "${table.name}": a unique constraint refers to an unknown column and was skipped.`);
2219
+ continue;
2220
+ }
2221
+ build.indexes.push({
2222
+ fields: names,
2223
+ isUnique: true,
2224
+ ...(unique.name === undefined ? {} : { name: unique.name }),
2225
+ });
2226
+ }
2227
+ // The column lists (in order) of the foreign keys, to recognise the indexes MySQL adds for them.
2228
+ const foreignKeyColumnLists = new Set(table.dialect === 'mysql'
2229
+ ? build.references.map((reference) => reference.columns
2230
+ .map((column) => nameOf(column.name) ?? '')
2231
+ .join('\u0000'))
2232
+ : []);
2233
+ for (const index of table.indexes) {
2234
+ const label = index.name === undefined ? '' : ` "${index.name}"`;
2235
+ if (index.elements.some((element) => element.isExpression)) {
2236
+ warnings.push(`table "${table.name}": the index${label} is on an expression, which the shared model cannot hold, and was skipped.`);
2237
+ continue;
2238
+ }
2239
+ const names = index.elements.map((element) => nameOf(element.column ?? '') ?? '');
2240
+ if (names.length === 0 || names.some((name) => name === '')) {
2241
+ warnings.push(`table "${table.name}": the index${label} refers to an unknown column and was skipped.`);
2242
+ continue;
2243
+ }
2244
+ // MySQL builds an index for every foreign key, so a plain index over exactly the key columns
2245
+ // is the one the database added (or one that duplicates it), not a separate declaration.
2246
+ if (table.dialect === 'mysql' &&
2247
+ !index.isUnique &&
2248
+ index.kind === undefined &&
2249
+ !index.isPartial &&
2250
+ index.method === undefined &&
2251
+ foreignKeyColumnLists.has(names.join('\u0000'))) {
2252
+ continue;
2253
+ }
2254
+ if (index.isPartial) {
2255
+ if (index.isUnique) {
2256
+ warnings.push(`table "${table.name}": the partial unique index${label} would become unconditionally unique and was skipped.`);
2257
+ continue;
2258
+ }
2259
+ warnings.push(`table "${table.name}": the WHERE condition of the index${label} was dropped.`);
2260
+ }
2261
+ const fieldOptions = {};
2262
+ index.elements.forEach((element, position) => {
2263
+ const name = names[position];
2264
+ const options = {
2265
+ ...(element.descending ? { sort: 'desc' } : {}),
2266
+ ...(element.length === undefined ? {} : { length: element.length }),
2267
+ ...(element.ops === undefined
2268
+ ? {}
2269
+ : { ops: `raw("${element.ops}")` }),
2270
+ };
2271
+ if (Object.keys(options).length > 0) {
2272
+ fieldOptions[name] = options;
2273
+ }
2274
+ });
2275
+ build.indexes.push({
2276
+ fields: names,
2277
+ isUnique: index.isUnique,
2278
+ ...(index.name === undefined ? {} : { name: index.name }),
2279
+ ...(index.kind === 'fulltext' ? { kind: 'fulltext' } : {}),
2280
+ ...(index.method === undefined
2281
+ ? {}
2282
+ : { method: indexMethod(index.method) }),
2283
+ ...(index.clustered === undefined ? {} : { clustered: index.clustered }),
2284
+ ...(Object.keys(fieldOptions).length === 0 ? {} : { fieldOptions }),
2285
+ });
2286
+ if (index.kind === 'spatial') {
2287
+ warnings.push(`table "${table.name}": the spatial index${label} is read as an ordinary index.`);
2288
+ }
2289
+ }
2290
+ }
2291
+ const INDEX_METHODS = {
2292
+ hash: 'Hash',
2293
+ gin: 'Gin',
2294
+ gist: 'Gist',
2295
+ spgist: 'SpGist',
2296
+ brin: 'Brin',
2297
+ btree: 'BTree',
2298
+ };
2299
+ function indexMethod(method) {
2300
+ return INDEX_METHODS[method.toLowerCase()] ?? toPascalCase(method);
2301
+ }
2302
+ //# sourceMappingURL=sqlDdl.js.map