ormbridge 0.13.0 → 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.
- package/README.md +5 -4
- package/dist/cliRunner.js +18 -0
- package/dist/cliRunner.js.map +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +17 -0
- package/dist/config.js.map +1 -1
- package/dist/emitters/jpa.d.ts +17 -2
- package/dist/emitters/jpa.js +55 -7
- package/dist/emitters/jpa.js.map +1 -1
- package/dist/emitters/jpaKeyClass.d.ts +18 -0
- package/dist/emitters/jpaKeyClass.js +88 -0
- package/dist/emitters/jpaKeyClass.js.map +1 -0
- package/dist/emitters/jpaKeys.d.ts +1 -7
- package/dist/emitters/jpaKeys.js +0 -43
- package/dist/emitters/jpaKeys.js.map +1 -1
- package/dist/emitters/jpaRelations.d.ts +5 -1
- package/dist/emitters/jpaRelations.js +7 -1
- package/dist/emitters/jpaRelations.js.map +1 -1
- package/dist/emitters/jpaRender.d.ts +5 -1
- package/dist/emitters/jpaRender.js +11 -4
- package/dist/emitters/jpaRender.js.map +1 -1
- package/dist/emitters/jpaTypes.d.ts +9 -3
- package/dist/emitters/jpaTypes.js +8 -0
- package/dist/emitters/jpaTypes.js.map +1 -1
- package/dist/formats.d.ts +11 -0
- package/dist/formats.js +13 -3
- package/dist/formats.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/io.js +62 -21
- package/dist/io.js.map +1 -1
- package/dist/parsers/jpaBuild.js +2 -1
- package/dist/parsers/jpaBuild.js.map +1 -1
- package/dist/parsers/jpaClasses.js +7 -1
- package/dist/parsers/jpaClasses.js.map +1 -1
- package/dist/parsers/jpaColumns.d.ts +4 -1
- package/dist/parsers/jpaColumns.js +8 -2
- package/dist/parsers/jpaColumns.js.map +1 -1
- package/dist/parsers/jpaEntities.js +15 -0
- package/dist/parsers/jpaEntities.js.map +1 -1
- package/dist/parsers/jpaModel.d.ts +6 -0
- package/dist/parsers/sqlDdl.js +1306 -65
- package/dist/parsers/sqlDdl.js.map +1 -1
- package/dist/parsers/sqlMigrationOrder.d.ts +8 -0
- package/dist/parsers/sqlMigrationOrder.js +110 -0
- package/dist/parsers/sqlMigrationOrder.js.map +1 -0
- package/dist/parsers/sqlSyntax.d.ts +6 -0
- package/dist/parsers/sqlSyntax.js +15 -2
- package/dist/parsers/sqlSyntax.js.map +1 -1
- package/dist/parsers/zod.d.ts +11 -0
- package/dist/parsers/zod.js +796 -0
- package/dist/parsers/zod.js.map +1 -0
- package/dist/parsers/zodFields.d.ts +73 -0
- package/dist/parsers/zodFields.js +681 -0
- package/dist/parsers/zodFields.js.map +1 -0
- package/dist/parsers/zodSyntax.d.ts +72 -0
- package/dist/parsers/zodSyntax.js +347 -0
- package/dist/parsers/zodSyntax.js.map +1 -0
- package/package.json +1 -1
package/dist/parsers/sqlDdl.js
CHANGED
|
@@ -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, {
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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 (
|
|
1394
|
-
cursor.
|
|
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
|
|
1397
|
-
const
|
|
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
|
-
|
|
1402
|
-
|
|
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
|
-
|
|
1405
|
-
|
|
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
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
1417
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
3031
|
+
hasOwnIndexes(table, build) ||
|
|
1791
3032
|
table.checks.length > 0 ||
|
|
1792
3033
|
referencedTables.has(table)) {
|
|
1793
3034
|
continue;
|