oak-db 3.3.13 → 4.0.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 -1
- package/lib/MySQL/connector.d.ts +9 -1
- package/lib/MySQL/connector.js +33 -14
- package/lib/MySQL/migration.d.ts +10 -0
- package/lib/MySQL/migration.js +649 -0
- package/lib/MySQL/store.d.ts +19 -2
- package/lib/MySQL/store.js +159 -110
- package/lib/MySQL/translator.d.ts +5 -1
- package/lib/MySQL/translator.js +47 -14
- package/lib/PostgreSQL/connector.d.ts +10 -0
- package/lib/PostgreSQL/connector.js +58 -51
- package/lib/PostgreSQL/migration.d.ts +10 -0
- package/lib/PostgreSQL/migration.js +984 -0
- package/lib/PostgreSQL/prepare.d.ts +2 -0
- package/lib/PostgreSQL/prepare.js +69 -0
- package/lib/PostgreSQL/store.d.ts +16 -2
- package/lib/PostgreSQL/store.js +196 -163
- package/lib/PostgreSQL/translator.d.ts +28 -8
- package/lib/PostgreSQL/translator.js +208 -226
- package/lib/index.d.ts +1 -0
- package/lib/migration.d.ts +27 -0
- package/lib/migration.js +1029 -0
- package/lib/sqlTranslator.d.ts +5 -1
- package/lib/sqlTranslator.js +12 -4
- package/lib/types/dbStore.d.ts +8 -15
- package/lib/types/migration.d.ts +251 -0
- package/lib/types/migration.js +2 -0
- package/lib/utils/indexInspection.d.ts +4 -0
- package/lib/utils/indexInspection.js +32 -0
- package/lib/utils/indexName.d.ts +15 -0
- package/lib/utils/indexName.js +76 -0
- package/lib/utils/inspection.d.ts +13 -0
- package/lib/utils/inspection.js +56 -0
- package/package.json +5 -2
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readMySqlSchema = readMySqlSchema;
|
|
4
|
+
exports.inspectMySqlSchema = inspectMySqlSchema;
|
|
5
|
+
exports.buildMySqlMigrationPlan = buildMySqlMigrationPlan;
|
|
6
|
+
const migration_1 = require("../migration");
|
|
7
|
+
const inspection_1 = require("../utils/inspection");
|
|
8
|
+
const indexInspection_1 = require("../utils/indexInspection");
|
|
9
|
+
function parseDefaultValue(value, attr) {
|
|
10
|
+
if (value === null || value === undefined) {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
if (attr.type === 'bool' || attr.type === 'boolean') {
|
|
14
|
+
if (value === '1' || value === 1) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
if (value === '0' || value === 0) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'number') {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'string') {
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
if (trimmed === '') {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
30
|
+
return Number(trimmed);
|
|
31
|
+
}
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === 'boolean') {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
return String(value);
|
|
38
|
+
}
|
|
39
|
+
function mapOnDeleteRule(deleteRule) {
|
|
40
|
+
switch ((deleteRule || '').toUpperCase()) {
|
|
41
|
+
case 'CASCADE':
|
|
42
|
+
return 'cascade';
|
|
43
|
+
case 'SET NULL':
|
|
44
|
+
return 'set null';
|
|
45
|
+
default:
|
|
46
|
+
return 'no action';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function makeWarning(code, message, table, column, sql) {
|
|
50
|
+
return {
|
|
51
|
+
code,
|
|
52
|
+
level: 'warning',
|
|
53
|
+
message,
|
|
54
|
+
table,
|
|
55
|
+
column,
|
|
56
|
+
sql,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function normalizeMySqlRow(row) {
|
|
60
|
+
const normalized = {};
|
|
61
|
+
Object.keys(row).forEach((key) => {
|
|
62
|
+
normalized[key.toLowerCase()] = row[key];
|
|
63
|
+
});
|
|
64
|
+
return normalized;
|
|
65
|
+
}
|
|
66
|
+
function mapMySqlIndexType(indexType) {
|
|
67
|
+
switch (indexType.toLowerCase()) {
|
|
68
|
+
case 'hash':
|
|
69
|
+
return 'hash';
|
|
70
|
+
case 'fulltext':
|
|
71
|
+
return 'fulltext';
|
|
72
|
+
case 'spatial':
|
|
73
|
+
case 'rtree':
|
|
74
|
+
return 'spatial';
|
|
75
|
+
default:
|
|
76
|
+
return 'btree';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function findMatchingMySqlIndexHint(translator, entityName, tableName, actualIndexName, indexes) {
|
|
80
|
+
return (indexes || []).find((candidate) => translator.getLegacyPhysicalIndexNames(entityName, tableName, candidate.name).includes(actualIndexName));
|
|
81
|
+
}
|
|
82
|
+
const mySqlAttributeHintRules = [
|
|
83
|
+
{
|
|
84
|
+
actualTypes: ['bigint'],
|
|
85
|
+
semanticTypes: ['date', 'time', 'datetime', 'money'],
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
actualTypes: ['json'],
|
|
89
|
+
semanticTypes: ['object', 'array'],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
actualTypes: ['tinyint(1)'],
|
|
93
|
+
semanticTypes: ['bool', 'boolean'],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
actualTypes: ['text'],
|
|
97
|
+
semanticTypes: ['image', 'function'],
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
function createMySqlAdapter(translator) {
|
|
101
|
+
const quote = (name) => translator.quoteIdentifier(name);
|
|
102
|
+
const getTableName = (table, tableDef) => tableDef?.storageName || table;
|
|
103
|
+
const getForeignKeyName = (tableName, column) => `${tableName}_${column}_fk`;
|
|
104
|
+
const getOnDeleteSql = (onDelete) => {
|
|
105
|
+
switch (onDelete) {
|
|
106
|
+
case 'cascade':
|
|
107
|
+
return 'CASCADE';
|
|
108
|
+
case 'set null':
|
|
109
|
+
return 'SET NULL';
|
|
110
|
+
default:
|
|
111
|
+
return 'NO ACTION';
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const buildIndexSql = (tableName, table, index) => {
|
|
115
|
+
const physicalName = translator.getPhysicalIndexName(table, tableName, index.name);
|
|
116
|
+
const sqlParts = [`ALTER TABLE ${quote(tableName)} ADD `];
|
|
117
|
+
if (index.config?.unique) {
|
|
118
|
+
sqlParts.push('UNIQUE ');
|
|
119
|
+
}
|
|
120
|
+
else if (index.config?.type === 'fulltext') {
|
|
121
|
+
sqlParts.push('FULLTEXT ');
|
|
122
|
+
}
|
|
123
|
+
else if (index.config?.type === 'spatial') {
|
|
124
|
+
sqlParts.push('SPATIAL ');
|
|
125
|
+
}
|
|
126
|
+
sqlParts.push(`INDEX ${quote(physicalName)} `);
|
|
127
|
+
if (index.config?.type === 'hash') {
|
|
128
|
+
sqlParts.push('USING HASH ');
|
|
129
|
+
}
|
|
130
|
+
sqlParts.push('(');
|
|
131
|
+
sqlParts.push(index.attributes.map(({ name, size, direction }) => {
|
|
132
|
+
let fragment = quote(String(name));
|
|
133
|
+
if (size) {
|
|
134
|
+
fragment += ` (${size})`;
|
|
135
|
+
}
|
|
136
|
+
if (direction) {
|
|
137
|
+
fragment += ` ${direction}`;
|
|
138
|
+
}
|
|
139
|
+
return fragment;
|
|
140
|
+
}).join(', '));
|
|
141
|
+
sqlParts.push(')');
|
|
142
|
+
if (index.config?.parser) {
|
|
143
|
+
sqlParts.push(` WITH PARSER ${index.config.parser}`);
|
|
144
|
+
}
|
|
145
|
+
sqlParts.push(';');
|
|
146
|
+
return sqlParts.join('');
|
|
147
|
+
};
|
|
148
|
+
const getExpectedPhysicalIndexName = (tableName, table, index) => translator.getPhysicalIndexName(table, tableName, index.name);
|
|
149
|
+
return {
|
|
150
|
+
dialect: 'mysql',
|
|
151
|
+
normalizeIdentifier(name) {
|
|
152
|
+
return name.toLowerCase();
|
|
153
|
+
},
|
|
154
|
+
getTableName,
|
|
155
|
+
buildPrepareSql() {
|
|
156
|
+
return [];
|
|
157
|
+
},
|
|
158
|
+
buildNewTablePlan(table, tableDef) {
|
|
159
|
+
const tableName = getTableName(table, tableDef);
|
|
160
|
+
const sql = translator.translateCreateEntity(table, {
|
|
161
|
+
ifExists: 'omit',
|
|
162
|
+
});
|
|
163
|
+
const foreignKeys = this.getForeignKeys(table, tableDef, translator.schema);
|
|
164
|
+
return {
|
|
165
|
+
forwardSql: sql,
|
|
166
|
+
deferredForwardSql: Object.values(foreignKeys).map((foreignKey) => `ALTER TABLE ${quote(tableName)} ADD CONSTRAINT ${quote(foreignKey.name)} FOREIGN KEY (${quote(foreignKey.column)}) REFERENCES ${quote(foreignKey.refTable)} (${quote(foreignKey.refColumn)}) ON DELETE ${getOnDeleteSql(foreignKey.onDelete)};`),
|
|
167
|
+
summary: {
|
|
168
|
+
newTables: 1,
|
|
169
|
+
addedIndexes: (tableDef.indexes || []).length,
|
|
170
|
+
addedForeignKeys: Object.keys(foreignKeys).length,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
buildDropTablePlan(tableName) {
|
|
175
|
+
return {
|
|
176
|
+
backwardSql: [
|
|
177
|
+
`DROP TABLE IF EXISTS ${quote(tableName)};`,
|
|
178
|
+
],
|
|
179
|
+
summary: {
|
|
180
|
+
removedTables: 1,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
buildRenamePlan(tableName, from, to, oldAttr, newAttr) {
|
|
185
|
+
const sql = [
|
|
186
|
+
`ALTER TABLE ${quote(tableName)} RENAME COLUMN ${quote(from)} TO ${quote(to)};`,
|
|
187
|
+
];
|
|
188
|
+
return {
|
|
189
|
+
manualSql: sql,
|
|
190
|
+
warnings: [
|
|
191
|
+
makeWarning('rename-candidate', `字段「${from}」与「${to}」结构一致,推测为重命名,请先确认再执行`, tableName, to, sql),
|
|
192
|
+
],
|
|
193
|
+
renameCandidates: [{
|
|
194
|
+
table: tableName,
|
|
195
|
+
from,
|
|
196
|
+
to,
|
|
197
|
+
reason: '旧字段与新字段结构完全一致',
|
|
198
|
+
sql,
|
|
199
|
+
}],
|
|
200
|
+
summary: {
|
|
201
|
+
manualActions: 1,
|
|
202
|
+
changedColumns: 1,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
buildColumnPlan(tableName, column, oldAttr, newAttr, classification) {
|
|
207
|
+
if (!oldAttr && newAttr) {
|
|
208
|
+
const sql = `ALTER TABLE ${quote(tableName)} ADD COLUMN ${translator.translateColumnDefinition(tableName, column, newAttr)};`;
|
|
209
|
+
if (classification.mode === 'manual') {
|
|
210
|
+
return {
|
|
211
|
+
manualSql: [sql],
|
|
212
|
+
warnings: [
|
|
213
|
+
makeWarning('manual-column-change', classification.reason || `新增字段「${column}」需要人工确认`, tableName, column, [sql]),
|
|
214
|
+
],
|
|
215
|
+
summary: {
|
|
216
|
+
addedColumns: 1,
|
|
217
|
+
manualActions: 1,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
forwardSql: [sql],
|
|
223
|
+
summary: {
|
|
224
|
+
addedColumns: 1,
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
if (oldAttr && !newAttr) {
|
|
229
|
+
return {
|
|
230
|
+
backwardSql: [
|
|
231
|
+
`ALTER TABLE ${quote(tableName)} DROP COLUMN ${quote(column)};`,
|
|
232
|
+
],
|
|
233
|
+
summary: {
|
|
234
|
+
removedColumns: 1,
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
if (classification.mode === 'none') {
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
const sql = `ALTER TABLE ${quote(tableName)} MODIFY COLUMN ${translator.translateColumnDefinition(tableName, column, newAttr)};`;
|
|
242
|
+
if (classification.mode === 'manual') {
|
|
243
|
+
return {
|
|
244
|
+
manualSql: [sql],
|
|
245
|
+
warnings: [
|
|
246
|
+
makeWarning(newAttr?.type === 'enum' || oldAttr?.type === 'enum' ? 'manual-enum-change' : 'manual-column-change', classification.reason || `字段「${column}」变更需要人工确认`, tableName, column, [sql]),
|
|
247
|
+
],
|
|
248
|
+
summary: {
|
|
249
|
+
changedColumns: 1,
|
|
250
|
+
manualActions: 1,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
255
|
+
forwardSql: [sql],
|
|
256
|
+
summary: {
|
|
257
|
+
changedColumns: 1,
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
buildIndexPlan(tableName, table, oldIndex, newIndex, context) {
|
|
262
|
+
const estimatedRows = context.tableStats?.rowCount;
|
|
263
|
+
const isLargeTable = !context.isNewTable
|
|
264
|
+
&& estimatedRows !== undefined
|
|
265
|
+
&& estimatedRows >= context.largeTableRowThreshold;
|
|
266
|
+
const oldPhysicalNames = oldIndex
|
|
267
|
+
? (0, indexInspection_1.getIndexOriginNames)(oldIndex)
|
|
268
|
+
: [];
|
|
269
|
+
const makeLargeTableIndexWarning = (message, sql) => makeWarning('large-table-index-change', `${message}${estimatedRows !== undefined ? `(当前估算行数: ${estimatedRows})` : ''}`, tableName, undefined, sql);
|
|
270
|
+
if (!oldIndex && newIndex) {
|
|
271
|
+
const sql = buildIndexSql(tableName, table, newIndex);
|
|
272
|
+
if (newIndex.config?.unique && !context.isNewTable) {
|
|
273
|
+
return {
|
|
274
|
+
manualSql: [sql],
|
|
275
|
+
warnings: [
|
|
276
|
+
makeWarning('manual-index-change', `索引「${newIndex.name}」是唯一索引,执行前需要确认历史数据无重复`, tableName, undefined, [sql]),
|
|
277
|
+
],
|
|
278
|
+
summary: {
|
|
279
|
+
addedIndexes: 1,
|
|
280
|
+
manualActions: 1,
|
|
281
|
+
},
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (isLargeTable) {
|
|
285
|
+
return {
|
|
286
|
+
manualSql: [sql],
|
|
287
|
+
warnings: [
|
|
288
|
+
makeLargeTableIndexWarning(`MySQL 大表新增索引「${newIndex.name}」已转为人工确认,避免自动 DDL 造成长时间锁表或性能抖动`, [sql]),
|
|
289
|
+
],
|
|
290
|
+
summary: {
|
|
291
|
+
addedIndexes: 1,
|
|
292
|
+
manualActions: 1,
|
|
293
|
+
largeTableIndexes: 1,
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
forwardSql: [sql],
|
|
299
|
+
summary: {
|
|
300
|
+
addedIndexes: 1,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
if (oldIndex && !newIndex) {
|
|
305
|
+
const dropSql = oldPhysicalNames.map((physicalName) => `ALTER TABLE ${quote(tableName)} DROP INDEX ${quote(physicalName)};`);
|
|
306
|
+
if (isLargeTable) {
|
|
307
|
+
return {
|
|
308
|
+
manualSql: dropSql,
|
|
309
|
+
warnings: [
|
|
310
|
+
makeLargeTableIndexWarning(`MySQL 大表删除索引「${oldIndex.name}」已转为人工确认`, dropSql),
|
|
311
|
+
],
|
|
312
|
+
summary: {
|
|
313
|
+
removedIndexes: 1,
|
|
314
|
+
manualActions: 1,
|
|
315
|
+
largeTableIndexes: 1,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
backwardSql: dropSql,
|
|
321
|
+
summary: {
|
|
322
|
+
removedIndexes: 1,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const dropSql = oldPhysicalNames.map((physicalName) => `ALTER TABLE ${quote(tableName)} DROP INDEX ${quote(physicalName)};`);
|
|
327
|
+
const createSql = buildIndexSql(tableName, table, newIndex);
|
|
328
|
+
if (isLargeTable) {
|
|
329
|
+
return {
|
|
330
|
+
manualSql: [...dropSql, createSql],
|
|
331
|
+
warnings: [
|
|
332
|
+
makeLargeTableIndexWarning(`MySQL 大表重建索引「${oldIndex.name}」已转为人工确认`, [...dropSql, createSql]),
|
|
333
|
+
],
|
|
334
|
+
summary: {
|
|
335
|
+
changedIndexes: 1,
|
|
336
|
+
manualActions: 1,
|
|
337
|
+
largeTableIndexes: 1,
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (oldIndex?.config?.unique || newIndex?.config?.unique) {
|
|
342
|
+
return {
|
|
343
|
+
manualSql: [...dropSql, createSql],
|
|
344
|
+
warnings: [
|
|
345
|
+
makeWarning('manual-index-change', `索引「${oldIndex.name}」包含唯一性变化,需要确认历史数据后再重建`, tableName, undefined, [...dropSql, createSql]),
|
|
346
|
+
],
|
|
347
|
+
summary: {
|
|
348
|
+
changedIndexes: 1,
|
|
349
|
+
manualActions: 1,
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
forwardSql: [...dropSql, createSql],
|
|
355
|
+
summary: {
|
|
356
|
+
changedIndexes: 1,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
},
|
|
360
|
+
buildRenameIndexPlan(tableName, table, oldIndex, newIndex) {
|
|
361
|
+
const oldPhysicalNames = (0, indexInspection_1.getIndexOriginNames)(oldIndex);
|
|
362
|
+
const acceptedPhysicalNames = translator.getLegacyPhysicalIndexNames(table, tableName, newIndex.name);
|
|
363
|
+
const newPhysicalName = getExpectedPhysicalIndexName(tableName, table, newIndex);
|
|
364
|
+
if (oldPhysicalNames.some((physicalName) => physicalName === newPhysicalName
|
|
365
|
+
|| acceptedPhysicalNames.includes(physicalName))) {
|
|
366
|
+
return {};
|
|
367
|
+
}
|
|
368
|
+
if (oldPhysicalNames.length !== 1) {
|
|
369
|
+
return {};
|
|
370
|
+
}
|
|
371
|
+
const [oldPhysicalName] = oldPhysicalNames;
|
|
372
|
+
if (oldPhysicalName === newPhysicalName
|
|
373
|
+
|| acceptedPhysicalNames.includes(oldPhysicalName)) {
|
|
374
|
+
return {};
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
forwardSql: [
|
|
378
|
+
`ALTER TABLE ${quote(tableName)} RENAME INDEX ${quote(oldPhysicalName)} TO ${quote(newPhysicalName)};`,
|
|
379
|
+
],
|
|
380
|
+
backwardSql: [
|
|
381
|
+
`ALTER TABLE ${quote(tableName)} RENAME INDEX ${quote(newPhysicalName)} TO ${quote(oldPhysicalName)};`,
|
|
382
|
+
],
|
|
383
|
+
summary: {
|
|
384
|
+
changedIndexes: 1,
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
},
|
|
388
|
+
buildForeignKeyPlan(tableName, oldForeignKey, newForeignKey, isNewTable) {
|
|
389
|
+
if (!oldForeignKey && newForeignKey) {
|
|
390
|
+
const sql = `ALTER TABLE ${quote(tableName)} ADD CONSTRAINT ${quote(newForeignKey.name)} FOREIGN KEY (${quote(newForeignKey.column)}) REFERENCES ${quote(newForeignKey.refTable)} (${quote(newForeignKey.refColumn)}) ON DELETE ${getOnDeleteSql(newForeignKey.onDelete)};`;
|
|
391
|
+
if (isNewTable) {
|
|
392
|
+
return {
|
|
393
|
+
forwardSql: [sql],
|
|
394
|
+
summary: {
|
|
395
|
+
addedForeignKeys: 1,
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
manualSql: [sql],
|
|
401
|
+
warnings: [
|
|
402
|
+
makeWarning('manual-foreign-key-change', `新增外键「${newForeignKey.name}」前需要确认历史数据引用完整`, tableName, newForeignKey.column, [sql]),
|
|
403
|
+
],
|
|
404
|
+
summary: {
|
|
405
|
+
addedForeignKeys: 1,
|
|
406
|
+
manualActions: 1,
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (oldForeignKey && !newForeignKey) {
|
|
411
|
+
return {
|
|
412
|
+
backwardSql: [
|
|
413
|
+
`ALTER TABLE ${quote(tableName)} DROP FOREIGN KEY ${quote(oldForeignKey.name)};`,
|
|
414
|
+
],
|
|
415
|
+
summary: {
|
|
416
|
+
removedForeignKeys: 1,
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const dropSql = `ALTER TABLE ${quote(tableName)} DROP FOREIGN KEY ${quote(oldForeignKey.name)};`;
|
|
421
|
+
const createSql = `ALTER TABLE ${quote(tableName)} ADD CONSTRAINT ${quote(newForeignKey.name)} FOREIGN KEY (${quote(newForeignKey.column)}) REFERENCES ${quote(newForeignKey.refTable)} (${quote(newForeignKey.refColumn)}) ON DELETE ${getOnDeleteSql(newForeignKey.onDelete)};`;
|
|
422
|
+
return {
|
|
423
|
+
manualSql: [dropSql, createSql],
|
|
424
|
+
warnings: [
|
|
425
|
+
makeWarning('manual-foreign-key-change', `外键「${oldForeignKey.name}」发生变化,需要确认数据后再重建`, tableName, newForeignKey.column, [dropSql, createSql]),
|
|
426
|
+
],
|
|
427
|
+
summary: {
|
|
428
|
+
changedForeignKeys: 1,
|
|
429
|
+
manualActions: 1,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
},
|
|
433
|
+
getForeignKeys(table, tableDef, fullSchema) {
|
|
434
|
+
const tableName = getTableName(table, tableDef);
|
|
435
|
+
const foreignKeys = {};
|
|
436
|
+
Object.keys(tableDef.attributes || {}).forEach((column) => {
|
|
437
|
+
const attr = tableDef.attributes[column];
|
|
438
|
+
if (attr.type !== 'ref' || Array.isArray(attr.ref) || !attr.ref) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const refTableDef = fullSchema[attr.ref];
|
|
442
|
+
const refTable = refTableDef ? getTableName(attr.ref, refTableDef) : attr.ref;
|
|
443
|
+
const foreignKey = {
|
|
444
|
+
name: getForeignKeyName(tableName, column),
|
|
445
|
+
column,
|
|
446
|
+
refTable,
|
|
447
|
+
refColumn: 'id',
|
|
448
|
+
onDelete: attr.onRefDelete === 'delete'
|
|
449
|
+
? 'cascade'
|
|
450
|
+
: (attr.onRefDelete === 'setNull' ? 'set null' : 'no action'),
|
|
451
|
+
};
|
|
452
|
+
foreignKeys[foreignKey.name] = foreignKey;
|
|
453
|
+
});
|
|
454
|
+
return foreignKeys;
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
async function readMySqlSchema(connector, translator) {
|
|
459
|
+
const inspection = await inspectMySqlSchema(connector, translator);
|
|
460
|
+
return inspection.schema;
|
|
461
|
+
}
|
|
462
|
+
async function inspectMySqlSchema(connector, translator) {
|
|
463
|
+
const database = connector.configuration.database;
|
|
464
|
+
const result = {};
|
|
465
|
+
const tableStats = {};
|
|
466
|
+
const tableHints = (0, inspection_1.buildTargetTableHintMap)(translator.schema);
|
|
467
|
+
const [tables] = await connector.exec(`
|
|
468
|
+
SELECT table_name, table_rows
|
|
469
|
+
FROM information_schema.tables
|
|
470
|
+
WHERE table_schema = '${database}'
|
|
471
|
+
AND table_type = 'BASE TABLE'
|
|
472
|
+
ORDER BY table_name;
|
|
473
|
+
`);
|
|
474
|
+
for (const rawTableRow of tables) {
|
|
475
|
+
const tableRow = normalizeMySqlRow(rawTableRow);
|
|
476
|
+
const tableName = tableRow.table_name;
|
|
477
|
+
const tableHintEntry = tableHints.get(tableName);
|
|
478
|
+
const tableHint = tableHintEntry?.tableDef;
|
|
479
|
+
const tableKey = tableHintEntry?.key || tableName;
|
|
480
|
+
const [columns] = await connector.exec(`
|
|
481
|
+
SELECT
|
|
482
|
+
column_name,
|
|
483
|
+
column_type,
|
|
484
|
+
is_nullable,
|
|
485
|
+
column_default,
|
|
486
|
+
extra
|
|
487
|
+
FROM information_schema.columns
|
|
488
|
+
WHERE table_schema = '${database}'
|
|
489
|
+
AND table_name = '${tableName}'
|
|
490
|
+
ORDER BY ordinal_position;
|
|
491
|
+
`);
|
|
492
|
+
const attributes = {};
|
|
493
|
+
for (const rawColumn of columns) {
|
|
494
|
+
const column = normalizeMySqlRow(rawColumn);
|
|
495
|
+
const attributeHint = tableHint?.attributes?.[column.column_name];
|
|
496
|
+
let attr = (0, inspection_1.applyInspectionAttributeHint)(translator.reTranslateToAttribute(column.column_type), attributeHint, mySqlAttributeHintRules);
|
|
497
|
+
attr = (0, inspection_1.applyOakManagedAttributeFallback)(attr, column.column_name, attributeHint);
|
|
498
|
+
if (column.column_name === '$$seq$$' || column.extra.toLowerCase().includes('auto_increment')) {
|
|
499
|
+
attr.type = 'sequence';
|
|
500
|
+
attr.sequenceStart = 10000;
|
|
501
|
+
}
|
|
502
|
+
attr.notNull = attr.type === 'sequence' ? false : column.is_nullable === 'NO';
|
|
503
|
+
const defaultValue = parseDefaultValue(column.column_default, attr);
|
|
504
|
+
if (defaultValue !== undefined && attr.type !== 'sequence') {
|
|
505
|
+
attr.default = defaultValue;
|
|
506
|
+
}
|
|
507
|
+
attributes[column.column_name] = attr;
|
|
508
|
+
}
|
|
509
|
+
// inspection 会先尽量恢复字段/索引语义,但只有 Oak 托管表才进入 planner。
|
|
510
|
+
// 这样可以把业务库里的外部表隔离在迁移系统之外,避免误报“待删除表”。
|
|
511
|
+
if (!(0, inspection_1.isOakManagedTable)(attributes, !!tableHint)) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
tableStats[tableName] = {
|
|
515
|
+
rowCount: tableRow.table_rows === null || tableRow.table_rows === undefined
|
|
516
|
+
? undefined
|
|
517
|
+
: Number(tableRow.table_rows),
|
|
518
|
+
approximate: true,
|
|
519
|
+
};
|
|
520
|
+
const [indexes] = await connector.exec(`
|
|
521
|
+
SELECT
|
|
522
|
+
index_name,
|
|
523
|
+
non_unique,
|
|
524
|
+
seq_in_index,
|
|
525
|
+
column_name,
|
|
526
|
+
index_type,
|
|
527
|
+
collation,
|
|
528
|
+
sub_part
|
|
529
|
+
FROM information_schema.statistics
|
|
530
|
+
WHERE table_schema = '${database}'
|
|
531
|
+
AND table_name = '${tableName}'
|
|
532
|
+
ORDER BY index_name, seq_in_index;
|
|
533
|
+
`);
|
|
534
|
+
const indexDict = {};
|
|
535
|
+
for (const rawIndexRow of indexes) {
|
|
536
|
+
const indexRow = normalizeMySqlRow(rawIndexRow);
|
|
537
|
+
if (indexRow.index_name === 'PRIMARY') {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (!indexDict[indexRow.index_name]) {
|
|
541
|
+
indexDict[indexRow.index_name] = [];
|
|
542
|
+
}
|
|
543
|
+
indexDict[indexRow.index_name].push(indexRow);
|
|
544
|
+
}
|
|
545
|
+
const indexList = [];
|
|
546
|
+
Object.keys(indexDict).forEach((indexName) => {
|
|
547
|
+
const group = indexDict[indexName];
|
|
548
|
+
if (group.length === 1 && group[0].non_unique === 0) {
|
|
549
|
+
const columnName = group[0].column_name;
|
|
550
|
+
if (attributes[columnName]) {
|
|
551
|
+
attributes[columnName].unique = true;
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const index = {
|
|
556
|
+
name: indexName,
|
|
557
|
+
attributes: group.map((item) => ({
|
|
558
|
+
name: item.column_name,
|
|
559
|
+
direction: item.collation === 'D' ? 'DESC' : (item.collation === 'A' ? 'ASC' : undefined),
|
|
560
|
+
size: item.sub_part || undefined,
|
|
561
|
+
})),
|
|
562
|
+
};
|
|
563
|
+
const indexType = group[0].index_type.toLowerCase();
|
|
564
|
+
const targetIndexHint = findMatchingMySqlIndexHint(translator, tableKey, tableName, indexName, tableHint?.indexes);
|
|
565
|
+
if (targetIndexHint) {
|
|
566
|
+
// 真实库里可能同时残留 long name / compact name / 服务端截断名。
|
|
567
|
+
// 这里借助目标 schema 的 hint,把它们折叠回同一个逻辑索引来源。
|
|
568
|
+
index.name = targetIndexHint.name;
|
|
569
|
+
}
|
|
570
|
+
if (targetIndexHint && ['fulltext', 'spatial'].includes(indexType)) {
|
|
571
|
+
index.attributes = targetIndexHint.attributes.map((attribute) => ({
|
|
572
|
+
...attribute,
|
|
573
|
+
}));
|
|
574
|
+
}
|
|
575
|
+
if (group[0].non_unique === 0 || indexType !== 'btree') {
|
|
576
|
+
index.config = targetIndexHint && ['fulltext', 'spatial'].includes(indexType)
|
|
577
|
+
? {
|
|
578
|
+
...targetIndexHint.config,
|
|
579
|
+
}
|
|
580
|
+
: {
|
|
581
|
+
unique: group[0].non_unique === 0,
|
|
582
|
+
type: mapMySqlIndexType(indexType),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
index.__originName = indexName;
|
|
586
|
+
const existed = indexList.find((candidate) => candidate.name === index.name
|
|
587
|
+
&& (0, indexInspection_1.areEquivalentInspectedIndexes)(candidate, index));
|
|
588
|
+
if (existed) {
|
|
589
|
+
// 如果同一个逻辑索引在库里留下了多个历史物理名,
|
|
590
|
+
// 合并 origin name,后续 diff 就不会把它误判成重复 drop/create 或 rename。
|
|
591
|
+
(0, indexInspection_1.mergeIndexOriginName)(existed, indexName);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
indexList.push(index);
|
|
595
|
+
});
|
|
596
|
+
// FK 单独读取并回填到 attribute 上,而不是只信 column_type。
|
|
597
|
+
// 对老 MySQL 库来说,char(36) 是否应当当成 ref,要看物理 FK 是否真的存在。
|
|
598
|
+
const [foreignKeys] = await connector.exec(`
|
|
599
|
+
SELECT
|
|
600
|
+
kcu.constraint_name,
|
|
601
|
+
kcu.column_name,
|
|
602
|
+
kcu.referenced_table_name,
|
|
603
|
+
kcu.referenced_column_name,
|
|
604
|
+
rc.delete_rule
|
|
605
|
+
FROM information_schema.key_column_usage kcu
|
|
606
|
+
JOIN information_schema.referential_constraints rc
|
|
607
|
+
ON rc.constraint_schema = kcu.constraint_schema
|
|
608
|
+
AND rc.constraint_name = kcu.constraint_name
|
|
609
|
+
WHERE kcu.table_schema = '${database}'
|
|
610
|
+
AND kcu.table_name = '${tableName}'
|
|
611
|
+
AND kcu.referenced_table_name IS NOT NULL;
|
|
612
|
+
`);
|
|
613
|
+
for (const rawForeignKey of foreignKeys) {
|
|
614
|
+
const foreignKey = normalizeMySqlRow(rawForeignKey);
|
|
615
|
+
const attr = attributes[foreignKey.column_name];
|
|
616
|
+
if (!attr) {
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
attr.type = 'ref';
|
|
620
|
+
attr.ref = foreignKey.referenced_table_name;
|
|
621
|
+
switch (mapOnDeleteRule(foreignKey.delete_rule)) {
|
|
622
|
+
case 'cascade':
|
|
623
|
+
attr.onRefDelete = 'delete';
|
|
624
|
+
break;
|
|
625
|
+
case 'set null':
|
|
626
|
+
attr.onRefDelete = 'setNull';
|
|
627
|
+
break;
|
|
628
|
+
default:
|
|
629
|
+
attr.onRefDelete = 'ignore';
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const inspectedTableDef = {
|
|
633
|
+
...(tableHint || {
|
|
634
|
+
actions: [],
|
|
635
|
+
actionType: 'crud',
|
|
636
|
+
}),
|
|
637
|
+
attributes,
|
|
638
|
+
indexes: indexList.length ? indexList : undefined,
|
|
639
|
+
};
|
|
640
|
+
result[tableKey] = inspectedTableDef;
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
schema: result,
|
|
644
|
+
tableStats,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function buildMySqlMigrationPlan(currentSchema, targetSchema, translator, options) {
|
|
648
|
+
return (0, migration_1.buildMigrationPlan)(currentSchema, targetSchema, createMySqlAdapter(translator), options);
|
|
649
|
+
}
|