ormbridge 0.12.1 → 0.14.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 (84) hide show
  1. package/README.md +19 -4
  2. package/dist/cliRunner.js +19 -1
  3. package/dist/cliRunner.js.map +1 -1
  4. package/dist/config.d.ts +5 -0
  5. package/dist/config.js +17 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/emitters/jpa.d.ts +17 -2
  8. package/dist/emitters/jpa.js +55 -7
  9. package/dist/emitters/jpa.js.map +1 -1
  10. package/dist/emitters/jpaKeyClass.d.ts +18 -0
  11. package/dist/emitters/jpaKeyClass.js +88 -0
  12. package/dist/emitters/jpaKeyClass.js.map +1 -0
  13. package/dist/emitters/jpaKeys.d.ts +1 -7
  14. package/dist/emitters/jpaKeys.js +0 -43
  15. package/dist/emitters/jpaKeys.js.map +1 -1
  16. package/dist/emitters/jpaRelations.d.ts +5 -1
  17. package/dist/emitters/jpaRelations.js +7 -1
  18. package/dist/emitters/jpaRelations.js.map +1 -1
  19. package/dist/emitters/jpaRender.d.ts +5 -1
  20. package/dist/emitters/jpaRender.js +11 -4
  21. package/dist/emitters/jpaRender.js.map +1 -1
  22. package/dist/emitters/jpaTypes.d.ts +9 -3
  23. package/dist/emitters/jpaTypes.js +8 -0
  24. package/dist/emitters/jpaTypes.js.map +1 -1
  25. package/dist/emitters/neo4jEdges.d.ts +46 -0
  26. package/dist/emitters/neo4jEdges.js +147 -0
  27. package/dist/emitters/neo4jEdges.js.map +1 -0
  28. package/dist/emitters/neo4jGraph.d.ts +9 -0
  29. package/dist/emitters/neo4jGraph.js +9 -5
  30. package/dist/emitters/neo4jGraph.js.map +1 -1
  31. package/dist/emitters/neo4jRelationships.d.ts +3 -1
  32. package/dist/emitters/neo4jRelationships.js +1 -1
  33. package/dist/emitters/neo4jRelationships.js.map +1 -1
  34. package/dist/emitters/neomodel.d.ts +20 -0
  35. package/dist/emitters/neomodel.js +415 -0
  36. package/dist/emitters/neomodel.js.map +1 -0
  37. package/dist/emitters/neomodelNames.d.ts +25 -0
  38. package/dist/emitters/neomodelNames.js +159 -0
  39. package/dist/emitters/neomodelNames.js.map +1 -0
  40. package/dist/emitters/neomodelProperties.d.ts +48 -0
  41. package/dist/emitters/neomodelProperties.js +220 -0
  42. package/dist/emitters/neomodelProperties.js.map +1 -0
  43. package/dist/emitters/sdn.d.ts +27 -0
  44. package/dist/emitters/sdn.js +584 -0
  45. package/dist/emitters/sdn.js.map +1 -0
  46. package/dist/emitters/sdnTypes.d.ts +7 -0
  47. package/dist/emitters/sdnTypes.js +81 -0
  48. package/dist/emitters/sdnTypes.js.map +1 -0
  49. package/dist/formats.d.ts +11 -0
  50. package/dist/formats.js +48 -3
  51. package/dist/formats.js.map +1 -1
  52. package/dist/index.d.ts +9 -2
  53. package/dist/index.js +5 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/io.js +62 -21
  56. package/dist/io.js.map +1 -1
  57. package/dist/parsers/jpaBuild.js +2 -1
  58. package/dist/parsers/jpaBuild.js.map +1 -1
  59. package/dist/parsers/jpaClasses.js +7 -1
  60. package/dist/parsers/jpaClasses.js.map +1 -1
  61. package/dist/parsers/jpaColumns.d.ts +4 -1
  62. package/dist/parsers/jpaColumns.js +8 -2
  63. package/dist/parsers/jpaColumns.js.map +1 -1
  64. package/dist/parsers/jpaEntities.js +15 -0
  65. package/dist/parsers/jpaEntities.js.map +1 -1
  66. package/dist/parsers/jpaModel.d.ts +6 -0
  67. package/dist/parsers/sqlDdl.js +1306 -65
  68. package/dist/parsers/sqlDdl.js.map +1 -1
  69. package/dist/parsers/sqlMigrationOrder.d.ts +8 -0
  70. package/dist/parsers/sqlMigrationOrder.js +110 -0
  71. package/dist/parsers/sqlMigrationOrder.js.map +1 -0
  72. package/dist/parsers/sqlSyntax.d.ts +6 -0
  73. package/dist/parsers/sqlSyntax.js +15 -2
  74. package/dist/parsers/sqlSyntax.js.map +1 -1
  75. package/dist/parsers/zod.d.ts +11 -0
  76. package/dist/parsers/zod.js +796 -0
  77. package/dist/parsers/zod.js.map +1 -0
  78. package/dist/parsers/zodFields.d.ts +73 -0
  79. package/dist/parsers/zodFields.js +681 -0
  80. package/dist/parsers/zodFields.js.map +1 -0
  81. package/dist/parsers/zodSyntax.d.ts +72 -0
  82. package/dist/parsers/zodSyntax.js +347 -0
  83. package/dist/parsers/zodSyntax.js.map +1 -0
  84. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { basename } from 'node:path';
2
2
  import { singularize, toPascalCase, toSnakeCase } from '../naming.js';
3
3
  import { describeThrown, err, ok } from '../result.js';
4
- import { Cursor, detectDialect, MAX_SOURCE_CHARACTERS, sourceOf, splitStatements, splitTopLevel, tokenize, } from './sqlSyntax.js';
4
+ import { Cursor, bestDialect, detectDialect, dialectScores, MAX_SOURCE_CHARACTERS, sourceOf, splitStatements, splitTopLevel, tokenize, } from './sqlSyntax.js';
5
5
  import { defaultExpression, interpretDefault, mapType, parseType, } from './sqlTypes.js';
6
6
  const DEFAULT_SCHEMAS = new Set(['public', 'dbo', 'main']);
7
7
  const MAX_WARNINGS = 400;
@@ -15,10 +15,13 @@ function warn(ctx, message) {
15
15
  ctx.suppressedWarnings += 1;
16
16
  }
17
17
  }
