@adminiumjs/schema-import 0.1.0-rc.1

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 (78) hide show
  1. package/LICENSE +661 -0
  2. package/dist/builder.d.ts +112 -0
  3. package/dist/builder.d.ts.map +1 -0
  4. package/dist/builder.js +291 -0
  5. package/dist/builder.js.map +1 -0
  6. package/dist/check-enum.d.ts +5 -0
  7. package/dist/check-enum.d.ts.map +1 -0
  8. package/dist/check-enum.js +40 -0
  9. package/dist/check-enum.js.map +1 -0
  10. package/dist/detect.d.ts +13 -0
  11. package/dist/detect.d.ts.map +1 -0
  12. package/dist/detect.js +47 -0
  13. package/dist/detect.js.map +1 -0
  14. package/dist/errors.d.ts +10 -0
  15. package/dist/errors.d.ts.map +1 -0
  16. package/dist/errors.js +12 -0
  17. package/dist/errors.js.map +1 -0
  18. package/dist/index.d.ts +14 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +14 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/parse.d.ts +3 -0
  23. package/dist/parse.d.ts.map +1 -0
  24. package/dist/parse.js +59 -0
  25. package/dist/parse.js.map +1 -0
  26. package/dist/parsers/django.d.ts +13 -0
  27. package/dist/parsers/django.d.ts.map +1 -0
  28. package/dist/parsers/django.js +489 -0
  29. package/dist/parsers/django.js.map +1 -0
  30. package/dist/parsers/drizzle.d.ts +13 -0
  31. package/dist/parsers/drizzle.d.ts.map +1 -0
  32. package/dist/parsers/drizzle.js +395 -0
  33. package/dist/parsers/drizzle.js.map +1 -0
  34. package/dist/parsers/json.d.ts +8 -0
  35. package/dist/parsers/json.d.ts.map +1 -0
  36. package/dist/parsers/json.js +31 -0
  37. package/dist/parsers/json.js.map +1 -0
  38. package/dist/parsers/prisma.d.ts +11 -0
  39. package/dist/parsers/prisma.d.ts.map +1 -0
  40. package/dist/parsers/prisma.js +402 -0
  41. package/dist/parsers/prisma.js.map +1 -0
  42. package/dist/parsers/rails.d.ts +13 -0
  43. package/dist/parsers/rails.d.ts.map +1 -0
  44. package/dist/parsers/rails.js +387 -0
  45. package/dist/parsers/rails.js.map +1 -0
  46. package/dist/parsers/sequelize.d.ts +12 -0
  47. package/dist/parsers/sequelize.d.ts.map +1 -0
  48. package/dist/parsers/sequelize.js +277 -0
  49. package/dist/parsers/sequelize.js.map +1 -0
  50. package/dist/parsers/sql-cursor.d.ts +40 -0
  51. package/dist/parsers/sql-cursor.d.ts.map +1 -0
  52. package/dist/parsers/sql-cursor.js +203 -0
  53. package/dist/parsers/sql-cursor.js.map +1 -0
  54. package/dist/parsers/sql.d.ts +17 -0
  55. package/dist/parsers/sql.d.ts.map +1 -0
  56. package/dist/parsers/sql.js +794 -0
  57. package/dist/parsers/sql.js.map +1 -0
  58. package/dist/parsers/typeorm.d.ts +13 -0
  59. package/dist/parsers/typeorm.d.ts.map +1 -0
  60. package/dist/parsers/typeorm.js +511 -0
  61. package/dist/parsers/typeorm.js.map +1 -0
  62. package/dist/text.d.ts +54 -0
  63. package/dist/text.d.ts.map +1 -0
  64. package/dist/text.js +254 -0
  65. package/dist/text.js.map +1 -0
  66. package/dist/type-map.d.ts +28 -0
  67. package/dist/type-map.d.ts.map +1 -0
  68. package/dist/type-map.js +171 -0
  69. package/dist/type-map.js.map +1 -0
  70. package/dist/types.d.ts +32 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +22 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/warnings.d.ts +17 -0
  75. package/dist/warnings.d.ts.map +1 -0
  76. package/dist/warnings.js +34 -0
  77. package/dist/warnings.js.map +1 -0
  78. package/package.json +29 -0