18
- function recordSkipped(ctx, file, kind, line) {
18
+ function recordSkipped(ctx, file, kind, line, table) {
19
19
  const entry = ctx.skipped.get(kind);
20
20
  if (entry === undefined) {
21
- ctx.skipped.set(kind, { count: 1, first: `${file.name}:${line}` });
21
+ ctx.skipped.set(kind, {
22
+ count: 1,
23
+ first: `${file.name}:${line}${table === undefined ? '' : `, table "${table}"`}`,
24
+ });
22
25
  }
23
26
  else {
24
27
  entry.count += 1;
@@ -54,8 +57,13 @@ function parseSources(sources, options) {
54
57
  suppressedWarnings: 0,
55
58
  skipped: new Map(),
56
59
  };
60
+ // A migration with no dialect hint of its own (ALTER TABLE t DROP COLUMN c) is read like the set it belongs to.
61
+ const setDialect = options.dialect ??
62
+ (sources.length > 1
63
+ ? detectDialect(sources.map((source) => source.text).join('\n'))
64
+ : 'postgresql');
57
65
  for (const source of sources) {
58
- const dialect = options.dialect ?? detectDialect(source.text);
66
+ const dialect = options.dialect ?? dialectOfFile(source.text, setDialect);
59
67
  const file = {
60
68
  name: basename(source.path),
61
69
  text: source.text,
@@ -87,6 +95,21 @@ function parseSources(sources, options) {
87
95
  }
88
96
  return ok(schema);
89
97
  }
98
+ /**
99
+ * The dialect of one file of a set: its own hint when it has one, otherwise the dialect of the set. Backticks
100
+ * alone (what drizzle-kit writes for SQLite) are too weak a hint to move a file out of a SQLite set.
101
+ */
102
+ function dialectOfFile(text, setDialect) {
103
+ const scores = dialectScores(text);
104
+ const best = bestDialect(scores);
105
+ if (scores[best] === 0) {
106
+ return setDialect;
107
+ }
108
+ if (best === 'mysql' && scores.mysql === 1 && setDialect === 'sqlite') {
109
+ return setDialect;
110
+ }
111
+ return best;
112
+ }
90
113
  function describeSkipReason(kind) {
91
114
  if (kind === 'data') {
92
115
  return 'row data is not part of the schema.';
@@ -133,6 +156,8 @@ const QUIET_STATEMENTS = new Set([
133
156
  'EXECUTE',
134
157
  'CHECKPOINT',
135
158
  'DISCARD',
159
+ // TRUNCATE empties a table and changes no column, key or index.
160
+ 'TRUNCATE',
136
161
  ]);
137
162
  /** Data statements, reported together as one warning. */
138
163
  const DATA_STATEMENTS = new Set([
@@ -141,7 +166,6 @@ const DATA_STATEMENTS = new Set([
141
166
  'DELETE',
142
167
  'SELECT',
143
168
  'COPY',
144
- 'TRUNCATE',
145
169
  'MERGE',
146
170
  'REPLACE',
147
171
  'WITH',
@@ -175,6 +199,11 @@ function handleStatement(ctx, file, tokens) {
175
199
  recordSkipped(ctx, file, 'unrecognised', line);
176
200
  return;
177
201
  }
202
+ if ((head.up === 'EXEC' || head.up === 'EXECUTE') &&
203
+ cursor.isKeyword('SP_RENAME', 1)) {
204
+ handleSpRename(ctx, file, cursor);
205
+ return;
206
+ }
178
207
  if (QUIET_STATEMENTS.has(head.up)) {
179
208
  return;
180
209
  }
@@ -192,6 +221,9 @@ function handleStatement(ctx, file, tokens) {
192
221
  case 'DROP':
193
222
  handleDrop(ctx, file, cursor);
194
223
  return;
224
+ case 'RENAME':
225
+ handleRename(ctx, file, cursor);
226
+ return;
195
227
  default:
196
228
  recordSkipped(ctx, file, head.up, line);
197
229
  }
@@ -343,6 +375,7 @@ function createTable(ctx, file, cursor, line) {
343
375
  indexes: [],
344
376
  checks: [],
345
377
  withoutRowid: false,
378
+ implicitIndexes: new Set(),
346
379
  dialect: file.dialect,
347
380
  line,
348
381
  file: file.name,
@@ -723,6 +756,7 @@ function parseTableConstraint(ctx, file, cursor, label) {
723
756
  column: column.value,
724
757
  tokens: expression,
725
758
  text: defaultExpression(expression, file.text),
759
+ ...(name === undefined ? {} : { name }),
726
760
  };
727
761
  }
728
762
  return { kind: 'ignored' };
@@ -795,6 +829,11 @@ function applyConstraint(ctx, table, result) {
795
829
  return;
796
830
  case 'reference':
797
831
  table.references.push(result.reference);
832
+ if (table.dialect === 'mysql') {
833
+ table.implicitIndexes.add((result.reference.name ??
834
+ result.reference.columns[0] ??
835
+ '').toLowerCase());
836
+ }
798
837
  return;
799
838
  case 'check':
800
839
  table.checks.push(result.check);
@@ -810,6 +849,12 @@ function applyConstraint(ctx, table, result) {
810
849
  }
811
850
  column.defaultTokens = result.tokens;
812
851
  column.defaultText = result.text;
852
+ if (result.name === undefined) {
853
+ delete column.defaultName;
854
+ }
855
+ else {
856
+ column.defaultName = result.name;
857
+ }
813
858
  return;
814
859
  }
815
860
  default:
@@ -941,6 +986,10 @@ function parseColumn(ctx, file, cursor, table, label) {
941
986
  const tokens = readDefault(cursor);
942
987
  column.defaultTokens = tokens;
943
988
  column.defaultText = defaultExpression(tokens, file.text);
989
+ if (pendingName !== undefined) {
990
+ column.defaultName = pendingName;
991
+ pendingName = undefined;
992
+ }
944
993
  continue;
945
994
  }
946
995
  case 'PRIMARY':
@@ -1154,7 +1203,7 @@ function createIndex(ctx, file, cursor, line) {
1154
1203
  return;
1155
1204
  }
1156
1205
  cursor.acceptKeyword('CONCURRENTLY');
1157
- cursor.acceptKeywords('IF', 'NOT', 'EXISTS');
1206
+ const ifNotExists = cursor.acceptKeywords('IF', 'NOT', 'EXISTS');
1158
1207
  let name;
1159
1208
  if (!cursor.isKeyword('ON')) {
1160
1209
  const parsed = parseQualifiedName(cursor);
@@ -1208,9 +1257,31 @@ function createIndex(ctx, file, cursor, line) {
1208
1257
  warn(ctx, `${file.name}:${line}: the index${name === undefined ? '' : ` "${name}"`} is on the unknown table "${target.name}" and was skipped.`);
1209
1258
  return;
1210
1259
  }
1260
+ if (ifNotExists &&
1261
+ name !== undefined &&
1262
+ table.indexes.some((existing) => existing.name !== undefined && sameName(existing.name, name))) {
1263
+ return;
1264
+ }
1265
+ const elements = readIndexElements(group);
1266
+ if (file.dialect === 'mysql' &&
1267
+ isUnique &&
1268
+ kind === undefined &&
1269
+ method === undefined &&
1270
+ !isPartial &&
1271
+ elements.every((element) => element.column !== undefined &&
1272
+ !element.descending &&
1273
+ element.length === undefined &&
1274
+ element.ops === undefined)) {
1275
+ // In MySQL a unique index is a unique key: SHOW CREATE TABLE writes it as UNIQUE KEY.
1276
+ table.uniques.push({
1277
+ columns: elements.map((element) => element.column ?? ''),
1278
+ ...(name === undefined ? {} : { name }),
1279
+ });
1280
+ return;
1281
+ }
1211
1282
  table.indexes.push({
1212
1283
  ...(name === undefined ? {} : { name }),
1213
- elements: readIndexElements(group),
1284
+ elements,
1214
1285
  isUnique,
1215
1286
  ...(kind === undefined ? {} : { kind }),
1216
1287
  ...(method === undefined || method === 'btree' ? {} : { method }),
@@ -1280,40 +1351,508 @@ function findEnum(ctx, name) {
1280
1351
  const found = ctx.enumsByName.get((typeof name === 'string' ? name : name.name).toLowerCase());
1281
1352
  return found === undefined || found.dropped ? undefined : found;
1282
1353
  }
1354
+ // ---------------------------------------------------------------------------
1355
+ // Replaying schema changes (ALTER, RENAME, DROP)
1356
+ // ---------------------------------------------------------------------------
1357
+ function sameName(left, right) {
1358
+ return left.toLowerCase() === right.toLowerCase();
1359
+ }
1360
+ function escapeRegExp(value) {
1361
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1362
+ }
1363
+ /** Says why a statement did not change anything, naming the file, line and statement. */
1364
+ function warnStatement(ctx, file, line, statement, problem) {
1365
+ warn(ctx, `${file.name}:${line}: ${statement}: ${problem}; the statement was skipped.`);
1366
+ }
1367
+ /**
1368
+ * Gives every unnamed constraint and index the name the database would have given it. The database keeps that
1369
+ * name when the table or a column is renamed (`t_a_key` stays `t_a_key` after `RENAME COLUMN a TO b`), while
1370
+ * the name this reader would work out follows the new names, so it is written down before a rename.
1371
+ */
1372
+ function freezeNames(table) {
1373
+ if (table.primaryKey !== undefined && table.primaryKey.name === undefined) {
1374
+ const name = impliedName(table, 'primaryKey', 0, table.primaryKey.columns);
1375
+ if (name !== undefined && table.dialect !== 'mysql') {
1376
+ table.primaryKey.name = name;
1377
+ }
1378
+ }
1379
+ table.uniques.forEach((unique, position) => {
1380
+ unique.name ??= impliedName(table, 'unique', position, unique.columns);
1381
+ });
1382
+ table.references.forEach((reference, position) => {
1383
+ reference.name ??= impliedName(table, 'reference', position, reference.columns);
1384
+ });
1385
+ table.checks.forEach((check, position) => {
1386
+ check.name ??= impliedName(table, 'check', position, checkColumns(table, check));
1387
+ });
1388
+ table.indexes.forEach((index, position) => {
1389
+ index.name ??= impliedName(table, 'index', position, index.elements
1390
+ .map((element) => element.column ?? '')
1391
+ .filter((column) => column !== ''));
1392
+ });
1393
+ }
1394
+ /** Drops the primary key. Its columns keep the NOT NULL they got from it, in every dialect. */
1395
+ function dropPrimaryKey(table) {
1396
+ if (table.primaryKey === undefined) {
1397
+ return;
1398
+ }
1399
+ for (const name of table.primaryKey.columns) {
1400
+ const column = findColumn(table, name);
1401
+ if (column !== undefined) {
1402
+ column.notNull = true;
1403
+ }
1404
+ }
1405
+ delete table.primaryKey;
1406
+ }
1407
+ function rebuildColumnIndex(table) {
1408
+ table.columnIndex.clear();
1409
+ for (const column of table.columns) {
1410
+ const key = column.name.toLowerCase();
1411
+ if (!table.columnIndex.has(key)) {
1412
+ table.columnIndex.set(key, column);
1413
+ }
1414
+ }
1415
+ }
1416
+ function targetOf(ctx, owner, reference) {
1417
+ return lookupTable(ctx, {
1418
+ ...(reference.refSchema === undefined
1419
+ ? {}
1420
+ : { schema: reference.refSchema }),
1421
+ name: reference.refTable,
1422
+ }, owner.dialect);
1423
+ }
1424
+ /** The foreign keys of all tables (the table itself included) that point at `target`. */
1425
+ function inboundReferences(ctx, target) {
1426
+ const found = [];
1427
+ for (const owner of ctx.tables) {
1428
+ if (owner.dropped) {
1429
+ continue;
1430
+ }
1431
+ for (const reference of owner.references) {
1432
+ if (targetOf(ctx, owner, reference) === target) {
1433
+ found.push({ owner, reference });
1434
+ }
1435
+ }
1436
+ }
1437
+ return found;
1438
+ }
1439
+ function renameTable(ctx, file, table, newName, line) {
1440
+ if (sameName(table.name, newName)) {
1441
+ table.name = newName;
1442
+ return;
1443
+ }
1444
+ const clash = lookupTable(ctx, {
1445
+ ...(table.schema === undefined ? {} : { schema: table.schema }),
1446
+ name: newName,
1447
+ }, table.dialect);
1448
+ if (clash !== undefined && clash !== table) {
1449
+ warnStatement(ctx, file, line, `RENAME of the table "${table.name}" to "${newName}"`, `a table named "${newName}" exists already`);
1450
+ return;
1451
+ }
1452
+ const inbound = inboundReferences(ctx, table);
1453
+ freezeNames(table);
1454
+ ctx.tableByKey.delete(tableKey(table.schema, table.name));
1455
+ const oldList = ctx.tablesByName.get(table.name.toLowerCase()) ?? [];
1456
+ ctx.tablesByName.set(table.name.toLowerCase(), oldList.filter((candidate) => candidate !== table));
1457
+ table.name = newName;
1458
+ ctx.tableByKey.set(tableKey(table.schema, table.name), table);
1459
+ const newList = ctx.tablesByName.get(newName.toLowerCase()) ?? [];
1460
+ newList.push(table);
1461
+ ctx.tablesByName.set(newName.toLowerCase(), newList);
1462
+ for (const entry of inbound) {
1463
+ entry.reference.refTable = newName;
1464
+ }
1465
+ }
1466
+ function replaceName(names, from, to) {
1467
+ for (let at = 0; at < names.length; at += 1) {
1468
+ if (sameName(names[at] ?? '', from)) {
1469
+ names[at] = to;
1470
+ }
1471
+ }
1472
+ }
1473
+ function mentions(tokens, name) {
1474
+ return tokens.some((token) => (token.kind === 'word' || token.kind === 'ident') &&
1475
+ sameName(token.value, name));
1476
+ }
1477
+ function renameColumn(ctx, table, column, newName) {
1478
+ const oldName = column.name;
1479
+ const inbound = inboundReferences(ctx, table);
1480
+ freezeNames(table);
1481
+ column.name = newName;
1482
+ rebuildColumnIndex(table);
1483
+ if (table.primaryKey !== undefined) {
1484
+ replaceName(table.primaryKey.columns, oldName, newName);
1485
+ }
1486
+ for (const unique of table.uniques) {
1487
+ replaceName(unique.columns, oldName, newName);
1488
+ }
1489
+ for (const index of table.indexes) {
1490
+ for (const element of index.elements) {
1491
+ if (element.column !== undefined && sameName(element.column, oldName)) {
1492
+ element.column = newName;
1493
+ }
1494
+ }
1495
+ }
1496
+ for (const reference of table.references) {
1497
+ replaceName(reference.columns, oldName, newName);
1498
+ }
1499
+ for (const entry of inbound) {
1500
+ replaceName(entry.reference.refColumns, oldName, newName);
1501
+ }
1502
+ const pattern = new RegExp(`\\b${escapeRegExp(oldName)}\\b`, 'gi');
1503
+ for (const check of table.checks) {
1504
+ check.tokens = check.tokens.map((token) => (token.kind === 'word' || token.kind === 'ident') &&
1505
+ sameName(token.value, oldName)
1506
+ ? {
1507
+ ...token,
1508
+ value: newName,
1509
+ up: token.kind === 'word' ? newName.toUpperCase() : '',
1510
+ }
1511
+ : token);
1512
+ check.text = check.text.replace(pattern, newName);
1513
+ }
1514
+ for (const other of table.columns) {
1515
+ if (other.generated !== undefined) {
1516
+ other.generated = {
1517
+ ...other.generated,
1518
+ expression: other.generated.expression.replace(pattern, newName),
1519
+ };
1520
+ }
1521
+ }
1522
+ }
1523
+ /**
1524
+ * Removes a column and what the database removes with it: PostgreSQL, SQLite and SQL Server drop every
1525
+ * key, index and check that mentions the column, MySQL only takes the column out of its keys and indexes
1526
+ * (and drops the ones left without a column). Foreign keys of other tables that point at the column go too
1527
+ * (the effect of CASCADE, the only way to drop a referenced column).
1528
+ */
1529
+ function removeColumn(table, column, inbound) {
1530
+ table.columns = table.columns.filter((candidate) => candidate !== column);
1531
+ rebuildColumnIndex(table);
1532
+ const name = column.name;
1533
+ const has = (names) => names.some((candidate) => sameName(candidate, name));
1534
+ const without = (names) => names.filter((candidate) => !sameName(candidate, name));
1535
+ if (table.dialect === 'mysql') {
1536
+ if (table.primaryKey !== undefined && has(table.primaryKey.columns)) {
1537
+ const remaining = without(table.primaryKey.columns);
1538
+ if (remaining.length === 0) {
1539
+ delete table.primaryKey;
1540
+ }
1541
+ else {
1542
+ table.primaryKey.columns = remaining;
1543
+ }
1544
+ }
1545
+ table.uniques = table.uniques.filter((unique) => {
1546
+ unique.columns = without(unique.columns);
1547
+ return unique.columns.length > 0;
1548
+ });
1549
+ table.indexes = table.indexes.filter((index) => {
1550
+ index.elements = index.elements.filter((element) => element.column === undefined || !sameName(element.column, name));
1551
+ return index.elements.length > 0;
1552
+ });
1553
+ }
1554
+ else {
1555
+ if (table.primaryKey !== undefined && has(table.primaryKey.columns)) {
1556
+ dropPrimaryKey(table);
1557
+ }
1558
+ table.uniques = table.uniques.filter((unique) => !has(unique.columns));
1559
+ table.indexes = table.indexes.filter((index) => !index.elements.some((element) => element.column !== undefined && sameName(element.column, name)));
1560
+ }
1561
+ table.references = table.references.filter((reference) => !has(reference.columns));
1562
+ table.checks = table.checks.filter((check) => !mentions(check.tokens, name));
1563
+ for (const entry of inbound) {
1564
+ if (has(entry.reference.refColumns)) {
1565
+ entry.owner.references = entry.owner.references.filter((reference) => reference !== entry.reference);
1566
+ }
1567
+ }
1568
+ }
1569
+ /** Foreign keys that point at a table that is going away (DROP TABLE ... CASCADE). */
1570
+ function removeInbound(inbound, except) {
1571
+ for (const entry of inbound) {
1572
+ if (entry.owner !== except) {
1573
+ entry.owner.references = entry.owner.references.filter((reference) => reference !== entry.reference);
1574
+ }
1575
+ }
1576
+ }
1577
+ /** Builds the unique name a database gives to an unnamed constraint or index, to find it by that name. */
1578
+ function impliedName(table, kind, position, columns) {
1579
+ if (table.dialect === 'mysql') {
1580
+ switch (kind) {
1581
+ case 'primaryKey':
1582
+ return 'PRIMARY';
1583
+ case 'unique':
1584
+ case 'index':
1585
+ return columns[0];
1586
+ case 'reference':
1587
+ return `${table.name}_ibfk_${position + 1}`;
1588
+ case 'check':
1589
+ return `${table.name}_chk_${position + 1}`;
1590
+ }
1591
+ }
1592
+ if (table.dialect === 'sqlserver' || table.dialect === 'sqlite') {
1593
+ // SQL Server invents random names and SQLite names nothing, so there is no name to work out.
1594
+ return undefined;
1595
+ }
1596
+ switch (kind) {
1597
+ case 'primaryKey':
1598
+ return `${table.name}_pkey`;
1599
+ case 'unique':
1600
+ return `${table.name}_${columns.join('_')}_key`;
1601
+ case 'reference':
1602
+ return `${table.name}_${columns.join('_')}_fkey`;
1603
+ case 'check':
1604
+ return columns.length === 1
1605
+ ? `${table.name}_${columns[0]}_check`
1606
+ : `${table.name}_check`;
1607
+ case 'index':
1608
+ return `${table.name}_${columns.join('_')}_idx`;
1609
+ }
1610
+ }
1611
+ function inScope(scope, kind) {
1612
+ switch (scope) {
1613
+ case 'constraint':
1614
+ return kind !== 'index';
1615
+ case 'index':
1616
+ return kind === 'index' || kind === 'unique' || kind === 'primaryKey';
1617
+ case 'foreignKey':
1618
+ return kind === 'reference';
1619
+ case 'check':
1620
+ return kind === 'check';
1621
+ }
1622
+ }
1623
+ /** The distinct columns a check expression mentions, to name it the way PostgreSQL does. */
1624
+ function checkColumns(table, check) {
1625
+ return table.columns
1626
+ .filter((column) => mentions(check.tokens, column.name))
1627
+ .map((column) => column.name);
1628
+ }
1629
+ function findNamed(table, name, scope) {
1630
+ const candidates = [];
1631
+ if (table.primaryKey !== undefined) {
1632
+ candidates.push({
1633
+ object: { kind: 'primaryKey', table, position: 0 },
1634
+ ...(table.primaryKey.name === undefined
1635
+ ? {}
1636
+ : { explicit: table.primaryKey.name }),
1637
+ implied: impliedName(table, 'primaryKey', 0, table.primaryKey.columns),
1638
+ });
1639
+ }
1640
+ table.uniques.forEach((unique, position) => {
1641
+ candidates.push({
1642
+ object: { kind: 'unique', table, position },
1643
+ ...(unique.name === undefined ? {} : { explicit: unique.name }),
1644
+ implied: impliedName(table, 'unique', position, unique.columns),
1645
+ });
1646
+ });
1647
+ table.references.forEach((reference, position) => {
1648
+ candidates.push({
1649
+ object: { kind: 'reference', table, position },
1650
+ ...(reference.name === undefined ? {} : { explicit: reference.name }),
1651
+ implied: impliedName(table, 'reference', position, reference.columns),
1652
+ });
1653
+ });
1654
+ table.checks.forEach((check, position) => {
1655
+ candidates.push({
1656
+ object: { kind: 'check', table, position },
1657
+ ...(check.name === undefined ? {} : { explicit: check.name }),
1658
+ implied: impliedName(table, 'check', position, checkColumns(table, check)),
1659
+ });
1660
+ });
1661
+ table.indexes.forEach((index, position) => {
1662
+ const columns = index.elements
1663
+ .map((element) => element.column ?? '')
1664
+ .filter((column) => column !== '');
1665
+ candidates.push({
1666
+ object: { kind: 'index', table, position },
1667
+ ...(index.name === undefined ? {} : { explicit: index.name }),
1668
+ implied: impliedName(table, 'index', position, columns),
1669
+ });
1670
+ });
1671
+ table.columns.forEach((column, position) => {
1672
+ if (column.defaultName !== undefined) {
1673
+ candidates.push({
1674
+ object: { kind: 'default', table, position },
1675
+ explicit: column.defaultName,
1676
+ });
1677
+ }
1678
+ });
1679
+ const usable = candidates.filter((candidate) => inScope(scope, candidate.object.kind));
1680
+ return (usable.find((candidate) => candidate.explicit !== undefined && sameName(candidate.explicit, name)) ??
1681
+ usable.find((candidate) => candidate.explicit === undefined &&
1682
+ candidate.implied !== undefined &&
1683
+ sameName(candidate.implied, name)))?.object;
1684
+ }
1685
+ function removeNamed(found) {
1686
+ const table = found.table;
1687
+ switch (found.kind) {
1688
+ case 'primaryKey':
1689
+ dropPrimaryKey(table);
1690
+ return;
1691
+ case 'unique':
1692
+ table.uniques.splice(found.position, 1);
1693
+ return;
1694
+ case 'reference':
1695
+ table.references.splice(found.position, 1);
1696
+ return;
1697
+ case 'check':
1698
+ table.checks.splice(found.position, 1);
1699
+ return;
1700
+ case 'index':
1701
+ table.indexes.splice(found.position, 1);
1702
+ return;
1703
+ case 'default': {
1704
+ const column = table.columns[found.position];
1705
+ if (column !== undefined) {
1706
+ delete column.defaultTokens;
1707
+ delete column.defaultText;
1708
+ delete column.defaultName;
1709
+ }
1710
+ return;
1711
+ }
1712
+ }
1713
+ }
1714
+ function nameNamed(found, name) {
1715
+ const table = found.table;
1716
+ switch (found.kind) {
1717
+ case 'primaryKey':
1718
+ if (table.primaryKey !== undefined) {
1719
+ table.primaryKey.name = name;
1720
+ }
1721
+ return;
1722
+ case 'unique': {
1723
+ const unique = table.uniques[found.position];
1724
+ if (unique !== undefined) {
1725
+ unique.name = name;
1726
+ }
1727
+ return;
1728
+ }
1729
+ case 'reference': {
1730
+ const reference = table.references[found.position];
1731
+ if (reference !== undefined) {
1732
+ reference.name = name;
1733
+ }
1734
+ return;
1735
+ }
1736
+ case 'check': {
1737
+ const check = table.checks[found.position];
1738
+ if (check !== undefined) {
1739
+ check.name = name;
1740
+ }
1741
+ return;
1742
+ }
1743
+ case 'index': {
1744
+ const index = table.indexes[found.position];
1745
+ if (index !== undefined) {
1746
+ index.name = name;
1747
+ }
1748
+ return;
1749
+ }
1750
+ case 'default': {
1751
+ const column = table.columns[found.position];
1752
+ if (column !== undefined) {
1753
+ column.defaultName = name;
1754
+ }
1755
+ return;
1756
+ }
1757
+ }
1758
+ }
1759
+ /** Finds an index or constraint by name in one table, or in every table when none is given (names are schema-wide). */
1760
+ function findNamedAnywhere(ctx, table, schema, name, scope) {
1761
+ if (table !== undefined) {
1762
+ return findNamed(table, name, scope);
1763
+ }
1764
+ for (const candidate of ctx.tables) {
1765
+ if (candidate.dropped ||
1766
+ (schema !== undefined &&
1767
+ candidate.schema !== undefined &&
1768
+ !sameName(candidate.schema, schema))) {
1769
+ continue;
1770
+ }
1771
+ const found = findNamed(candidate, name, scope);
1772
+ if (found !== undefined) {
1773
+ return found;
1774
+ }
1775
+ }
1776
+ return undefined;
1777
+ }
1778
+ // Serial types are auto-increment columns; once their default or type is touched they become plain integers.
1779
+ const SERIAL_BASE = {
1780
+ serial: 'integer',
1781
+ serial4: 'integer',
1782
+ smallserial: 'smallint',
1783
+ serial2: 'smallint',
1784
+ bigserial: 'bigint',
1785
+ serial8: 'bigint',
1786
+ };
1787
+ /** Turns a `serial` column into an integer column marked auto-increment. Returns true when it was one. */
1788
+ function detachSerial(column, dialect) {
1789
+ const base = SERIAL_BASE[column.type.name];
1790
+ if (base === undefined || dialect === 'sqlite' || dialect === 'mysql') {
1791
+ return false;
1792
+ }
1793
+ column.autoIncrement = true;
1794
+ column.notNull = true;
1795
+ column.type = { ...column.type, name: base, text: base };
1796
+ return true;
1797
+ }
1798
+ function setDefault(file, column, cursor, dialect) {
1799
+ const wasSerial = detachSerial(column, dialect);
1800
+ const tokens = readDefault(cursor);
1801
+ column.defaultTokens = tokens;
1802
+ column.defaultText = defaultExpression(tokens, file.text);
1803
+ delete column.defaultName;
1804
+ if (wasSerial && !/nextval|unique_rowid/i.test(column.defaultText)) {
1805
+ column.autoIncrement = false;
1806
+ }
1807
+ }
1808
+ /** True for a word that begins an ALTER TABLE action, as opposed to the next column of an `ADD a int, b int` list. */
1809
+ const ALTER_VERBS = new Set([
1810
+ 'ADD',
1811
+ 'DROP',
1812
+ 'ALTER',
1813
+ 'MODIFY',
1814
+ 'CHANGE',
1815
+ 'RENAME',
1816
+ 'SET',
1817
+ 'RESET',
1818
+ 'OWNER',
1819
+ 'ENABLE',
1820
+ 'DISABLE',
1821
+ 'FORCE',
1822
+ 'NO',
1823
+ 'CLUSTER',
1824
+ 'REPLICA',
1825
+ 'ATTACH',
1826
+ 'DETACH',
1827
+ 'VALIDATE',
1828
+ 'INHERIT',
1829
+ 'OF',
1830
+ 'NOT',
1831
+ 'WITH',
1832
+ 'CHECK',
1833
+ 'NOCHECK',
1834
+ 'TRIGGER',
1835
+ 'SWITCH',
1836
+ 'REBUILD',
1837
+ ]);
1283
1838
  function handleAlter(ctx, file, cursor) {
1284
1839
  const line = cursor.peek().line;
1285
1840
  cursor.next();
1286
1841
  if (cursor.isKeyword('TYPE')) {
1287
1842
  cursor.next();
1288
- const name = parseQualifiedName(cursor);
1289
- const target = name === undefined ? undefined : findEnum(ctx, name);
1290
- if (target !== undefined && cursor.acceptKeywords('ADD', 'VALUE')) {
1291
- cursor.acceptKeywords('IF', 'NOT', 'EXISTS');
1292
- const value = cursor.next();
1293
- if (value.kind === 'string' && !target.seen.has(value.value)) {
1294
- let position = target.values.length;
1295
- if (cursor.isKeyword('BEFORE') || cursor.isKeyword('AFTER')) {
1296
- const after = cursor.next().up === 'AFTER';
1297
- const anchor = cursor.next();
1298
- // Looking the anchor up is linear; a huge enum is appended to instead.
1299
- const anchorAt = target.values.length > MAX_POSITIONED_ENUM_VALUES
1300
- ? -1
1301
- : target.values.indexOf(anchor.value);
1302
- if (anchorAt !== -1) {
1303
- position = after ? anchorAt + 1 : anchorAt;
1304
- }
1305
- }
1306
- target.values.splice(position, 0, value.value);
1307
- target.seen.add(value.value);
1308
- }
1309
- }
1843
+ alterType(ctx, file, cursor, line);
1844
+ return;
1845
+ }
1846
+ if (cursor.isKeyword('INDEX')) {
1847
+ cursor.next();
1848
+ alterIndex(ctx, file, cursor, line);
1310
1849
  return;
1311
1850
  }
1312
1851
  if (!cursor.acceptKeyword('TABLE')) {
1313
1852
  // ALTER SEQUENCE, ALTER VIEW, ALTER DATABASE and similar change nothing the model holds.
1314
1853
  return;
1315
1854
  }
1316
- cursor.acceptKeywords('IF', 'EXISTS');
1855
+ const ifExists = cursor.acceptKeywords('IF', 'EXISTS');
1317
1856
  cursor.acceptKeyword('ONLY');
1318
1857
  const name = parseQualifiedName(cursor);
1319
1858
  if (name === undefined) {
@@ -1330,10 +1869,41 @@ function handleAlter(ctx, file, cursor) {
1330
1869
  }
1331
1870
  const table = lookupTable(ctx, name, file.dialect);
1332
1871
  if (table === undefined) {
1333
- warn(ctx, `${file.name}:${line}: ALTER TABLE on the unknown table "${name.name}" was skipped.`);
1872
+ if (!ifExists) {
1873
+ warn(ctx, `${file.name}:${line}: ALTER TABLE on the unknown table "${name.name}" was skipped.`);
1874
+ }
1334
1875
  return;
1335
1876
  }
1877
+ // SQL Server lists items after one verb: ALTER TABLE t ADD a int, b int / DROP COLUMN a, b / DROP CONSTRAINT a, b.
1878
+ let continuation = [];
1336
1879
  for (const action of splitTopLevel(rest)) {
1880
+ const first = action[0];
1881
+ if (file.dialect === 'sqlserver' &&
1882
+ continuation.length > 0 &&
1883
+ first !== undefined &&
1884
+ !(first.kind === 'word' && ALTER_VERBS.has(first.up))) {
1885
+ const prefix = continuation.map((word) => ({
1886
+ kind: 'word',
1887
+ text: word,
1888
+ value: word,
1889
+ up: word,
1890
+ start: first.start,
1891
+ end: first.start,
1892
+ line: first.line,
1893
+ }));
1894
+ handleAlterAction(ctx, file, table, new Cursor([...prefix, ...action]), line);
1895
+ continue;
1896
+ }
1897
+ continuation = [];
1898
+ if (first?.kind === 'word' && first.up === 'ADD') {
1899
+ continuation = ['ADD'];
1900
+ }
1901
+ else if (first?.kind === 'word' &&
1902
+ first.up === 'DROP' &&
1903
+ action[1]?.kind === 'word' &&
1904
+ (action[1].up === 'COLUMN' || action[1].up === 'CONSTRAINT')) {
1905
+ continuation = ['DROP', action[1].up];
1906
+ }
1337
1907
  handleAlterAction(ctx, file, table, new Cursor(action), line);
1338
1908
  }
1339
1909
  }
@@ -1344,7 +1914,6 @@ const QUIET_ALTER_ACTIONS = new Set([
1344
1914
  'FORCE',
1345
1915
  'NO',
1346
1916
  'CLUSTER',
1347
- 'SET',
1348
1917
  'RESET',
1349
1918
  'REPLICA',
1350
1919
  'CHECK',
@@ -1367,70 +1936,554 @@ const QUIET_ALTER_ACTIONS = new Set([
1367
1936
  'NOT',
1368
1937
  'UNION',
1369
1938
  'WITH',
1939
+ 'SWITCH',
1940
+ 'REBUILD',
1941
+ 'DEFAULT',
1370
1942
  ]);
1943
+ /** MySQL: a trailing `FIRST` or `AFTER col` places a column; the tokens without it are returned. */
1944
+ function splitColumnPosition(tokens) {
1945
+ const last = tokens[tokens.length - 1];
1946
+ const beforeLast = tokens[tokens.length - 2];
1947
+ if (last?.kind === 'word' && last.up === 'FIRST') {
1948
+ return { tokens: tokens.slice(0, -1), first: true };
1949
+ }
1950
+ if (beforeLast?.kind === 'word' &&
1951
+ beforeLast.up === 'AFTER' &&
1952
+ (last?.kind === 'word' || last?.kind === 'ident')) {
1953
+ return { tokens: tokens.slice(0, -2), first: false, after: last.value };
1954
+ }
1955
+ return { tokens, first: false };
1956
+ }
1957
+ function placeColumn(table, column, position) {
1958
+ if (!position.first && position.after === undefined) {
1959
+ return;
1960
+ }
1961
+ const rest = table.columns.filter((candidate) => candidate !== column);
1962
+ let at = 0;
1963
+ if (position.after !== undefined) {
1964
+ const anchor = rest.findIndex((candidate) => sameName(candidate.name, position.after ?? ''));
1965
+ at = anchor === -1 ? rest.length : anchor + 1;
1966
+ }
1967
+ rest.splice(at, 0, column);
1968
+ table.columns = rest;
1969
+ }
1371
1970
  function handleAlterAction(ctx, file, table, cursor, line) {
1372
1971
  const head = cursor.peek();
1373
1972
  if (head.kind !== 'word') {
1374
1973
  return;
1375
1974
  }
1376
- if (head.up === 'ADD') {
1377
- cursor.next();
1378
- cursor.acceptKeyword('COLUMN');
1379
- cursor.acceptKeywords('IF', 'NOT', 'EXISTS');
1380
- if (isConstraintStart(cursor, file.dialect)) {
1381
- applyConstraint(ctx, table, parseTableConstraint(ctx, file, cursor, table.name));
1975
+ const statement = (text) => `ALTER TABLE "${table.name}" ${text}`;
1976
+ switch (head.up) {
1977
+ case 'ADD':
1978
+ addToTable(ctx, file, table, cursor, line);
1382
1979
  return;
1383
- }
1384
- if (cursor.isKeyword('GENERATED') || cursor.isPunct('(') || cursor.atEnd) {
1980
+ case 'DROP':
1981
+ dropFromTable(ctx, file, table, cursor, line, statement);
1982
+ return;
1983
+ case 'ALTER':
1984
+ alterColumn(ctx, file, table, cursor, line, statement);
1985
+ return;
1986
+ case 'MODIFY':
1987
+ case 'CHANGE': {
1988
+ cursor.next();
1989
+ cursor.acceptKeyword('COLUMN');
1990
+ cursor.acceptKeywords('IF', 'EXISTS');
1991
+ const from = cursor.next();
1992
+ if (head.up === 'MODIFY') {
1993
+ // The definition starts with the column name, which is the column being changed.
1994
+ cursor.position -= 1;
1995
+ }
1996
+ redefineColumn(ctx, file, table, from.value, head.up === 'CHANGE', cursor, line, statement(`${head.up} COLUMN "${from.value}"`));
1385
1997
  return;
1386
1998
  }
1387
- const column = parseColumn(ctx, file, cursor, table, table.name);
1388
- if (column === undefined) {
1389
- warn(ctx, `${file.name}:${line}: ALTER TABLE "${table.name}" ADD could not be read and was skipped.`);
1999
+ case 'RENAME':
2000
+ renameInTable(ctx, file, table, cursor, line, statement);
2001
+ return;
2002
+ case 'SET':
2003
+ if (cursor.isKeyword('SCHEMA', 1)) {
2004
+ recordSkipped(ctx, file, 'ALTER TABLE ... SET SCHEMA', line, table.name);
2005
+ }
2006
+ return;
2007
+ default:
2008
+ break;
2009
+ }
2010
+ if (QUIET_ALTER_ACTIONS.has(head.up)) {
2011
+ return;
2012
+ }
2013
+ recordSkipped(ctx, file, `ALTER TABLE ... ${head.up}`, line, table.name);
2014
+ }
2015
+ function addToTable(ctx, file, table, cursor, line) {
2016
+ cursor.next();
2017
+ const hasColumnKeyword = cursor.acceptKeyword('COLUMN');
2018
+ const ifNotExists = cursor.acceptKeywords('IF', 'NOT', 'EXISTS');
2019
+ if (cursor.isPunct('(')) {
2020
+ // MySQL: ADD COLUMN (a int, b int)
2021
+ const group = cursor.readGroup() ?? [];
2022
+ for (const element of splitTopLevel(group)) {
2023
+ addElement(ctx, file, table, new Cursor(element), line, ifNotExists);
1390
2024
  }
1391
2025
  return;
1392
2026
  }
1393
- if (head.up === 'ALTER') {
1394
- cursor.next();
2027
+ if (!hasColumnKeyword &&
2028
+ (cursor.isKeyword('PARTITION') ||
2029
+ cursor.isKeyword('PERIOD') ||
2030
+ cursor.isKeyword('SYSTEM') ||
2031
+ cursor.isKeyword('SUPPORT'))) {
2032
+ return;
2033
+ }
2034
+ if (cursor.isKeyword('GENERATED') || cursor.atEnd) {
2035
+ return;
2036
+ }
2037
+ addElement(ctx, file, table, cursor, line, ifNotExists);
2038
+ }
2039
+ function addElement(ctx, file, table, cursor, line, ifNotExists) {
2040
+ if (isConstraintStart(cursor, file.dialect)) {
2041
+ applyConstraint(ctx, table, parseTableConstraint(ctx, file, cursor, table.name));
2042
+ return;
2043
+ }
2044
+ const nameToken = cursor.peek();
2045
+ if (ifNotExists &&
2046
+ (nameToken.kind === 'word' || nameToken.kind === 'ident') &&
2047
+ findColumn(table, nameToken.value) !== undefined) {
2048
+ return;
2049
+ }
2050
+ const remaining = cursor.tokens.slice(cursor.position, cursor.end);
2051
+ const position = splitColumnPosition(remaining);
2052
+ const before = table.columns.length;
2053
+ const column = parseColumn(ctx, file, new Cursor(position.tokens), table, table.name);
2054
+ if (column === undefined) {
2055
+ warn(ctx, `${file.name}:${line}: ALTER TABLE "${table.name}" ADD could not be read and was skipped.`);
2056
+ return;
2057
+ }
2058
+ if (table.columns.length > before) {
2059
+ placeColumn(table, column, position);
2060
+ }
2061
+ }
2062
+ function dropFromTable(ctx, file, table, cursor, line, statement) {
2063
+ cursor.next();
2064
+ const dropNamed = (scope, label) => {
2065
+ const ifExists = cursor.acceptKeywords('IF', 'EXISTS');
2066
+ const nameToken = cursor.next();
2067
+ if (nameToken.kind !== 'word' && nameToken.kind !== 'ident') {
2068
+ return;
2069
+ }
2070
+ const found = findNamed(table, nameToken.value, scope);
2071
+ if (found === undefined) {
2072
+ if (scope === 'index' &&
2073
+ table.implicitIndexes.delete(nameToken.value.toLowerCase())) {
2074
+ return;
2075
+ }
2076
+ if (!ifExists) {
2077
+ warnStatement(ctx, file, line, statement(`DROP ${label} "${nameToken.value}"`), `no ${label.toLowerCase()} of that name exists at this point`);
2078
+ }
2079
+ return;
2080
+ }
2081
+ removeNamed(found);
2082
+ };
2083
+ if (cursor.acceptKeyword('CONSTRAINT')) {
2084
+ dropNamed('constraint', 'CONSTRAINT');
2085
+ }
2086
+ else if (cursor.acceptKeyword('INDEX') || cursor.acceptKeyword('KEY')) {
2087
+ dropNamed('index', 'INDEX');
2088
+ }
2089
+ else if (cursor.acceptKeywords('FOREIGN', 'KEY')) {
2090
+ dropNamed('foreignKey', 'FOREIGN KEY');
2091
+ }
2092
+ else if (cursor.acceptKeywords('PRIMARY', 'KEY')) {
2093
+ if (table.primaryKey === undefined) {
2094
+ warnStatement(ctx, file, line, statement('DROP PRIMARY KEY'), 'the table has no primary key at this point');
2095
+ }
2096
+ else {
2097
+ dropPrimaryKey(table);
2098
+ }
2099
+ }
2100
+ else if (cursor.acceptKeyword('CHECK')) {
2101
+ dropNamed('check', 'CHECK');
2102
+ }
2103
+ else {
2104
+ // DROP [COLUMN] [IF EXISTS] name (MySQL also allows DROP name).
1395
2105
  cursor.acceptKeyword('COLUMN');
1396
- const columnToken = cursor.next();
1397
- const column = findColumn(table, columnToken.value);
2106
+ const ifExists = cursor.acceptKeywords('IF', 'EXISTS');
2107
+ const nameToken = cursor.next();
2108
+ if (nameToken.kind !== 'word' && nameToken.kind !== 'ident') {
2109
+ return;
2110
+ }
2111
+ const column = findColumn(table, nameToken.value);
1398
2112
  if (column === undefined) {
2113
+ if (!ifExists) {
2114
+ warnStatement(ctx, file, line, statement(`DROP COLUMN "${nameToken.value}"`), 'the column does not exist at this point');
2115
+ }
1399
2116
  return;
1400
2117
  }
1401
- if (cursor.acceptKeywords('SET', 'NOT', 'NULL')) {
1402
- column.notNull = true;
2118
+ removeColumn(table, column, inboundReferences(ctx, table));
2119
+ }
2120
+ }
2121
+ /** `ALTER COLUMN c SET <word>` options that change storage or sequences, not the model. */
2122
+ const QUIET_COLUMN_SETTINGS = new Set([
2123
+ 'STATISTICS',
2124
+ 'STORAGE',
2125
+ 'COMPRESSION',
2126
+ 'GENERATED',
2127
+ 'VISIBLE',
2128
+ 'INVISIBLE',
2129
+ 'SEQUENCE',
2130
+ 'START',
2131
+ 'INCREMENT',
2132
+ 'MAXVALUE',
2133
+ 'MINVALUE',
2134
+ 'CACHE',
2135
+ 'CYCLE',
2136
+ 'NO',
2137
+ ]);
2138
+ function alterColumn(ctx, file, table, cursor, line, statement) {
2139
+ cursor.next();
2140
+ if (cursor.acceptKeywords('PRIMARY', 'KEY')) {
2141
+ // CockroachDB: ALTER PRIMARY KEY USING COLUMNS (a, b)
2142
+ if (cursor.acceptKeyword('USING') && cursor.acceptKeyword('COLUMNS')) {
2143
+ const columns = readColumnList(cursor);
2144
+ if (columns.length > 0) {
2145
+ table.primaryKey = {
2146
+ columns,
2147
+ ...(table.primaryKey?.name === undefined
2148
+ ? {}
2149
+ : { name: table.primaryKey.name }),
2150
+ };
2151
+ }
1403
2152
  }
1404
- else if (cursor.acceptKeywords('DROP', 'NOT', 'NULL')) {
1405
- column.notNull = false;
2153
+ return;
2154
+ }
2155
+ cursor.acceptKeyword('COLUMN');
2156
+ const ifExists = cursor.acceptKeywords('IF', 'EXISTS');
2157
+ const columnToken = cursor.next();
2158
+ const column = findColumn(table, columnToken.value);
2159
+ if (column === undefined) {
2160
+ if (!ifExists) {
2161
+ warnStatement(ctx, file, line, statement(`ALTER COLUMN "${columnToken.value}"`), 'the column does not exist at this point');
1406
2162
  }
1407
- else if (cursor.acceptKeywords('SET', 'DEFAULT')) {
1408
- const tokens = readDefault(cursor);
1409
- column.defaultTokens = tokens;
1410
- column.defaultText = defaultExpression(tokens, file.text);
2163
+ return;
2164
+ }
2165
+ const dialect = file.dialect;
2166
+ if (cursor.acceptKeywords('SET', 'NOT', 'NULL')) {
2167
+ column.notNull = true;
2168
+ }
2169
+ else if (cursor.acceptKeywords('DROP', 'NOT', 'NULL')) {
2170
+ column.notNull = false;
2171
+ }
2172
+ else if (cursor.acceptKeywords('SET', 'DEFAULT')) {
2173
+ setDefault(file, column, cursor, dialect);
2174
+ }
2175
+ else if (cursor.acceptKeywords('DROP', 'DEFAULT')) {
2176
+ if (detachSerial(column, dialect)) {
2177
+ column.autoIncrement = false;
1411
2178
  }
1412
- else if (cursor.acceptKeywords('DROP', 'DEFAULT')) {
1413
- delete column.defaultTokens;
1414
- delete column.defaultText;
2179
+ delete column.defaultTokens;
2180
+ delete column.defaultText;
2181
+ delete column.defaultName;
2182
+ }
2183
+ else if (cursor.acceptKeywords('DROP', 'IDENTITY')) {
2184
+ detachSerial(column, dialect);
2185
+ column.autoIncrement = false;
2186
+ // An identity column is NOT NULL, and stays so.
2187
+ column.notNull = true;
2188
+ }
2189
+ else if (cursor.acceptKeywords('DROP', 'EXPRESSION')) {
2190
+ delete column.generated;
2191
+ }
2192
+ else if (cursor.acceptKeyword('ADD') && cursor.isKeyword('GENERATED')) {
2193
+ column.autoIncrement = true;
2194
+ }
2195
+ else if (cursor.isKeyword('TYPE') ||
2196
+ (cursor.isKeyword('SET') &&
2197
+ cursor.isKeyword('DATA', 1) &&
2198
+ cursor.isKeyword('TYPE', 2))) {
2199
+ if (cursor.acceptKeyword('SET')) {
2200
+ cursor.next();
2201
+ }
2202
+ cursor.next();
2203
+ changeType(ctx, file, table, column, cursor, line, statement, false);
2204
+ }
2205
+ else if (cursor.isKeyword('RESTART') ||
2206
+ cursor.isKeyword('RESET') ||
2207
+ (cursor.isKeyword('SET') &&
2208
+ (QUIET_COLUMN_SETTINGS.has(cursor.peek(1).up) ||
2209
+ cursor.peek(1).kind === 'punct'))) {
2210
+ // SET STATISTICS / STORAGE / COMPRESSION / VISIBLE, SET GENERATED, RESTART: no effect on the model.
2211
+ return;
2212
+ }
2213
+ else if (dialect === 'sqlserver' && cursor.peek().kind !== 'punct') {
2214
+ // SQL Server: ALTER COLUMN c varchar(100) NOT NULL
2215
+ changeType(ctx, file, table, column, cursor, line, statement, true);
2216
+ }
2217
+ else {
2218
+ recordSkipped(ctx, file, 'ALTER TABLE ... ALTER COLUMN (type or option change)', line, table.name);
2219
+ }
2220
+ }
2221
+ function changeType(ctx, file, table, column, cursor, line, statement, setsNullability) {
2222
+ const type = parseType(cursor, file.text, file.dialect);
2223
+ if (type.name === '') {
2224
+ warnStatement(ctx, file, line, statement(`ALTER COLUMN "${column.name}" TYPE`), 'the new type could not be read');
2225
+ return;
2226
+ }
2227
+ detachSerial(column, file.dialect);
2228
+ column.type = type;
2229
+ if (setsNullability) {
2230
+ // SQL Server restates the nullability; NULL is the default when it is not written.
2231
+ column.notNull = false;
2232
+ while (!cursor.atEnd) {
2233
+ if (cursor.acceptKeywords('NOT', 'NULL')) {
2234
+ column.notNull = true;
2235
+ }
2236
+ else {
2237
+ cursor.next();
2238
+ }
1415
2239
  }
1416
- else if (cursor.acceptKeyword('ADD') && cursor.isKeyword('GENERATED')) {
1417
- column.autoIncrement = true;
2240
+ }
2241
+ void table;
2242
+ }
2243
+ /** MySQL `MODIFY col definition` and `CHANGE old new definition`: the definition replaces the column. */
2244
+ function redefineColumn(ctx, file, table, oldName, isChange, cursor, line, label) {
2245
+ const existing = findColumn(table, oldName);
2246
+ if (existing === undefined) {
2247
+ warnStatement(ctx, file, line, label, 'the column does not exist at this point');
2248
+ return;
2249
+ }
2250
+ const position = splitColumnPosition(cursor.tokens.slice(cursor.position, cursor.end));
2251
+ const scratch = {
2252
+ ...table,
2253
+ columns: [],
2254
+ columnIndex: new Map(),
2255
+ uniques: [],
2256
+ references: [],
2257
+ indexes: [],
2258
+ checks: [],
2259
+ };
2260
+ delete scratch.primaryKey;
2261
+ const parsed = parseColumn(ctx, file, new Cursor(position.tokens), scratch, table.name);
2262
+ if (parsed === undefined) {
2263
+ warnStatement(ctx, file, line, label, 'the new definition could not be read');
2264
+ return;
2265
+ }
2266
+ if (isChange && !sameName(parsed.name, existing.name)) {
2267
+ if (findColumn(table, parsed.name) !== undefined) {
2268
+ warnStatement(ctx, file, line, label, `a column named "${parsed.name}" exists already`);
2269
+ return;
2270
+ }
2271
+ renameColumn(ctx, table, existing, parsed.name);
2272
+ }
2273
+ const at = table.columns.indexOf(existing);
2274
+ if (at !== -1) {
2275
+ parsed.name = existing.name;
2276
+ table.columns[at] = parsed;
2277
+ rebuildColumnIndex(table);
2278
+ }
2279
+ for (const unique of scratch.uniques) {
2280
+ table.uniques.push(unique);
2281
+ }
2282
+ for (const reference of scratch.references) {
2283
+ table.references.push(reference);
2284
+ }
2285
+ for (const check of scratch.checks) {
2286
+ table.checks.push(check);
2287
+ }
2288
+ if (scratch.primaryKey !== undefined && table.primaryKey === undefined) {
2289
+ table.primaryKey = { columns: [parsed.name] };
2290
+ }
2291
+ placeColumn(table, parsed, position);
2292
+ }
2293
+ function renameInTable(ctx, file, table, cursor, line, statement) {
2294
+ cursor.next();
2295
+ const rename = (scope, label) => {
2296
+ const fromToken = cursor.next();
2297
+ cursor.acceptKeyword('TO');
2298
+ const toToken = cursor.next();
2299
+ if ((fromToken.kind !== 'word' && fromToken.kind !== 'ident') ||
2300
+ (toToken.kind !== 'word' && toToken.kind !== 'ident')) {
2301
+ return;
2302
+ }
2303
+ const text = statement(`RENAME ${label} "${fromToken.value}"`);
2304
+ if (scope === 'column') {
2305
+ const column = findColumn(table, fromToken.value);
2306
+ if (column === undefined) {
2307
+ warnStatement(ctx, file, line, text, 'the column does not exist at this point');
2308
+ }
2309
+ else if (!sameName(fromToken.value, toToken.value) &&
2310
+ findColumn(table, toToken.value) !== undefined) {
2311
+ warnStatement(ctx, file, line, text, `a column named "${toToken.value}" exists already`);
2312
+ }
2313
+ else {
2314
+ renameColumn(ctx, table, column, toToken.value);
2315
+ }
2316
+ return;
2317
+ }
2318
+ const found = findNamed(table, fromToken.value, scope);
2319
+ if (found === undefined) {
2320
+ warnStatement(ctx, file, line, text, `no ${label.toLowerCase()} of that name exists at this point`);
2321
+ return;
2322
+ }
2323
+ nameNamed(found, toToken.value);
2324
+ };
2325
+ if (cursor.acceptKeyword('COLUMN')) {
2326
+ rename('column', 'COLUMN');
2327
+ }
2328
+ else if (cursor.acceptKeyword('CONSTRAINT')) {
2329
+ rename('constraint', 'CONSTRAINT');
2330
+ }
2331
+ else if (cursor.acceptKeyword('INDEX') || cursor.acceptKeyword('KEY')) {
2332
+ rename('index', 'INDEX');
2333
+ }
2334
+ else if (cursor.isKeyword('TO') || cursor.isKeyword('AS')) {
2335
+ cursor.next();
2336
+ renameToName(ctx, file, table, cursor, line);
2337
+ }
2338
+ else if (cursor.isKeyword('TO', 1)) {
2339
+ // PostgreSQL allows RENAME a TO b for a column.
2340
+ rename('column', 'COLUMN');
2341
+ }
2342
+ else {
2343
+ // MySQL: ALTER TABLE t RENAME u
2344
+ renameToName(ctx, file, table, cursor, line);
2345
+ }
2346
+ }
2347
+ function renameToName(ctx, file, table, cursor, line) {
2348
+ const target = parseQualifiedName(cursor);
2349
+ if (target !== undefined) {
2350
+ renameTable(ctx, file, table, target.name, line);
2351
+ }
2352
+ }
2353
+ /** `ALTER TYPE name RENAME TO new`, `RENAME VALUE 'a' TO 'b'` and `ADD VALUE`. */
2354
+ function alterType(ctx, file, cursor, line) {
2355
+ const name = parseQualifiedName(cursor);
2356
+ const target = name === undefined ? undefined : findEnum(ctx, name);
2357
+ if (target === undefined) {
2358
+ return;
2359
+ }
2360
+ if (cursor.acceptKeywords('RENAME', 'VALUE')) {
2361
+ const from = cursor.next();
2362
+ cursor.acceptKeyword('TO');
2363
+ const to = cursor.next();
2364
+ const at = target.values.indexOf(from.value);
2365
+ if (from.kind === 'string' && to.kind === 'string' && at !== -1) {
2366
+ target.values[at] = to.value;
2367
+ target.seen.delete(from.value);
2368
+ target.seen.add(to.value);
2369
+ for (const table of ctx.tables) {
2370
+ for (const column of table.columns) {
2371
+ if (!table.dropped &&
2372
+ sameName(column.type.userName, target.name) &&
2373
+ column.defaultTokens !== undefined) {
2374
+ column.defaultTokens = column.defaultTokens.map((token) => token.kind === 'string' && token.value === from.value
2375
+ ? { ...token, value: to.value }
2376
+ : token);
2377
+ }
2378
+ }
2379
+ }
1418
2380
  }
1419
2381
  else {
1420
- recordSkipped(ctx, file, 'ALTER TABLE ... ALTER COLUMN (type or option change)', line);
2382
+ warnStatement(ctx, file, line, `ALTER TYPE "${target.name}" RENAME VALUE`, 'the value does not exist at this point');
1421
2383
  }
1422
2384
  return;
1423
2385
  }
1424
- if (QUIET_ALTER_ACTIONS.has(head.up)) {
2386
+ if (cursor.acceptKeywords('RENAME', 'TO')) {
2387
+ const to = parseQualifiedName(cursor);
2388
+ if (to === undefined) {
2389
+ return;
2390
+ }
2391
+ const oldName = target.name;
2392
+ ctx.enumsByName.delete(oldName.toLowerCase());
2393
+ target.name = to.name;
2394
+ ctx.enumsByName.set(to.name.toLowerCase(), target);
2395
+ for (const table of ctx.tables) {
2396
+ for (const column of table.columns) {
2397
+ if (!table.dropped && sameName(column.type.userName, oldName)) {
2398
+ column.type = {
2399
+ ...column.type,
2400
+ userName: to.name,
2401
+ name: to.name.toLowerCase(),
2402
+ };
2403
+ }
2404
+ }
2405
+ }
2406
+ return;
2407
+ }
2408
+ if (cursor.acceptKeywords('ADD', 'VALUE')) {
2409
+ cursor.acceptKeywords('IF', 'NOT', 'EXISTS');
2410
+ const value = cursor.next();
2411
+ if (value.kind === 'string' && !target.seen.has(value.value)) {
2412
+ let position = target.values.length;
2413
+ if (cursor.isKeyword('BEFORE') || cursor.isKeyword('AFTER')) {
2414
+ const after = cursor.next().up === 'AFTER';
2415
+ const anchor = cursor.next();
2416
+ // Looking the anchor up is linear; a huge enum is appended to instead.
2417
+ const anchorAt = target.values.length > MAX_POSITIONED_ENUM_VALUES
2418
+ ? -1
2419
+ : target.values.indexOf(anchor.value);
2420
+ if (anchorAt !== -1) {
2421
+ position = after ? anchorAt + 1 : anchorAt;
2422
+ }
2423
+ }
2424
+ target.values.splice(position, 0, value.value);
2425
+ target.seen.add(value.value);
2426
+ }
2427
+ }
2428
+ }
2429
+ /** Reads `[schema.]name` or CockroachDB's `table@name` (the scanner reads `table@name` as one word). */
2430
+ function parseIndexName(cursor) {
2431
+ const parsed = parseQualifiedName(cursor);
2432
+ if (parsed === undefined) {
2433
+ return undefined;
2434
+ }
2435
+ const at = parsed.name.lastIndexOf('@');
2436
+ if (at > 0) {
2437
+ return {
2438
+ name: parsed.name.slice(at + 1),
2439
+ table: parsed.name.slice(0, at),
2440
+ };
2441
+ }
2442
+ return parsed;
2443
+ }
2444
+ function indexOwner(ctx, file, parsed, onTable) {
2445
+ if (parsed.table !== undefined) {
2446
+ return {
2447
+ table: lookupTable(ctx, { name: parsed.table }, file.dialect),
2448
+ hasTable: true,
2449
+ };
2450
+ }
2451
+ if (onTable !== undefined) {
2452
+ return { table: lookupTable(ctx, onTable, file.dialect), hasTable: true };
2453
+ }
2454
+ if (file.dialect === 'sqlserver' && parsed.schema !== undefined) {
2455
+ // The old SQL Server form: DROP INDEX table.index
2456
+ return {
2457
+ table: lookupTable(ctx, { name: parsed.schema }, file.dialect),
2458
+ hasTable: true,
2459
+ };
2460
+ }
2461
+ return { table: undefined, hasTable: false };
2462
+ }
2463
+ function alterIndex(ctx, file, cursor, line) {
2464
+ const ifExists = cursor.acceptKeywords('IF', 'EXISTS');
2465
+ const parsed = parseIndexName(cursor);
2466
+ if (parsed === undefined || !cursor.acceptKeywords('RENAME', 'TO')) {
1425
2467
  return;
1426
2468
  }
1427
- recordSkipped(ctx, file, `ALTER TABLE ... ${head.up}`, line);
2469
+ const to = cursor.next();
2470
+ const owner = indexOwner(ctx, file, parsed, undefined);
2471
+ const found = findNamedAnywhere(ctx, owner.table, parsed.schema, parsed.name, 'index');
2472
+ if (found === undefined) {
2473
+ if (!ifExists) {
2474
+ warnStatement(ctx, file, line, `ALTER INDEX "${parsed.name}" RENAME TO "${to.value}"`, 'no index of that name exists at this point');
2475
+ }
2476
+ return;
2477
+ }
2478
+ nameNamed(found, to.value);
1428
2479
  }
1429
2480
  function handleDrop(ctx, file, cursor) {
2481
+ const line = cursor.peek().line;
1430
2482
  cursor.next();
1431
2483
  const kind = cursor.next();
1432
2484
  if (kind.up === 'TABLE') {
1433
- cursor.acceptKeywords('IF', 'EXISTS');
2485
+ const ifExists = cursor.acceptKeywords('IF', 'EXISTS');
2486
+ const dropped = [];
1434
2487
  for (;;) {
1435
2488
  const name = parseQualifiedName(cursor);
1436
2489
  if (name === undefined) {
@@ -1438,13 +2491,22 @@ function handleDrop(ctx, file, cursor) {
1438
2491
  }
1439
2492
  const table = lookupTable(ctx, name, file.dialect);
1440
2493
  if (table !== undefined) {
2494
+ dropped.push({ table, inbound: inboundReferences(ctx, table) });
1441
2495
  table.dropped = true;
1442
2496
  ctx.tableByKey.delete(tableKey(table.schema, table.name));
1443
2497
  }
2498
+ else if (!ifExists) {
2499
+ warnStatement(ctx, file, line, `DROP TABLE "${name.name}"`, 'the table does not exist at this point');
2500
+ }
1444
2501
  if (!cursor.acceptPunct(',')) {
1445
2502
  break;
1446
2503
  }
1447
2504
  }
2505
+ if (cursor.isKeyword('CASCADE')) {
2506
+ for (const entry of dropped) {
2507
+ removeInbound(entry.inbound, entry.table);
2508
+ }
2509
+ }
1448
2510
  }
1449
2511
  else if (kind.up === 'TYPE') {
1450
2512
  cursor.acceptKeywords('IF', 'EXISTS');
@@ -1454,6 +2516,165 @@ function handleDrop(ctx, file, cursor) {
1454
2516
  target.dropped = true;
1455
2517
  }
1456
2518
  }
2519
+ else if (kind.up === 'INDEX') {
2520
+ dropIndexes(ctx, file, cursor, line);
2521
+ }
2522
+ }
2523
+ /** `DROP INDEX` in the four dialects: `a, b`, `a ON t`, `t.a` and CockroachDB's `t@a`. */
2524
+ function dropIndexes(ctx, file, cursor, line) {
2525
+ cursor.acceptKeyword('CONCURRENTLY');
2526
+ const ifExists = cursor.acceptKeywords('IF', 'EXISTS');
2527
+ for (;;) {
2528
+ const parsed = parseIndexName(cursor);
2529
+ if (parsed === undefined) {
2530
+ break;
2531
+ }
2532
+ let onTable;
2533
+ if (cursor.acceptKeyword('ON')) {
2534
+ onTable = parseQualifiedName(cursor);
2535
+ }
2536
+ const owner = indexOwner(ctx, file, parsed, onTable);
2537
+ const indexName = parsed.name;
2538
+ const found = owner.hasTable && owner.table === undefined
2539
+ ? undefined
2540
+ : findNamedAnywhere(ctx, owner.table, owner.hasTable ? undefined : parsed.schema, indexName, 'index');
2541
+ if (found === undefined) {
2542
+ if (owner.table?.implicitIndexes.delete(indexName.toLowerCase()) === true) {
2543
+ continue;
2544
+ }
2545
+ if (!ifExists) {
2546
+ warnStatement(ctx, file, line, `DROP INDEX "${indexName}"`, 'no index of that name exists at this point');
2547
+ }
2548
+ }
2549
+ else if (found.kind === 'primaryKey') {
2550
+ // MySQL: DROP INDEX `PRIMARY` ON t
2551
+ removeNamed(found);
2552
+ }
2553
+ else {
2554
+ removeNamed(found);
2555
+ }
2556
+ if (!cursor.acceptPunct(',')) {
2557
+ break;
2558
+ }
2559
+ }
2560
+ }
2561
+ /** MySQL `RENAME TABLE a TO b, c TO d`. */
2562
+ function handleRename(ctx, file, cursor) {
2563
+ const line = cursor.peek().line;
2564
+ cursor.next();
2565
+ if (!cursor.acceptKeyword('TABLE')) {
2566
+ return;
2567
+ }
2568
+ cursor.acceptKeywords('IF', 'EXISTS');
2569
+ for (;;) {
2570
+ const from = parseQualifiedName(cursor);
2571
+ if (from === undefined || !cursor.acceptKeyword('TO')) {
2572
+ break;
2573
+ }
2574
+ const to = parseQualifiedName(cursor);
2575
+ const table = lookupTable(ctx, from, file.dialect);
2576
+ if (to !== undefined) {
2577
+ if (table === undefined) {
2578
+ warnStatement(ctx, file, line, `RENAME TABLE "${from.name}" TO "${to.name}"`, 'the table does not exist at this point');
2579
+ }
2580
+ else {
2581
+ renameTable(ctx, file, table, to.name, line);
2582
+ }
2583
+ }
2584
+ if (!cursor.acceptPunct(',')) {
2585
+ break;
2586
+ }
2587
+ }
2588
+ }
2589
+ /** Splits the quoted object name of `sp_rename 'dbo.t.c'` into its parts. */
2590
+ function splitObjectName(value) {
2591
+ return value
2592
+ .split('.')
2593
+ .map((part) => part.replace(/^[[\]"`]+|[[\]"`]+$/g, ''))
2594
+ .filter((part) => part !== '');
2595
+ }
2596
+ /** SQL Server `EXEC sp_rename 'dbo.t.old', 'new', 'COLUMN'` (also INDEX and OBJECT, and the named-parameter form). */
2597
+ function handleSpRename(ctx, file, cursor) {
2598
+ const line = cursor.peek().line;
2599
+ cursor.next();
2600
+ cursor.next();
2601
+ const positional = [];
2602
+ const named = new Map();
2603
+ for (const argument of splitTopLevel(cursor.tokens.slice(cursor.position, cursor.end))) {
2604
+ const parts = new Cursor(argument);
2605
+ const first = parts.peek();
2606
+ if (first.kind === 'word' && first.value.startsWith('@')) {
2607
+ // The named form: @objname = N'dbo.t', @newname = N'u', @objtype = N'COLUMN'
2608
+ parts.next();
2609
+ parts.acceptPunct('=');
2610
+ named.set(first.value.slice(1).toLowerCase(), parts.next().value);
2611
+ }
2612
+ else {
2613
+ positional.push(parts.next().value);
2614
+ }
2615
+ }
2616
+ const objectName = named.get('objname') ?? positional[0] ?? '';
2617
+ const newName = named.get('newname') ?? positional[1] ?? '';
2618
+ const objectType = (named.get('objtype') ??
2619
+ positional[2] ??
2620
+ 'OBJECT').toUpperCase();
2621
+ const parts = splitObjectName(objectName);
2622
+ const statement = `sp_rename '${objectName}', '${newName}'`;
2623
+ if (parts.length === 0 || newName === '') {
2624
+ warnStatement(ctx, file, line, statement, 'the arguments could not be read');
2625
+ return;
2626
+ }
2627
+ const lookup = (tableParts) => {
2628
+ const tableName = tableParts[tableParts.length - 1];
2629
+ if (tableName === undefined) {
2630
+ return undefined;
2631
+ }
2632
+ const schema = tableParts[tableParts.length - 2];
2633
+ return lookupTable(ctx, schema === undefined ? { name: tableName } : { schema, name: tableName }, file.dialect);
2634
+ };
2635
+ if (objectType === 'COLUMN' || objectType === 'INDEX') {
2636
+ const leaf = parts[parts.length - 1] ?? '';
2637
+ const table = lookup(parts.slice(0, -1));
2638
+ if (table === undefined) {
2639
+ warnStatement(ctx, file, line, statement, 'the table does not exist at this point');
2640
+ return;
2641
+ }
2642
+ if (objectType === 'COLUMN') {
2643
+ const column = findColumn(table, leaf);
2644
+ if (column === undefined) {
2645
+ warnStatement(ctx, file, line, statement, 'the column does not exist at this point');
2646
+ }
2647
+ else if (findColumn(table, newName) !== undefined &&
2648
+ !sameName(leaf, newName)) {
2649
+ warnStatement(ctx, file, line, statement, `a column named "${newName}" exists already`);
2650
+ }
2651
+ else {
2652
+ renameColumn(ctx, table, column, newName);
2653
+ }
2654
+ return;
2655
+ }
2656
+ const found = findNamed(table, leaf, 'index');
2657
+ if (found === undefined) {
2658
+ warnStatement(ctx, file, line, statement, 'the index does not exist at this point');
2659
+ }
2660
+ else {
2661
+ nameNamed(found, newName);
2662
+ }
2663
+ return;
2664
+ }
2665
+ const table = lookup(parts);
2666
+ if (table !== undefined) {
2667
+ renameTable(ctx, file, table, newName, line);
2668
+ return;
2669
+ }
2670
+ // A constraint is renamed with the OBJECT type and its schema-qualified name.
2671
+ const constraint = parts[parts.length - 1] ?? '';
2672
+ const found = findNamedAnywhere(ctx, undefined, parts.length > 1 ? parts[parts.length - 2] : undefined, constraint, 'constraint');
2673
+ if (found === undefined) {
2674
+ warnStatement(ctx, file, line, statement, 'no table or constraint of that name exists at this point');
2675
+ return;
2676
+ }
2677
+ nameNamed(found, newName);
1457
2678
  }
1458
2679
  function sanitizeIdentifier(value) {
1459
2680
  let result = value.replace(/[^A-Za-z0-9_]+/g, '_');
@@ -1651,6 +2872,26 @@ function targetKeyColumns(target) {
1651
2872
  }
1652
2873
  return found;
1653
2874
  }
2875
+ /**
2876
+ * True when the table has an index of its own. MySQL builds an index for every foreign key, and a dump lists it,
2877
+ * so a plain index over exactly the columns of a foreign key does not count (the same rule `buildIndexes` uses).
2878
+ */
2879
+ function hasOwnIndexes(table, build) {
2880
+ if (table.dialect !== 'mysql') {
2881
+ return table.indexes.length > 0;
2882
+ }
2883
+ const keyColumns = new Set(build.references.map((reference) => reference.columns
2884
+ .map((column) => column.name.toLowerCase())
2885
+ .join('\u0000')));
2886
+ return table.indexes.some((index) => index.isUnique ||
2887
+ index.kind !== undefined ||
2888
+ index.isPartial ||
2889
+ index.method !== undefined ||
2890
+ index.elements.some((element) => element.column === undefined) ||
2891
+ !keyColumns.has(index.elements
2892
+ .map((element) => (element.column ?? '').toLowerCase())
2893
+ .join('\u0000')));
2894
+ }
1654
2895
  function buildSchema(ctx, tables, options) {
1655
2896
  const warnings = {
1656
2897
  push: (message) => warn(ctx, message),
@@ -1787,7 +3028,7 @@ function buildSchema(ctx, tables, options) {
1787
3028
  key.length !== 2 ||
1788
3029
  build.references.length !== 2 ||
1789
3030
  table.uniques.length > 0 ||
1790
- table.indexes.length > 0 ||
3031
+ hasOwnIndexes(table, build) ||
1791
3032
  table.checks.length > 0 ||
1792
3033
  referencedTables.has(table)) {
1793
3034
  continue;