@@ -0,0 +1,794 @@
1
+ import { ModelBuilder } from '../builder.js';
2
+ import { extractCheckEnum } from '../check-enum.js';
3
+ import { SchemaImportError } from '../errors.js';
4
+ import { collectStrings, splitTopLevel, stripComments } from '../text.js';
5
+ import { classifySqlDefault, isSerialType, mapSqlType } from '../type-map.js';
6
+ import { SqlCursor } from './sql-cursor.js';
7
+ const SQL_SCAN = { dollarQuotes: true, lineComments: ['--'], blockComments: true };
8
+ function splitStatements(sql) {
9
+ const statements = [];
10
+ let start = 0;
11
+ let line = 1;
12
+ let startLine = 1;
13
+ let i = 0;
14
+ const bump = (from, to) => {
15
+ for (let j = from; j < to; j += 1)
16
+ if (sql[j] === '\n')
17
+ line += 1;
18
+ };
19
+ while (i < sql.length) {
20
+ const ch = sql[i];
21
+ if (ch === "'" || ch === '"' || ch === '`' || ch === '$') {
22
+ const next = ch === '$'
23
+ ? skipDollar(sql, i)
24
+ : skipQuoted(sql, i);
25
+ if (next !== i) {
26
+ bump(i, next);
27
+ i = next;
28
+ continue;
29
+ }
30
+ }
31
+ if (ch === ';') {
32
+ const text = sql.slice(start, i).trim();
33
+ if (text.length > 0)
34
+ statements.push({ text, line: startLine });
35
+ start = i + 1;
36
+ i += 1;
37
+ startLine = line;
38
+ continue;
39
+ }
40
+ if (ch === '\n')
41
+ line += 1;
42
+ if (sql.slice(start, i).trim().length === 0)
43
+ startLine = line;
44
+ i += 1;
45
+ }
46
+ const tail = sql.slice(start).trim();
47
+ if (tail.length > 0)
48
+ statements.push({ text: tail, line: startLine });
49
+ return statements;
50
+ }
51
+ function skipQuoted(sql, i) {
52
+ const quote = sql[i];
53
+ let j = i + 1;
54
+ while (j < sql.length) {
55
+ if (sql[j] === quote) {
56
+ if (sql[j + 1] === quote) {
57
+ j += 2;
58
+ continue;
59
+ }
60
+ return j + 1;
61
+ }
62
+ j += 1;
63
+ }
64
+ return sql.length;
65
+ }
66
+ function skipDollar(sql, i) {
67
+ const m = /^\$[A-Za-z_][A-Za-z0-9_]*\$|^\$\$/.exec(sql.slice(i));
68
+ if (!m)
69
+ return i;
70
+ const tag = m[0];
71
+ const end = sql.indexOf(tag, i + tag.length);
72
+ return end === -1 ? sql.length : end + tag.length;
73
+ }
74
+ function sniffDialect(content) {
75
+ if (/`/.test(content) || /ENGINE\s*=/i.test(content) || /AUTO_INCREMENT/i.test(content)) {
76
+ return 'mysql';
77
+ }
78
+ if (/\bAUTOINCREMENT\b/i.test(content) || /WITHOUT\s+ROWID/i.test(content))
79
+ return 'sqlite';
80
+ return 'postgres';
81
+ }
82
+ const FK_ACTION_MAP = {
83
+ CASCADE: 'cascade',
84
+ RESTRICT: 'restrict',
85
+ 'SET NULL': 'set-null',
86
+ 'SET DEFAULT': 'set-default',
87
+ 'NO ACTION': 'no-action',
88
+ };
89
+ export function parseSql(content, name, warnings) {
90
+ const dialect = sniffDialect(content);
91
+ const builder = new ModelBuilder(warnings);
92
+ const state = { builder, warnings, enumTypes: new Map(), defaultSchema: 'public' };
93
+ // pg_dump COPY payloads are newline-delimited rows terminated by `\.` — not
94
+ // `;`-terminated SQL. Drop the row data before tokenizing so it cannot
95
+ // swallow the following statement (setup never reads rows anyway).
96
+ const withoutCopyData = content.replace(/(\bFROM\s+stdin;)[\s\S]*?(?:\n\\\.|$)/gi, '$1');
97
+ const cleaned = stripComments(withoutCopyData, SQL_SCAN);
98
+ const statements = splitStatements(cleaned);
99
+ if (statements.length === 0) {
100
+ throw new SchemaImportError('no SQL statements found in input');
101
+ }
102
+ for (const stmt of statements) {
103
+ try {
104
+ parseStatement(stmt, state);
105
+ }
106
+ catch {
107
+ warnings.add('unparseable-statement', `could not parse statement at line ${stmt.line}: ${stmt.text.slice(0, 60)}…`);
108
+ }
109
+ }
110
+ resolveEnumColumns(state);
111
+ return builder.finalize({
112
+ format: 'sql',
113
+ dialect,
114
+ name,
115
+ capabilities: {
116
+ hasEnums: dialect !== 'sqlite',
117
+ hasFKs: true,
118
+ hasSchemas: dialect === 'postgres',
119
+ hasComments: dialect !== 'sqlite',
120
+ hasChecks: true,
121
+ maxIdentifierLength: dialect === 'postgres' ? 63 : dialect === 'mysql' ? 64 : 128,
122
+ },
123
+ });
124
+ }
125
+ function parseStatement(stmt, state) {
126
+ const cur = new SqlCursor(stmt.text);
127
+ const first = cur.peekWord();
128
+ if (first === 'CREATE') {
129
+ cur.takeWord();
130
+ // Skip noise qualifiers.
131
+ while (cur.tryWords('OR', 'REPLACE') || cur.tryWords('GLOBAL') || cur.tryWords('LOCAL') ||
132
+ cur.tryWords('TEMPORARY') || cur.tryWords('TEMP') || cur.tryWords('UNLOGGED')) {
133
+ /* consumed */
134
+ }
135
+ const unique = cur.tryWords('UNIQUE');
136
+ const kind = cur.peekWord();
137
+ if (kind === 'TABLE') {
138
+ cur.takeWord();
139
+ parseCreateTable(cur, stmt, state);
140
+ return;
141
+ }
142
+ if (kind === 'TYPE') {
143
+ cur.takeWord();
144
+ parseCreateType(cur, state);
145
+ return;
146
+ }
147
+ if (kind === 'INDEX') {
148
+ cur.takeWord();
149
+ parseCreateIndex(cur, unique, state);
150
+ return;
151
+ }
152
+ if (kind === 'MATERIALIZED' || kind === 'VIEW') {
153
+ const materialized = kind === 'MATERIALIZED';
154
+ cur.takeWord();
155
+ if (materialized)
156
+ cur.tryWords('VIEW');
157
+ parseCreateView(cur, materialized, state);
158
+ return;
159
+ }
160
+ state.warnings.addCount('skipped-statement', `skipped CREATE ${kind ?? '?'} statement`);
161
+ return;
162
+ }
163
+ if (first === 'ALTER') {
164
+ cur.takeWord();
165
+ if (cur.tryWords('TABLE')) {
166
+ parseAlterTable(cur, stmt, state);
167
+ return;
168
+ }
169
+ state.warnings.addCount('skipped-statement', `skipped ALTER ${cur.peekWord() ?? '?'} statement`);
170
+ return;
171
+ }
172
+ if (first === 'COMMENT') {
173
+ cur.takeWord();
174
+ parseCommentOn(cur, state);
175
+ return;
176
+ }
177
+ state.warnings.addCount('skipped-statement', `skipped ${first ?? 'unknown'} statement`);
178
+ }
179
+ function qualifiedToParts(parts, state) {
180
+ if (!parts || parts.length === 0)
181
+ return null;
182
+ const name = parts[parts.length - 1];
183
+ const schema = parts.length > 1 ? parts[parts.length - 2] : state.defaultSchema;
184
+ return { schema, name };
185
+ }
186
+ function parseCreateTable(cur, stmt, state) {
187
+ cur.tryWords('IF', 'NOT', 'EXISTS');
188
+ const named = qualifiedToParts(cur.takeQualifiedIdentifier(), state);
189
+ const body = cur.takeParenGroup();
190
+ if (!named || body === null) {
191
+ state.warnings.add('unparseable-statement', `could not parse CREATE TABLE at line ${stmt.line}`);
192
+ return;
193
+ }
194
+ const table = state.builder.addTable({ name: named.name, schema: named.schema });
195
+ for (const item of splitTopLevel(body, ',', SQL_SCAN)) {
196
+ parseTableItem(item, table, state);
197
+ }
198
+ // MySQL table options tail: ENGINE=… COMMENT='…'.
199
+ const tail = cur.rest();
200
+ const commentMatch = /\bCOMMENT\s*=?\s*'((?:[^']|'')*)'/i.exec(tail);
201
+ if (commentMatch)
202
+ table.comment = commentMatch[1].replaceAll("''", "'");
203
+ }
204
+ function parseTableItem(item, table, state) {
205
+ const cur = new SqlCursor(item);
206
+ let constraintName = null;
207
+ if (cur.tryWords('CONSTRAINT'))
208
+ constraintName = cur.takeIdentifier();
209
+ const head = cur.peekWord();
210
+ if (head === 'PRIMARY' ||
211
+ head === 'FOREIGN' ||
212
+ head === 'UNIQUE' ||
213
+ head === 'CHECK' ||
214
+ head === 'KEY' ||
215
+ head === 'INDEX' ||
216
+ head === 'FULLTEXT' ||
217
+ head === 'SPATIAL' ||
218
+ head === 'EXCLUDE') {
219
+ parseTableConstraint(cur, table, constraintName, state);
220
+ return;
221
+ }
222
+ if (constraintName !== null) {
223
+ // `CONSTRAINT x <something we don't know>`
224
+ state.warnings.addCount('unsupported-constraint', `skipped unsupported constraint kind`);
225
+ return;
226
+ }
227
+ parseColumnDef(cur, table, state);
228
+ }
229
+ function parseTableConstraint(cur, table, constraintName, state) {
230
+ if (cur.tryWords('PRIMARY', 'KEY')) {
231
+ const group = cur.takeParenGroup();
232
+ if (group !== null)
233
+ table.primaryKey = identList(group);
234
+ return;
235
+ }
236
+ if (cur.tryWords('FOREIGN', 'KEY')) {
237
+ // mysql allows an index name between FOREIGN KEY and the column list.
238
+ let group = cur.takeParenGroup();
239
+ if (group === null) {
240
+ cur.takeIdentifier();
241
+ group = cur.takeParenGroup();
242
+ }
243
+ if (group === null)
244
+ return;
245
+ const columns = identList(group);
246
+ if (!cur.tryWords('REFERENCES'))
247
+ return;
248
+ const target = qualifiedToParts(cur.takeQualifiedIdentifier(), state);
249
+ if (!target)
250
+ return;
251
+ const targetCols = cur.takeParenGroup();
252
+ const actions = parseFkTail(cur);
253
+ addForeignKey(table, columns, target, targetCols === null ? null : identList(targetCols), actions, state);
254
+ return;
255
+ }
256
+ if (cur.tryWords('UNIQUE')) {
257
+ if (!cur.tryWords('KEY'))
258
+ cur.tryWords('INDEX');
259
+ let group = cur.takeParenGroup();
260
+ let name = constraintName;
261
+ if (group === null) {
262
+ name = cur.takeIdentifier() ?? name;
263
+ group = cur.takeParenGroup();
264
+ }
265
+ if (group !== null)
266
+ table.uniques.push({ name, columns: identList(group) });
267
+ return;
268
+ }
269
+ if (cur.tryWords('CHECK')) {
270
+ const expr = cur.takeParenGroup();
271
+ if (expr !== null) {
272
+ table.checks.push({ name: constraintName, expression: expr.trim() });
273
+ synthesizeCheckEnum(expr, table, state);
274
+ }
275
+ return;
276
+ }
277
+ if (cur.tryWords('KEY') || cur.tryWords('INDEX') || cur.tryWords('FULLTEXT') || cur.tryWords('SPATIAL')) {
278
+ if (!cur.tryWords('KEY'))
279
+ cur.tryWords('INDEX'); // FULLTEXT KEY / SPATIAL INDEX
280
+ const name = cur.takeIdentifier() ?? constraintName ?? `${table.name}_idx`;
281
+ const group = cur.takeParenGroup();
282
+ if (group !== null) {
283
+ table.indexes.push({ name, columns: identList(group), unique: false });
284
+ }
285
+ return;
286
+ }
287
+ state.warnings.addCount('unsupported-constraint', 'skipped unsupported table constraint');
288
+ }
289
+ function parseColumnDef(cur, table, state) {
290
+ const name = cur.takeIdentifier();
291
+ if (name === null) {
292
+ state.warnings.addCount('unparseable-column', `skipped unparseable column on "${table.name}"`);
293
+ return;
294
+ }
295
+ const typeRaw = parseTypePhrase(cur);
296
+ if (typeRaw === null) {
297
+ state.warnings.addCount('unparseable-column', `skipped unparseable column on "${table.name}"`);
298
+ return;
299
+ }
300
+ const mapped = mapSqlType(typeRaw.replace(/^[^.]*\./, ''));
301
+ const col = {
302
+ name,
303
+ dbType: typeRaw,
304
+ logicalType: mapped.logicalType,
305
+ maxLength: mapped.maxLength,
306
+ numericPrecision: mapped.numericPrecision,
307
+ numericScale: mapped.numericScale,
308
+ isArray: mapped.isArray,
309
+ };
310
+ if (isSerialType(typeRaw)) {
311
+ col.default = { kind: 'autoincrement' };
312
+ col.nullable = false;
313
+ }
314
+ // MySQL inline enum('a','b') / set('a','b').
315
+ const enumMatch = /^(enum|set)\s*\((.*)\)$/is.exec(typeRaw.trim());
316
+ if (enumMatch) {
317
+ const values = collectStrings(enumMatch[2]);
318
+ if (enumMatch[1].toLowerCase() === 'enum' && values.length > 0) {
319
+ const id = `${table.schema ?? 'public'}.${table.name}.${name}`;
320
+ state.builder.addEnum({ id, name, values, source: 'column-type' });
321
+ col.logicalType = 'enum';
322
+ col.enumRef = id;
323
+ }
324
+ else {
325
+ col.logicalType = 'text';
326
+ state.warnings.add('set-type', `MySQL SET column "${table.name}"."${name}" mapped to text`, `${table.schema ?? 'public'}.${table.name}`);
327
+ }
328
+ }
329
+ parseColumnModifiers(cur, col, table, state);
330
+ table.columns.push(col);
331
+ }
332
+ /** Consume a (possibly multi-word, possibly parameterized) SQL type phrase. */
333
+ function parseTypePhrase(cur) {
334
+ const parts = cur.takeQualifiedIdentifier();
335
+ if (!parts)
336
+ return null;
337
+ let phrase = parts.join('.');
338
+ const lower = parts[parts.length - 1].toLowerCase();
339
+ if (lower === 'double' && cur.tryWords('PRECISION'))
340
+ phrase += ' precision';
341
+ if ((lower === 'character' || lower === 'national') && cur.tryWords('VARYING'))
342
+ phrase += ' varying';
343
+ if (lower === 'bit' && cur.tryWords('VARYING'))
344
+ phrase += ' varying';
345
+ const args = cur.takeParenGroup();
346
+ if (args !== null)
347
+ phrase += `(${args})`;
348
+ if (lower === 'time' || lower === 'timestamp') {
349
+ if (cur.tryWords('WITH', 'TIME', 'ZONE'))
350
+ phrase += ' with time zone';
351
+ else if (cur.tryWords('WITHOUT', 'TIME', 'ZONE'))
352
+ phrase += ' without time zone';
353
+ }
354
+ if (lower === 'interval') {
355
+ const fields = ['YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND', 'TO'];
356
+ while (fields.includes(cur.peekWord() ?? ''))
357
+ cur.takeWord();
358
+ }
359
+ if (lower === 'int' || lower === 'integer' || lower === 'tinyint' || lower === 'smallint' ||
360
+ lower === 'mediumint' || lower === 'bigint' || lower === 'decimal' || lower === 'numeric' ||
361
+ lower === 'float' || lower === 'double') {
362
+ while (cur.tryWords('UNSIGNED') || cur.tryWords('ZEROFILL'))
363
+ phrase += '';
364
+ }
365
+ // Array suffixes: text[] / text[][].
366
+ while (cur.tryChar('[')) {
367
+ cur.tryChar(']');
368
+ phrase += '[]';
369
+ }
370
+ if (cur.tryWords('ARRAY'))
371
+ phrase += '[]';
372
+ return phrase;
373
+ }
374
+ function parseColumnModifiers(cur, col, table, state) {
375
+ while (!cur.eof()) {
376
+ if (cur.tryWords('CONSTRAINT')) {
377
+ cur.takeIdentifier();
378
+ continue;
379
+ }
380
+ if (cur.tryWords('NOT', 'NULL')) {
381
+ col.nullable = false;
382
+ continue;
383
+ }
384
+ if (cur.tryWords('NULL')) {
385
+ col.nullable = true;
386
+ continue;
387
+ }
388
+ if (cur.tryWords('DEFAULT')) {
389
+ if (cur.tryWords('NULL')) {
390
+ col.default = null;
391
+ continue;
392
+ }
393
+ const raw = takeDefaultExpression(cur);
394
+ col.default = classifySqlDefault(raw);
395
+ continue;
396
+ }
397
+ if (cur.tryWords('PRIMARY', 'KEY')) {
398
+ col.isPrimaryKey = true;
399
+ col.nullable = false;
400
+ // sqlite: INTEGER PRIMARY KEY AUTOINCREMENT
401
+ if (cur.tryWords('AUTOINCREMENT'))
402
+ col.default = { kind: 'autoincrement' };
403
+ continue;
404
+ }
405
+ if (cur.tryWords('UNIQUE')) {
406
+ cur.tryWords('KEY');
407
+ col.isUnique = true;
408
+ continue;
409
+ }
410
+ if (cur.tryWords('REFERENCES')) {
411
+ const target = qualifiedToParts(cur.takeQualifiedIdentifier(), state);
412
+ const group = cur.takeParenGroup();
413
+ const actions = parseFkTail(cur);
414
+ if (target) {
415
+ col.references = {
416
+ table: `${target.schema}.${target.name}`,
417
+ column: group === null ? undefined : identList(group)[0],
418
+ onDelete: actions.onDelete,
419
+ onUpdate: actions.onUpdate,
420
+ };
421
+ }
422
+ continue;
423
+ }
424
+ if (cur.tryWords('CHECK')) {
425
+ const expr = cur.takeParenGroup();
426
+ if (expr !== null) {
427
+ table.checks.push({ name: null, expression: expr.trim() });
428
+ synthesizeCheckEnumForColumn(expr, col, table, state);
429
+ }
430
+ cur.tryWords('NOT', 'ENFORCED');
431
+ continue;
432
+ }
433
+ if (cur.tryWords('AUTO_INCREMENT') || cur.tryWords('AUTOINCREMENT')) {
434
+ col.default = { kind: 'autoincrement' };
435
+ continue;
436
+ }
437
+ if (cur.tryWords('GENERATED')) {
438
+ if (cur.tryWords('BY', 'DEFAULT', 'AS', 'IDENTITY') || cur.tryWords('ALWAYS', 'AS', 'IDENTITY')) {
439
+ cur.takeParenGroup();
440
+ col.default = { kind: 'autoincrement' };
441
+ continue;
442
+ }
443
+ if (cur.tryWords('ALWAYS', 'AS')) {
444
+ cur.takeParenGroup();
445
+ if (!cur.tryWords('STORED'))
446
+ cur.tryWords('VIRTUAL');
447
+ col.isGenerated = true;
448
+ continue;
449
+ }
450
+ continue;
451
+ }
452
+ if (cur.tryWords('AS')) {
453
+ // sqlite/mysql generated column shorthand: `col type AS (expr)`.
454
+ cur.takeParenGroup();
455
+ if (!cur.tryWords('STORED'))
456
+ cur.tryWords('VIRTUAL');
457
+ col.isGenerated = true;
458
+ continue;
459
+ }
460
+ if (cur.tryWords('COMMENT')) {
461
+ const text = cur.takeStringLiteral();
462
+ if (text !== null)
463
+ col.comment = text;
464
+ continue;
465
+ }
466
+ if (cur.tryWords('COLLATE')) {
467
+ cur.takeQualifiedIdentifier();
468
+ continue;
469
+ }
470
+ if (cur.tryWords('CHARACTER', 'SET') || cur.tryWords('CHARSET')) {
471
+ cur.takeIdentifier();
472
+ continue;
473
+ }
474
+ if (cur.tryWords('ON', 'UPDATE')) {
475
+ // mysql `ON UPDATE CURRENT_TIMESTAMP`.
476
+ cur.takeWord();
477
+ cur.takeParenGroup();
478
+ continue;
479
+ }
480
+ const tok = cur.takeWord();
481
+ if (tok === null) {
482
+ // Punctuation we don't understand — drop the rest of this column item.
483
+ state.warnings.addCount('unsupported-column-modifier', 'ignored trailing tokens in a column definition');
484
+ return;
485
+ }
486
+ state.warnings.addCount('unsupported-column-modifier', `ignored column modifier ${tok}`);
487
+ }
488
+ }
489
+ /** Capture a DEFAULT expression: one expression unit plus trailing casts. */
490
+ function takeDefaultExpression(cur) {
491
+ const start = cur.save();
492
+ // Unit: string | number | word[(args)] | (expr)
493
+ if (cur.takeStringLiteral() === null) {
494
+ if (cur.tryNumber() === null) {
495
+ if (cur.takeParenGroup() === null) {
496
+ const word = cur.takeWord();
497
+ if (word !== null)
498
+ cur.takeParenGroup();
499
+ }
500
+ }
501
+ }
502
+ // Trailing casts: ::type, possibly multi-word / array.
503
+ let rest = cur.rest();
504
+ while (rest.startsWith('::')) {
505
+ cur.tryChar(':');
506
+ cur.tryChar(':');
507
+ cur.takeQualifiedIdentifier();
508
+ if (!cur.tryWords('VARYING'))
509
+ cur.tryWords('PRECISION');
510
+ cur.takeParenGroup();
511
+ while (cur.tryChar('['))
512
+ cur.tryChar(']');
513
+ rest = cur.rest();
514
+ }
515
+ return cur.textFrom(start);
516
+ }
517
+ function parseFkTail(cur) {
518
+ let onDelete = null;
519
+ let onUpdate = null;
520
+ for (;;) {
521
+ if (cur.tryWords('MATCH')) {
522
+ cur.takeWord();
523
+ continue;
524
+ }
525
+ if (cur.tryWords('ON', 'DELETE')) {
526
+ onDelete = takeFkAction(cur);
527
+ continue;
528
+ }
529
+ if (cur.tryWords('ON', 'UPDATE')) {
530
+ onUpdate = takeFkAction(cur);
531
+ continue;
532
+ }
533
+ if (cur.tryWords('NOT', 'DEFERRABLE') || cur.tryWords('DEFERRABLE'))
534
+ continue;
535
+ if (cur.tryWords('INITIALLY')) {
536
+ cur.takeWord();
537
+ continue;
538
+ }
539
+ if (cur.tryWords('NOT', 'VALID'))
540
+ continue;
541
+ return { onDelete, onUpdate };
542
+ }
543
+ }
544
+ function takeFkAction(cur) {
545
+ if (cur.tryWords('SET', 'NULL'))
546
+ return 'set-null';
547
+ if (cur.tryWords('SET', 'DEFAULT'))
548
+ return 'set-default';
549
+ if (cur.tryWords('NO', 'ACTION'))
550
+ return 'no-action';
551
+ const word = cur.takeWord();
552
+ if (word !== null && FK_ACTION_MAP[word] !== undefined)
553
+ return FK_ACTION_MAP[word];
554
+ return null;
555
+ }
556
+ function identList(group) {
557
+ return splitTopLevel(group, ',', SQL_SCAN)
558
+ .map((part) => {
559
+ const cur = new SqlCursor(part);
560
+ return cur.takeIdentifier() ?? part.trim();
561
+ })
562
+ .filter((s) => s.length > 0);
563
+ }
564
+ function addForeignKey(table, columns, target, targetColumns, actions, state) {
565
+ const targetName = `${target.schema}.${target.name}`;
566
+ if (columns.length === 1) {
567
+ const col = table.columns.find((c) => c.name === columns[0]);
568
+ if (col) {
569
+ col.references = {
570
+ table: targetName,
571
+ column: targetColumns?.[0],
572
+ onDelete: actions.onDelete,
573
+ onUpdate: actions.onUpdate,
574
+ };
575
+ return;
576
+ }
577
+ }
578
+ const targetTable = state.builder.getTable(targetName);
579
+ const toColumns = targetColumns ?? targetTable?.primaryKey ?? [];
580
+ if (toColumns.length !== columns.length) {
581
+ state.warnings.add('unresolved-reference', `composite foreign key on "${table.name}" (${columns.join(', ')}) could not be resolved`);
582
+ return;
583
+ }
584
+ state.builder.addRelation({
585
+ kind: 'declared-fk',
586
+ cardinality: 'one-to-many',
587
+ from: { table: `${table.schema ?? 'public'}.${table.name}`, columns },
588
+ to: { table: targetName, columns: toColumns },
589
+ onDelete: actions.onDelete,
590
+ onUpdate: actions.onUpdate,
591
+ });
592
+ }
593
+ /* --------------------------- CHECK → enum synthesis --------------------------- */
594
+ function synthesizeCheckEnum(expression, table, state) {
595
+ const found = extractCheckEnum(expression);
596
+ if (!found)
597
+ return;
598
+ const col = table.columns.find((c) => c.name === found.column);
599
+ if (!col || col.logicalType === 'enum')
600
+ return;
601
+ applyCheckEnum(col, found.values, table, state);
602
+ }
603
+ function synthesizeCheckEnumForColumn(expression, col, table, state) {
604
+ const found = extractCheckEnum(expression);
605
+ if (!found || found.column !== col.name || col.logicalType === 'enum')
606
+ return;
607
+ applyCheckEnum(col, found.values, table, state);
608
+ }
609
+ function applyCheckEnum(col, values, table, state) {
610
+ const id = `${table.schema ?? 'public'}.${table.name}.${col.name}`;
611
+ state.builder.addEnum({ id, name: col.name, values, source: 'check' });
612
+ col.logicalType = 'enum';
613
+ col.enumRef = id;
614
+ }
615
+ /* --------------------------------- other DDL --------------------------------- */
616
+ function parseCreateType(cur, state) {
617
+ const named = qualifiedToParts(cur.takeQualifiedIdentifier(), state);
618
+ if (!named || !cur.tryWords('AS', 'ENUM')) {
619
+ state.warnings.addCount('skipped-statement', 'skipped CREATE TYPE (non-enum) statement');
620
+ return;
621
+ }
622
+ const group = cur.takeParenGroup();
623
+ if (group === null)
624
+ return;
625
+ const values = collectStrings(group);
626
+ if (values.length === 0)
627
+ return;
628
+ const id = `${named.schema}.${named.name}`;
629
+ state.builder.addEnum({ id, name: named.name, values, source: 'native' });
630
+ state.enumTypes.set(named.name.toLowerCase(), id);
631
+ state.enumTypes.set(`${named.schema}.${named.name}`.toLowerCase(), id);
632
+ }
633
+ function parseCreateIndex(cur, unique, state) {
634
+ cur.tryWords('CONCURRENTLY');
635
+ cur.tryWords('IF', 'NOT', 'EXISTS');
636
+ const indexName = cur.takeIdentifier() ?? 'index';
637
+ if (!cur.tryWords('ON'))
638
+ return;
639
+ cur.tryWords('ONLY');
640
+ const named = qualifiedToParts(cur.takeQualifiedIdentifier(), state);
641
+ if (!named)
642
+ return;
643
+ const table = state.builder.getTable(`${named.schema}.${named.name}`);
644
+ if (!table) {
645
+ state.warnings.add('unknown-table', `CREATE INDEX on unknown table "${named.name}"`);
646
+ return;
647
+ }
648
+ let method = null;
649
+ if (cur.tryWords('USING'))
650
+ method = (cur.takeIdentifier() ?? '').toLowerCase() || null;
651
+ const group = cur.takeParenGroup();
652
+ if (group === null)
653
+ return;
654
+ const parts = splitTopLevel(group, ',', SQL_SCAN);
655
+ const known = new Set(table.columns.map((c) => c.name));
656
+ const cols = [];
657
+ let isExpression = false;
658
+ for (const part of parts) {
659
+ const partCur = new SqlCursor(part);
660
+ const ident = partCur.takeIdentifier();
661
+ if (ident !== null && known.has(ident))
662
+ cols.push(ident);
663
+ else
664
+ isExpression = true;
665
+ }
666
+ const partial = /\bWHERE\b/i.test(cur.rest());
667
+ table.indexes.push({
668
+ name: indexName,
669
+ columns: cols,
670
+ expression: isExpression ? group.trim() : null,
671
+ unique,
672
+ method,
673
+ partial,
674
+ });
675
+ if (unique && cols.length > 0 && !isExpression) {
676
+ table.uniques.push({ name: indexName, columns: cols });
677
+ }
678
+ }
679
+ function parseCreateView(cur, materialized, state) {
680
+ cur.tryWords('IF', 'NOT', 'EXISTS');
681
+ const named = qualifiedToParts(cur.takeQualifiedIdentifier(), state);
682
+ if (!named)
683
+ return;
684
+ // Columns only when explicitly listed: CREATE VIEW v (a, b) AS …
685
+ const group = cur.takeParenGroup();
686
+ if (group === null) {
687
+ state.warnings.addCount('skipped-statement', `skipped CREATE ${materialized ? 'MATERIALIZED VIEW' : 'VIEW'} without column list`);
688
+ return;
689
+ }
690
+ const table = state.builder.addTable({
691
+ name: named.name,
692
+ schema: named.schema,
693
+ kind: materialized ? 'materialized-view' : 'view',
694
+ });
695
+ for (const colName of identList(group)) {
696
+ table.columns.push({ name: colName, dbType: 'unknown', logicalType: 'unknown' });
697
+ }
698
+ }
699
+ function parseAlterTable(cur, stmt, state) {
700
+ cur.tryWords('IF', 'EXISTS');
701
+ cur.tryWords('ONLY');
702
+ const named = qualifiedToParts(cur.takeQualifiedIdentifier(), state);
703
+ if (!named)
704
+ return;
705
+ const table = state.builder.getTable(`${named.schema}.${named.name}`);
706
+ if (!table) {
707
+ state.warnings.add('unknown-table', `ALTER TABLE on unknown table "${named.name}" skipped`);
708
+ return;
709
+ }
710
+ if (cur.tryWords('ADD', 'CONSTRAINT')) {
711
+ const constraintName = cur.takeIdentifier();
712
+ parseTableConstraint(cur, table, constraintName, state);
713
+ return;
714
+ }
715
+ if (cur.tryWords('ADD')) {
716
+ const head = cur.peekWord();
717
+ if (head === 'PRIMARY' || head === 'FOREIGN' || head === 'UNIQUE' || head === 'CHECK' ||
718
+ head === 'KEY' || head === 'INDEX') {
719
+ parseTableConstraint(cur, table, null, state);
720
+ return;
721
+ }
722
+ cur.tryWords('COLUMN');
723
+ cur.tryWords('IF', 'NOT', 'EXISTS');
724
+ parseColumnDef(cur, table, state);
725
+ return;
726
+ }
727
+ if (cur.tryWords('ALTER', 'COLUMN') || cur.tryWords('ALTER')) {
728
+ const colName = cur.takeIdentifier();
729
+ const col = table.columns.find((c) => c.name === colName);
730
+ if (!col)
731
+ return;
732
+ if (cur.tryWords('SET', 'DEFAULT')) {
733
+ col.default = classifySqlDefault(takeDefaultExpression(cur));
734
+ return;
735
+ }
736
+ if (cur.tryWords('SET', 'NOT', 'NULL')) {
737
+ col.nullable = false;
738
+ return;
739
+ }
740
+ if (cur.tryWords('DROP', 'NOT', 'NULL')) {
741
+ col.nullable = true;
742
+ return;
743
+ }
744
+ state.warnings.addCount('skipped-statement', 'skipped ALTER TABLE ALTER COLUMN action');
745
+ return;
746
+ }
747
+ state.warnings.addCount('skipped-statement', `skipped ALTER TABLE ${cur.peekWord() ?? '?'} action`);
748
+ }
749
+ function parseCommentOn(cur, state) {
750
+ if (!cur.tryWords('ON'))
751
+ return;
752
+ if (cur.tryWords('TABLE')) {
753
+ const named = qualifiedToParts(cur.takeQualifiedIdentifier(), state);
754
+ if (named && cur.tryWords('IS')) {
755
+ const text = cur.takeStringLiteral();
756
+ const table = state.builder.getTable(`${named.schema}.${named.name}`);
757
+ if (table && text !== null)
758
+ table.comment = text;
759
+ }
760
+ return;
761
+ }
762
+ if (cur.tryWords('COLUMN')) {
763
+ const parts = cur.takeQualifiedIdentifier();
764
+ if (parts && parts.length >= 2 && cur.tryWords('IS')) {
765
+ const text = cur.takeStringLiteral();
766
+ const colName = parts[parts.length - 1];
767
+ const tableRef = parts.slice(0, -1).join('.');
768
+ const table = state.builder.getTable(tableRef);
769
+ const col = table?.columns.find((c) => c.name === colName);
770
+ if (col && text !== null)
771
+ col.comment = text;
772
+ }
773
+ return;
774
+ }
775
+ state.warnings.addCount('skipped-statement', `skipped COMMENT ON ${cur.peekWord() ?? '?'} statement`);
776
+ }
777
+ /** Late pass: columns typed with a `CREATE TYPE … AS ENUM` name become enum columns. */
778
+ function resolveEnumColumns(state) {
779
+ if (state.enumTypes.size === 0)
780
+ return;
781
+ for (const table of state.builder.allTables()) {
782
+ for (const col of table.columns) {
783
+ if (col.logicalType !== 'unknown' || col.dbType === undefined)
784
+ continue;
785
+ const base = col.dbType.replace(/\(.*\)/s, '').replace(/\[\]/g, '').trim().toLowerCase();
786
+ const id = state.enumTypes.get(base) ?? state.enumTypes.get(base.replace(/^[^.]*\./, ''));
787
+ if (id !== undefined) {
788
+ col.logicalType = 'enum';
789
+ col.enumRef = id;
790
+ }
791
+ }
792
+ }
793
+ }
794
+ //# sourceMappingURL=sql.js.map