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.
@@ -0,0 +1,1029 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeAttribute = normalizeAttribute;
4
+ exports.normalizeIndex = normalizeIndex;
5
+ exports.areAttributesEquivalent = areAttributesEquivalent;
6
+ exports.areIndexesEquivalent = areIndexesEquivalent;
7
+ exports.classifyAttributeChange = classifyAttributeChange;
8
+ exports.buildMigrationPlan = buildMigrationPlan;
9
+ const migrationSqlCategoryOrder = [
10
+ 'prepareSql',
11
+ 'forwardSql',
12
+ 'onlineSql',
13
+ 'manualSql',
14
+ 'backwardSql',
15
+ ];
16
+ function createEmptySummary() {
17
+ return {
18
+ newTables: 0,
19
+ removedTables: 0,
20
+ addedColumns: 0,
21
+ removedColumns: 0,
22
+ changedColumns: 0,
23
+ addedIndexes: 0,
24
+ removedIndexes: 0,
25
+ changedIndexes: 0,
26
+ addedForeignKeys: 0,
27
+ removedForeignKeys: 0,
28
+ changedForeignKeys: 0,
29
+ manualActions: 0,
30
+ onlineActions: 0,
31
+ largeTableIndexes: 0,
32
+ };
33
+ }
34
+ function createEmptySqlByCategory() {
35
+ return {
36
+ prepareSql: [],
37
+ forwardSql: [],
38
+ onlineSql: [],
39
+ manualSql: [],
40
+ backwardSql: [],
41
+ };
42
+ }
43
+ function appendUnique(target, additions) {
44
+ additions.forEach((sql) => {
45
+ if (!target.includes(sql)) {
46
+ target.push(sql);
47
+ }
48
+ });
49
+ }
50
+ function getChangePlanSql(changePlan) {
51
+ return {
52
+ prepareSql: [...(changePlan.prepareSql || [])],
53
+ forwardSql: [
54
+ ...(changePlan.forwardSql || []),
55
+ ...(changePlan.deferredForwardSql || []),
56
+ ],
57
+ onlineSql: [...(changePlan.onlineSql || [])],
58
+ manualSql: [...(changePlan.manualSql || [])],
59
+ backwardSql: [...(changePlan.backwardSql || [])],
60
+ };
61
+ }
62
+ function getSqlCategories(sql) {
63
+ return migrationSqlCategoryOrder.filter((category) => sql[category].length > 0);
64
+ }
65
+ function hasSqlInAnyCategory(sql) {
66
+ return getSqlCategories(sql).length > 0;
67
+ }
68
+ function appendSqlByCategory(target, additions) {
69
+ migrationSqlCategoryOrder.forEach((category) => {
70
+ appendUnique(target[category], additions[category]);
71
+ });
72
+ }
73
+ function createEmptyTableChange(entity, table, lifecycle) {
74
+ return {
75
+ entity,
76
+ table,
77
+ lifecycle,
78
+ summary: createEmptySummary(),
79
+ sql: createEmptySqlByCategory(),
80
+ columns: {
81
+ added: [],
82
+ removed: [],
83
+ changed: [],
84
+ renamed: [],
85
+ },
86
+ indexes: {
87
+ added: [],
88
+ removed: [],
89
+ changed: [],
90
+ renamed: [],
91
+ },
92
+ foreignKeys: {
93
+ added: [],
94
+ removed: [],
95
+ changed: [],
96
+ },
97
+ warnings: [],
98
+ renameCandidates: [],
99
+ };
100
+ }
101
+ function ensureTableChange(plan, tableChanges, entity, table, lifecycle) {
102
+ const existing = tableChanges.get(table);
103
+ if (existing) {
104
+ if (existing.lifecycle === 'alter' && lifecycle !== 'alter') {
105
+ existing.lifecycle = lifecycle;
106
+ }
107
+ return existing;
108
+ }
109
+ const tableChange = createEmptyTableChange(entity, table, lifecycle);
110
+ tableChanges.set(table, tableChange);
111
+ plan.tableChanges.push(tableChange);
112
+ return tableChange;
113
+ }
114
+ function mergeSummary(summary, patch) {
115
+ if (!patch) {
116
+ return;
117
+ }
118
+ Object.keys(patch).forEach((key) => {
119
+ summary[key] += patch[key] || 0;
120
+ });
121
+ }
122
+ function collectMeta(plan, changePlan, tableChange) {
123
+ plan.prepareSql.push(...(changePlan.prepareSql || []));
124
+ plan.onlineSql.push(...(changePlan.onlineSql || []));
125
+ plan.manualSql.push(...(changePlan.manualSql || []));
126
+ plan.warnings.push(...(changePlan.warnings || []));
127
+ plan.renameCandidates.push(...(changePlan.renameCandidates || []));
128
+ mergeSummary(plan.summary, changePlan.summary);
129
+ if (tableChange) {
130
+ appendSqlByCategory(tableChange.sql, getChangePlanSql(changePlan));
131
+ tableChange.warnings.push(...(changePlan.warnings || []));
132
+ tableChange.renameCandidates.push(...(changePlan.renameCandidates || []));
133
+ mergeSummary(tableChange.summary, changePlan.summary);
134
+ }
135
+ }
136
+ function hasTableChange(tableChange) {
137
+ if (Object.values(tableChange.summary).some((value) => value > 0)) {
138
+ return true;
139
+ }
140
+ if (tableChange.warnings.length > 0 || tableChange.renameCandidates.length > 0) {
141
+ return true;
142
+ }
143
+ return migrationSqlCategoryOrder.some((category) => tableChange.sql[category].length > 0);
144
+ }
145
+ function createSqlBuckets() {
146
+ return {
147
+ forwardTables: [],
148
+ forwardColumns: [],
149
+ forwardIndexes: [],
150
+ forwardForeignKeys: [],
151
+ backwardForeignKeys: [],
152
+ backwardIndexes: [],
153
+ backwardColumns: [],
154
+ backwardTables: [],
155
+ };
156
+ }
157
+ function normalizeDefaultValue(value) {
158
+ if (typeof value === 'string') {
159
+ return value.trim();
160
+ }
161
+ return value;
162
+ }
163
+ function resolveRefTarget(ref, schema) {
164
+ if (Array.isArray(ref)) {
165
+ return [...ref].map((item) => schema?.[item]?.storageName || item).sort();
166
+ }
167
+ if (typeof ref === 'string') {
168
+ return schema?.[ref]?.storageName || ref;
169
+ }
170
+ return ref;
171
+ }
172
+ function shouldCompareDatabaseRef(attr, compareForeignKeys = true) {
173
+ return compareForeignKeys
174
+ && attr.type === 'ref'
175
+ && typeof attr.ref === 'string';
176
+ }
177
+ function normalizeIntegerType(width, dialect) {
178
+ if (dialect === 'postgresql') {
179
+ switch (width) {
180
+ case 1:
181
+ case 2:
182
+ return 'smallint';
183
+ case 3:
184
+ case 4:
185
+ case undefined:
186
+ return 'integer';
187
+ default:
188
+ return 'bigint';
189
+ }
190
+ }
191
+ switch (width) {
192
+ case 1:
193
+ return 'tinyint';
194
+ case 2:
195
+ return 'smallint';
196
+ case 3:
197
+ return 'mediumint';
198
+ case 4:
199
+ case undefined:
200
+ return 'integer';
201
+ default:
202
+ return 'bigint';
203
+ }
204
+ }
205
+ function normalizeAttribute(attr, options) {
206
+ // diff 前先把方言差异、历史别名和 Oak 语义压成统一快照;
207
+ // 后面的分类逻辑只比较快照,避免直接比较原始 attr 时被存储细节噪音干扰。
208
+ let type = typeof attr.sequenceStart === 'number' ? 'sequence' : String(attr.type);
209
+ const enumeration = attr.enumeration ? [...attr.enumeration] : undefined;
210
+ const ref = shouldCompareDatabaseRef(attr, options?.compareForeignKeys)
211
+ ? resolveRefTarget(attr.ref, options?.schema)
212
+ : undefined;
213
+ let params = type === 'ref'
214
+ ? undefined
215
+ : (attr.params ? { ...attr.params } : undefined);
216
+ if (type === 'ref' && options?.compareForeignKeys === false) {
217
+ // 老 MySQL 库里可能只有 char(36) 列而没有物理 FK。
218
+ // 关闭 FK 对比时,把 ref 退化成列类型比较,先让旧库的 upgrade 计划收敛。
219
+ type = 'char';
220
+ params = {
221
+ length: 36,
222
+ };
223
+ }
224
+ if (type === 'int') {
225
+ type = normalizeIntegerType(params?.width, options?.dialect);
226
+ params = undefined;
227
+ }
228
+ else if (type === 'bool') {
229
+ type = 'boolean';
230
+ }
231
+ const isIdColumn = options?.column === 'id';
232
+ const isSequenceColumn = type === 'sequence';
233
+ return {
234
+ type,
235
+ params,
236
+ enumeration,
237
+ ref,
238
+ onRefDelete: ref ? (attr.onRefDelete || 'ignore') : undefined,
239
+ default: normalizeDefaultValue(attr.default),
240
+ notNull: isSequenceColumn ? false : (isIdColumn ? true : (!!attr.notNull || type === 'geometry')),
241
+ unique: isSequenceColumn ? false : !!attr.unique,
242
+ sequence: isSequenceColumn,
243
+ };
244
+ }
245
+ function normalizeTsConfig(tsConfig) {
246
+ if (Array.isArray(tsConfig)) {
247
+ return [...tsConfig].sort();
248
+ }
249
+ return tsConfig;
250
+ }
251
+ function normalizeIndexDirection(direction) {
252
+ return direction === 'ASC' ? undefined : direction;
253
+ }
254
+ function normalizeIndex(index) {
255
+ return {
256
+ name: index.name,
257
+ attributes: index.attributes.map(({ name, direction, size }) => ({
258
+ name: String(name),
259
+ direction: normalizeIndexDirection(direction),
260
+ size,
261
+ })),
262
+ config: {
263
+ unique: !!index.config?.unique,
264
+ type: index.config?.type || 'btree',
265
+ parser: index.config?.parser,
266
+ tsConfig: normalizeTsConfig(index.config?.tsConfig),
267
+ chineseParser: index.config?.chineseParser,
268
+ },
269
+ };
270
+ }
271
+ function recordAddedColumn(tableChange, name, attr, sql, options) {
272
+ tableChange.columns.added.push({
273
+ name,
274
+ categories: getSqlCategories(sql),
275
+ reason: options.reason,
276
+ definition: normalizeAttribute(attr, {
277
+ schema: options.schema,
278
+ column: name,
279
+ dialect: options.dialect,
280
+ compareForeignKeys: options.compareForeignKeys,
281
+ }),
282
+ sql,
283
+ });
284
+ }
285
+ function recordRemovedColumn(tableChange, name, attr, sql, options) {
286
+ tableChange.columns.removed.push({
287
+ name,
288
+ categories: getSqlCategories(sql),
289
+ definition: normalizeAttribute(attr, {
290
+ schema: options.schema,
291
+ column: name,
292
+ dialect: options.dialect,
293
+ compareForeignKeys: options.compareForeignKeys,
294
+ }),
295
+ sql,
296
+ });
297
+ }
298
+ function recordChangedColumn(tableChange, name, oldAttr, newAttr, sql, options) {
299
+ tableChange.columns.changed.push({
300
+ name,
301
+ categories: getSqlCategories(sql),
302
+ reason: options.reason,
303
+ before: normalizeAttribute(oldAttr, {
304
+ schema: options.oldSchema,
305
+ column: name,
306
+ dialect: options.dialect,
307
+ compareForeignKeys: options.compareForeignKeys,
308
+ }),
309
+ after: normalizeAttribute(newAttr, {
310
+ schema: options.newSchema,
311
+ column: name,
312
+ dialect: options.dialect,
313
+ compareForeignKeys: options.compareForeignKeys,
314
+ }),
315
+ sql,
316
+ });
317
+ }
318
+ function recordRenamedColumn(tableChange, from, to, oldAttr, newAttr, sql, options) {
319
+ tableChange.columns.renamed.push({
320
+ from,
321
+ to,
322
+ categories: getSqlCategories(sql),
323
+ reason: options.reason,
324
+ before: normalizeAttribute(oldAttr, {
325
+ schema: options.oldSchema,
326
+ column: from,
327
+ dialect: options.dialect,
328
+ compareForeignKeys: options.compareForeignKeys,
329
+ }),
330
+ after: normalizeAttribute(newAttr, {
331
+ schema: options.newSchema,
332
+ column: to,
333
+ dialect: options.dialect,
334
+ compareForeignKeys: options.compareForeignKeys,
335
+ }),
336
+ sql,
337
+ });
338
+ }
339
+ function recordAddedIndex(tableChange, index, sql) {
340
+ tableChange.indexes.added.push({
341
+ name: index.name,
342
+ categories: getSqlCategories(sql),
343
+ definition: normalizeIndex(index),
344
+ sql,
345
+ });
346
+ }
347
+ function recordRemovedIndex(tableChange, index, sql) {
348
+ tableChange.indexes.removed.push({
349
+ name: index.name,
350
+ categories: getSqlCategories(sql),
351
+ definition: normalizeIndex(index),
352
+ sql,
353
+ });
354
+ }
355
+ function recordChangedIndex(tableChange, oldIndex, newIndex, sql) {
356
+ tableChange.indexes.changed.push({
357
+ name: newIndex.name,
358
+ categories: getSqlCategories(sql),
359
+ before: normalizeIndex(oldIndex),
360
+ after: normalizeIndex(newIndex),
361
+ sql,
362
+ });
363
+ }
364
+ function recordRenamedIndex(tableChange, oldIndex, newIndex, sql) {
365
+ tableChange.indexes.renamed.push({
366
+ from: oldIndex.name,
367
+ to: newIndex.name,
368
+ categories: getSqlCategories(sql),
369
+ before: normalizeIndex(oldIndex),
370
+ after: normalizeIndex(newIndex),
371
+ sql,
372
+ });
373
+ }
374
+ function recordAddedForeignKey(tableChange, foreignKey, sql) {
375
+ tableChange.foreignKeys.added.push({
376
+ name: foreignKey.name,
377
+ categories: getSqlCategories(sql),
378
+ definition: {
379
+ ...foreignKey,
380
+ },
381
+ sql,
382
+ });
383
+ }
384
+ function recordRemovedForeignKey(tableChange, foreignKey, sql) {
385
+ tableChange.foreignKeys.removed.push({
386
+ name: foreignKey.name,
387
+ categories: getSqlCategories(sql),
388
+ definition: {
389
+ ...foreignKey,
390
+ },
391
+ sql,
392
+ });
393
+ }
394
+ function recordChangedForeignKey(tableChange, oldForeignKey, newForeignKey, sql) {
395
+ tableChange.foreignKeys.changed.push({
396
+ name: newForeignKey?.name || oldForeignKey?.name || 'unknown',
397
+ categories: getSqlCategories(sql),
398
+ before: oldForeignKey ? {
399
+ ...oldForeignKey,
400
+ } : undefined,
401
+ after: newForeignKey ? {
402
+ ...newForeignKey,
403
+ } : undefined,
404
+ sql,
405
+ });
406
+ }
407
+ function isObjectEqual(a, b) {
408
+ return JSON.stringify(a) === JSON.stringify(b);
409
+ }
410
+ function areAttributesEquivalent(oldAttr, newAttr, options) {
411
+ return isObjectEqual(normalizeAttribute(oldAttr, {
412
+ schema: options?.oldSchema,
413
+ column: options?.oldColumn || options?.column,
414
+ dialect: options?.dialect,
415
+ compareForeignKeys: options?.compareForeignKeys,
416
+ }), normalizeAttribute(newAttr, {
417
+ schema: options?.newSchema,
418
+ column: options?.newColumn || options?.column,
419
+ dialect: options?.dialect,
420
+ compareForeignKeys: options?.compareForeignKeys,
421
+ }));
422
+ }
423
+ function areIndexesEquivalent(oldIndex, newIndex) {
424
+ const oldSnapshot = normalizeIndex(oldIndex);
425
+ const newSnapshot = normalizeIndex(newIndex);
426
+ return isObjectEqual({
427
+ ...oldSnapshot,
428
+ name: undefined,
429
+ }, {
430
+ ...newSnapshot,
431
+ name: undefined,
432
+ });
433
+ }
434
+ function getTypeFamily(type) {
435
+ const lowerType = type.toLowerCase();
436
+ if (['char', 'varchar', 'text'].includes(lowerType)) {
437
+ return 'text';
438
+ }
439
+ if (['decimal', 'numeric', 'float', 'double', 'real'].includes(lowerType)) {
440
+ return 'decimal';
441
+ }
442
+ if (['int', 'integer', 'smallint', 'bigint', 'tinyint', 'mediumint'].includes(lowerType)) {
443
+ return 'integer';
444
+ }
445
+ return lowerType;
446
+ }
447
+ function getIntegerRank(type) {
448
+ const rank = {
449
+ tinyint: 1,
450
+ smallint: 2,
451
+ mediumint: 3,
452
+ int: 4,
453
+ integer: 4,
454
+ bigint: 5,
455
+ };
456
+ return rank[type.toLowerCase()] || 0;
457
+ }
458
+ function isSameEnumerationPrefix(oldValues, newValues) {
459
+ if (newValues.length < oldValues.length) {
460
+ return false;
461
+ }
462
+ return oldValues.every((value, index) => newValues[index] === value);
463
+ }
464
+ function isTypeChangeSafe(oldAttr, newAttr) {
465
+ if (oldAttr.type === newAttr.type) {
466
+ switch (getTypeFamily(oldAttr.type)) {
467
+ case 'text': {
468
+ const oldLength = oldAttr.params?.length || 0;
469
+ const newLength = newAttr.params?.length || 0;
470
+ if (newAttr.type === 'text') {
471
+ return true;
472
+ }
473
+ if (!oldLength || !newLength) {
474
+ return true;
475
+ }
476
+ return newLength >= oldLength;
477
+ }
478
+ case 'decimal': {
479
+ const oldPrecision = oldAttr.params?.precision || 0;
480
+ const oldScale = oldAttr.params?.scale || 0;
481
+ const newPrecision = newAttr.params?.precision || 0;
482
+ const newScale = newAttr.params?.scale || 0;
483
+ return newPrecision >= oldPrecision && newScale >= oldScale;
484
+ }
485
+ case 'integer': {
486
+ return getIntegerRank(newAttr.type) >= getIntegerRank(oldAttr.type);
487
+ }
488
+ default: {
489
+ return false;
490
+ }
491
+ }
492
+ }
493
+ if (getTypeFamily(oldAttr.type) !== getTypeFamily(newAttr.type)) {
494
+ return false;
495
+ }
496
+ if (getTypeFamily(oldAttr.type) === 'text' && newAttr.type === 'text') {
497
+ return true;
498
+ }
499
+ if (getTypeFamily(oldAttr.type) === 'integer') {
500
+ return getIntegerRank(newAttr.type) >= getIntegerRank(oldAttr.type);
501
+ }
502
+ return false;
503
+ }
504
+ function classifyAttributeChange(oldAttr, newAttr, options) {
505
+ if (!oldAttr && !newAttr) {
506
+ return {
507
+ mode: 'none',
508
+ };
509
+ }
510
+ if (!oldAttr && newAttr) {
511
+ const normalized = normalizeAttribute(newAttr, {
512
+ dialect: options?.dialect,
513
+ column: options?.column,
514
+ schema: options?.newSchema,
515
+ compareForeignKeys: options?.compareForeignKeys,
516
+ });
517
+ if (normalized.notNull && normalized.default === undefined && !normalized.sequence) {
518
+ return {
519
+ mode: 'manual',
520
+ reason: '新字段为非空且没有默认值,需要先补齐历史数据',
521
+ };
522
+ }
523
+ return {
524
+ mode: 'forward',
525
+ };
526
+ }
527
+ if (oldAttr && !newAttr) {
528
+ return {
529
+ mode: 'forward',
530
+ };
531
+ }
532
+ if (areAttributesEquivalent(oldAttr, newAttr, options)) {
533
+ return {
534
+ mode: 'none',
535
+ };
536
+ }
537
+ const oldSnapshot = normalizeAttribute(oldAttr, {
538
+ schema: options?.oldSchema,
539
+ column: options?.column,
540
+ dialect: options?.dialect,
541
+ compareForeignKeys: options?.compareForeignKeys,
542
+ });
543
+ const newSnapshot = normalizeAttribute(newAttr, {
544
+ schema: options?.newSchema,
545
+ column: options?.column,
546
+ dialect: options?.dialect,
547
+ compareForeignKeys: options?.compareForeignKeys,
548
+ });
549
+ if (!isObjectEqual(oldSnapshot.ref, newSnapshot.ref)
550
+ || oldSnapshot.onRefDelete !== newSnapshot.onRefDelete) {
551
+ return {
552
+ mode: 'manual',
553
+ reason: '外键引用目标或删除规则发生变化,需要确认现有数据兼容',
554
+ };
555
+ }
556
+ if (oldSnapshot.unique !== newSnapshot.unique) {
557
+ return {
558
+ mode: 'manual',
559
+ reason: '唯一约束变化需要先确认历史数据是否满足约束',
560
+ };
561
+ }
562
+ if (oldSnapshot.type === 'enum' || newSnapshot.type === 'enum') {
563
+ if (oldSnapshot.type === 'enum'
564
+ && newSnapshot.type === 'enum'
565
+ && oldSnapshot.enumeration
566
+ && newSnapshot.enumeration
567
+ && isSameEnumerationPrefix(oldSnapshot.enumeration, newSnapshot.enumeration)) {
568
+ return {
569
+ mode: 'forward',
570
+ enumAppendOnly: true,
571
+ };
572
+ }
573
+ return {
574
+ mode: 'manual',
575
+ reason: '枚举值集合变化需要确认现有数据映射关系',
576
+ };
577
+ }
578
+ const typeChanged = oldSnapshot.type !== newSnapshot.type
579
+ || !isObjectEqual(oldSnapshot.params, newSnapshot.params);
580
+ if (typeChanged) {
581
+ if (!isTypeChangeSafe(oldSnapshot, newSnapshot)) {
582
+ return {
583
+ mode: 'manual',
584
+ reason: '字段类型变化可能需要显式转换或人工清洗数据',
585
+ };
586
+ }
587
+ }
588
+ if (oldSnapshot.notNull !== newSnapshot.notNull) {
589
+ if (!oldSnapshot.notNull && newSnapshot.notNull) {
590
+ return {
591
+ mode: 'manual',
592
+ reason: '字段要从可空改为非空,需要先补齐空值',
593
+ };
594
+ }
595
+ }
596
+ return {
597
+ mode: 'forward',
598
+ };
599
+ }
600
+ function buildTableMap(schema, adapter) {
601
+ const tableMap = new Map();
602
+ Object.keys(schema).forEach((table) => {
603
+ const tableDef = schema[table];
604
+ const actualName = adapter.getTableName(table, tableDef);
605
+ tableMap.set(adapter.normalizeIdentifier(actualName), {
606
+ key: table,
607
+ actualName,
608
+ tableDef,
609
+ });
610
+ });
611
+ return tableMap;
612
+ }
613
+ function pickRenameCandidates(tableName, oldAttrs, newAttrs, oldOnly, newOnly, oldSchema, newSchema, dialect, compareForeignKeys) {
614
+ const renameCandidates = [];
615
+ const pendingNew = [...newOnly];
616
+ for (const oldColumn of oldOnly) {
617
+ const matchIndex = pendingNew.findIndex((newColumn) => areAttributesEquivalent(oldAttrs[oldColumn], newAttrs[newColumn], {
618
+ oldSchema,
619
+ newSchema,
620
+ oldColumn,
621
+ newColumn,
622
+ dialect,
623
+ compareForeignKeys,
624
+ }));
625
+ if (matchIndex >= 0) {
626
+ const [newColumn] = pendingNew.splice(matchIndex, 1);
627
+ renameCandidates.push({
628
+ from: oldColumn,
629
+ to: newColumn,
630
+ });
631
+ }
632
+ }
633
+ const oldRemain = oldOnly.filter((column) => !renameCandidates.find((candidate) => candidate.from === column));
634
+ const newRemain = newOnly.filter((column) => !renameCandidates.find((candidate) => candidate.to === column));
635
+ return {
636
+ renameCandidates,
637
+ oldRemain,
638
+ newRemain,
639
+ };
640
+ }
641
+ function cloneIndex(index) {
642
+ return {
643
+ ...index,
644
+ attributes: index.attributes.map((attr) => ({
645
+ ...attr,
646
+ })),
647
+ config: index.config ? {
648
+ ...index.config,
649
+ } : undefined,
650
+ __originNames: index.__originNames ? [...index.__originNames] : undefined,
651
+ };
652
+ }
653
+ function buildIndexList(indexes) {
654
+ return (indexes || []).map(cloneIndex);
655
+ }
656
+ function buildIndexNameVariants(name, options) {
657
+ const variants = new Set([name]);
658
+ const pending = [name];
659
+ const prefixes = [options?.currentActualName, options?.targetKey].filter(Boolean);
660
+ while (pending.length > 0) {
661
+ const current = pending.shift();
662
+ prefixes.forEach((prefix) => {
663
+ if (current.startsWith(`${prefix}_`)) {
664
+ const stripped = current.slice(prefix.length + 1);
665
+ if (!variants.has(stripped)) {
666
+ variants.add(stripped);
667
+ pending.push(stripped);
668
+ }
669
+ }
670
+ });
671
+ }
672
+ return variants;
673
+ }
674
+ function areIndexNamesCompatible(oldIndex, newIndex, oldOptions, newOptions) {
675
+ if (oldIndex.name === newIndex.name) {
676
+ return true;
677
+ }
678
+ const newVariants = buildIndexNameVariants(newIndex.name, newOptions);
679
+ for (const variant of buildIndexNameVariants(oldIndex.name, oldOptions)) {
680
+ if (newVariants.has(variant)) {
681
+ return true;
682
+ }
683
+ }
684
+ return false;
685
+ }
686
+ function findCompatibleIndexMatch(oldIndex, newIndexes, matchedNewIndexes, oldOptions, newOptions) {
687
+ const aliasCandidates = [];
688
+ for (let i = 0; i < newIndexes.length; i++) {
689
+ if (matchedNewIndexes.has(i)) {
690
+ continue;
691
+ }
692
+ const newIndex = newIndexes[i];
693
+ if (oldIndex.name === newIndex.name) {
694
+ return i;
695
+ }
696
+ if (areIndexNamesCompatible(oldIndex, newIndex, oldOptions, newOptions)) {
697
+ aliasCandidates.push(i);
698
+ }
699
+ }
700
+ if (aliasCandidates.length === 1) {
701
+ return aliasCandidates[0];
702
+ }
703
+ return undefined;
704
+ }
705
+ function pickEquivalentIndexRenameCandidates(oldIndexes, newIndexes) {
706
+ const renameCandidates = [];
707
+ const pendingNew = [...newIndexes];
708
+ for (const oldIndex of oldIndexes) {
709
+ const matchIndex = pendingNew.findIndex((newIndex) => areIndexesEquivalent(oldIndex, newIndex));
710
+ if (matchIndex >= 0) {
711
+ const [newIndex] = pendingNew.splice(matchIndex, 1);
712
+ renameCandidates.push({
713
+ oldIndex,
714
+ newIndex,
715
+ });
716
+ }
717
+ }
718
+ const oldRemain = oldIndexes.filter((index) => !renameCandidates.find((candidate) => candidate.oldIndex === index));
719
+ const newRemain = newIndexes.filter((index) => !renameCandidates.find((candidate) => candidate.newIndex === index));
720
+ return {
721
+ renameCandidates,
722
+ oldRemain,
723
+ newRemain,
724
+ };
725
+ }
726
+ function getForeignKeyMap(table, tableDef, fullSchema, adapter) {
727
+ return adapter.getForeignKeys(table, tableDef, fullSchema);
728
+ }
729
+ function hasExplicitIndexOrigin(index) {
730
+ return !!index.__originName || !!index.__originNames?.length;
731
+ }
732
+ function buildMigrationPlan(currentSchema, targetSchema, adapter, options = {}) {
733
+ // planner 的核心算法分两步:
734
+ // 1. 先按“建表 / 改表 / 删表”收集变更语义;
735
+ // 2. 再把 SQL 按依赖关系重新分桶,避免 schema 遍历顺序影响最终执行顺序。
736
+ const largeTableRowThreshold = options.largeTableRowThreshold || 100000;
737
+ const compareForeignKeys = options.compareForeignKeys !== false;
738
+ const buckets = createSqlBuckets();
739
+ const plan = {
740
+ currentSchema,
741
+ targetSchema,
742
+ prepareSql: [],
743
+ forwardSql: [],
744
+ onlineSql: [],
745
+ manualSql: [],
746
+ backwardSql: [],
747
+ warnings: [],
748
+ renameCandidates: [],
749
+ summary: createEmptySummary(),
750
+ tableChanges: [],
751
+ };
752
+ const tableChanges = new Map();
753
+ const currentTables = buildTableMap(currentSchema, adapter);
754
+ const matchedCurrentTables = new Set();
755
+ plan.prepareSql.push(...adapter.buildPrepareSql(currentSchema, targetSchema));
756
+ Object.keys(targetSchema).forEach((table) => {
757
+ const targetDef = targetSchema[table];
758
+ const tableName = adapter.getTableName(table, targetDef);
759
+ const normalized = adapter.normalizeIdentifier(tableName);
760
+ const currentEntry = currentTables.get(normalized);
761
+ if (!currentEntry) {
762
+ // 新表先记录完整语义;真正的 FK SQL 会延后到 forwardForeignKeys 桶,
763
+ // 这样可以保证它一定落在所有 CREATE TABLE 之后。
764
+ const tableChange = ensureTableChange(plan, tableChanges, table, tableName, 'create');
765
+ const changePlan = adapter.buildNewTablePlan(table, targetDef);
766
+ const changePlanSql = getChangePlanSql(changePlan);
767
+ collectMeta(plan, changePlan, tableChange);
768
+ Object.entries(targetDef.attributes || {}).forEach(([column, attr]) => {
769
+ recordAddedColumn(tableChange, column, attr, changePlanSql, {
770
+ schema: targetSchema,
771
+ dialect: adapter.dialect,
772
+ compareForeignKeys,
773
+ });
774
+ });
775
+ (targetDef.indexes || []).forEach((index) => {
776
+ recordAddedIndex(tableChange, index, changePlanSql);
777
+ });
778
+ Object.values(getForeignKeyMap(table, targetDef, targetSchema, adapter)).forEach((foreignKey) => {
779
+ recordAddedForeignKey(tableChange, foreignKey, changePlanSql);
780
+ });
781
+ buckets.forwardTables.push(...(changePlan.forwardSql || []));
782
+ buckets.forwardForeignKeys.push(...(changePlan.deferredForwardSql || []));
783
+ return;
784
+ }
785
+ matchedCurrentTables.add(normalized);
786
+ // 已存在表按“列 -> 索引 -> FK”的顺序规划,
787
+ // 这样后面的索引和约束总是基于列已经收敛后的结构。
788
+ const tableChange = ensureTableChange(plan, tableChanges, table, tableName, 'alter');
789
+ const oldAttrs = currentEntry.tableDef.attributes || {};
790
+ const newAttrs = targetDef.attributes || {};
791
+ const oldAttrNames = Object.keys(oldAttrs);
792
+ const newAttrNames = Object.keys(newAttrs);
793
+ const oldOnly = oldAttrNames.filter((name) => !newAttrs[name]);
794
+ const newOnly = newAttrNames.filter((name) => !oldAttrs[name]);
795
+ const renameResult = pickRenameCandidates(tableName, oldAttrs, newAttrs, oldOnly, newOnly, currentSchema, targetSchema, adapter.dialect, compareForeignKeys);
796
+ renameResult.renameCandidates.forEach(({ from, to }) => {
797
+ const renamePlan = adapter.buildRenamePlan(tableName, from, to, oldAttrs[from], newAttrs[to]);
798
+ const renamePlanSql = getChangePlanSql(renamePlan);
799
+ collectMeta(plan, renamePlan, tableChange);
800
+ recordRenamedColumn(tableChange, from, to, oldAttrs[from], newAttrs[to], renamePlanSql, {
801
+ oldSchema: currentSchema,
802
+ newSchema: targetSchema,
803
+ dialect: adapter.dialect,
804
+ compareForeignKeys,
805
+ reason: renamePlan.renameCandidates?.[0]?.reason || '检测到等价字段,建议按重命名处理',
806
+ });
807
+ });
808
+ oldAttrNames
809
+ .filter((name) => !!newAttrs[name])
810
+ .forEach((column) => {
811
+ const classification = classifyAttributeChange(oldAttrs[column], newAttrs[column], {
812
+ oldSchema: currentSchema,
813
+ newSchema: targetSchema,
814
+ column,
815
+ dialect: adapter.dialect,
816
+ compareForeignKeys,
817
+ });
818
+ const changePlan = adapter.buildColumnPlan(tableName, column, oldAttrs[column], newAttrs[column], classification);
819
+ const changePlanSql = getChangePlanSql(changePlan);
820
+ collectMeta(plan, changePlan, tableChange);
821
+ if (classification.mode !== 'none') {
822
+ recordChangedColumn(tableChange, column, oldAttrs[column], newAttrs[column], changePlanSql, {
823
+ oldSchema: currentSchema,
824
+ newSchema: targetSchema,
825
+ dialect: adapter.dialect,
826
+ compareForeignKeys,
827
+ reason: classification.reason,
828
+ });
829
+ }
830
+ buckets.forwardColumns.push(...(changePlan.forwardSql || []));
831
+ buckets.backwardColumns.push(...(changePlan.backwardSql || []));
832
+ });
833
+ renameResult.newRemain.forEach((column) => {
834
+ const classification = classifyAttributeChange(undefined, newAttrs[column], {
835
+ newSchema: targetSchema,
836
+ column,
837
+ dialect: adapter.dialect,
838
+ compareForeignKeys,
839
+ });
840
+ const changePlan = adapter.buildColumnPlan(tableName, column, undefined, newAttrs[column], classification);
841
+ const changePlanSql = getChangePlanSql(changePlan);
842
+ collectMeta(plan, changePlan, tableChange);
843
+ recordAddedColumn(tableChange, column, newAttrs[column], changePlanSql, {
844
+ schema: targetSchema,
845
+ dialect: adapter.dialect,
846
+ compareForeignKeys,
847
+ reason: classification.reason,
848
+ });
849
+ buckets.forwardColumns.push(...(changePlan.forwardSql || []));
850
+ buckets.backwardColumns.push(...(changePlan.backwardSql || []));
851
+ });
852
+ renameResult.oldRemain.forEach((column) => {
853
+ const classification = classifyAttributeChange(oldAttrs[column], undefined, {
854
+ oldSchema: currentSchema,
855
+ column,
856
+ dialect: adapter.dialect,
857
+ compareForeignKeys,
858
+ });
859
+ const changePlan = adapter.buildColumnPlan(tableName, column, oldAttrs[column], undefined, classification);
860
+ const changePlanSql = getChangePlanSql(changePlan);
861
+ collectMeta(plan, changePlan, tableChange);
862
+ recordRemovedColumn(tableChange, column, oldAttrs[column], changePlanSql, {
863
+ schema: currentSchema,
864
+ dialect: adapter.dialect,
865
+ compareForeignKeys,
866
+ });
867
+ buckets.forwardColumns.push(...(changePlan.forwardSql || []));
868
+ buckets.backwardColumns.push(...(changePlan.backwardSql || []));
869
+ });
870
+ const oldIndexMatchOptions = {
871
+ currentActualName: currentEntry.actualName,
872
+ targetKey: table,
873
+ };
874
+ const newIndexMatchOptions = {
875
+ currentActualName: tableName,
876
+ targetKey: table,
877
+ };
878
+ const oldIndexes = buildIndexList(currentEntry.tableDef.indexes);
879
+ const newIndexes = buildIndexList(targetDef.indexes);
880
+ const matchedOldIndexes = new Set();
881
+ const matchedNewIndexes = new Set();
882
+ oldIndexes.forEach((oldIndex, oldIndexPosition) => {
883
+ const matchedNewPosition = findCompatibleIndexMatch(oldIndex, newIndexes, matchedNewIndexes, oldIndexMatchOptions, newIndexMatchOptions);
884
+ if (matchedNewPosition === undefined) {
885
+ return;
886
+ }
887
+ const newIndex = newIndexes[matchedNewPosition];
888
+ matchedOldIndexes.add(oldIndexPosition);
889
+ matchedNewIndexes.add(matchedNewPosition);
890
+ if (areIndexesEquivalent(oldIndex, newIndex)) {
891
+ if (hasExplicitIndexOrigin(oldIndex)) {
892
+ const renamePlan = adapter.buildRenameIndexPlan(tableName, table, oldIndex, newIndex, {
893
+ isNewTable: false,
894
+ tableStats: options.currentTableStats?.[tableName],
895
+ largeTableRowThreshold,
896
+ });
897
+ const renamePlanSql = getChangePlanSql(renamePlan);
898
+ collectMeta(plan, renamePlan, tableChange);
899
+ if (hasSqlInAnyCategory(renamePlanSql)) {
900
+ recordRenamedIndex(tableChange, oldIndex, newIndex, renamePlanSql);
901
+ }
902
+ buckets.forwardIndexes.push(...(renamePlan.forwardSql || []));
903
+ buckets.backwardIndexes.push(...(renamePlan.backwardSql || []));
904
+ }
905
+ return;
906
+ }
907
+ const changePlan = adapter.buildIndexPlan(tableName, table, oldIndex, newIndex, {
908
+ isNewTable: false,
909
+ tableStats: options.currentTableStats?.[tableName],
910
+ largeTableRowThreshold,
911
+ });
912
+ const changePlanSql = getChangePlanSql(changePlan);
913
+ collectMeta(plan, changePlan, tableChange);
914
+ recordChangedIndex(tableChange, oldIndex, newIndex, changePlanSql);
915
+ buckets.forwardIndexes.push(...(changePlan.forwardSql || []));
916
+ buckets.backwardIndexes.push(...(changePlan.backwardSql || []));
917
+ });
918
+ const indexRenameResult = pickEquivalentIndexRenameCandidates(oldIndexes.filter((_, index) => !matchedOldIndexes.has(index)), newIndexes.filter((_, index) => !matchedNewIndexes.has(index)));
919
+ indexRenameResult.renameCandidates.forEach(({ oldIndex, newIndex }) => {
920
+ const renamePlan = adapter.buildRenameIndexPlan(tableName, table, oldIndex, newIndex, {
921
+ isNewTable: false,
922
+ tableStats: options.currentTableStats?.[tableName],
923
+ largeTableRowThreshold,
924
+ });
925
+ const renamePlanSql = getChangePlanSql(renamePlan);
926
+ collectMeta(plan, renamePlan, tableChange);
927
+ if (hasSqlInAnyCategory(renamePlanSql)) {
928
+ recordRenamedIndex(tableChange, oldIndex, newIndex, renamePlanSql);
929
+ }
930
+ buckets.forwardIndexes.push(...(renamePlan.forwardSql || []));
931
+ buckets.backwardIndexes.push(...(renamePlan.backwardSql || []));
932
+ });
933
+ indexRenameResult.oldRemain.forEach((oldIndex) => {
934
+ const changePlan = adapter.buildIndexPlan(tableName, table, oldIndex, undefined, {
935
+ isNewTable: false,
936
+ tableStats: options.currentTableStats?.[tableName],
937
+ largeTableRowThreshold,
938
+ });
939
+ const changePlanSql = getChangePlanSql(changePlan);
940
+ collectMeta(plan, changePlan, tableChange);
941
+ recordRemovedIndex(tableChange, oldIndex, changePlanSql);
942
+ buckets.forwardIndexes.push(...(changePlan.forwardSql || []));
943
+ buckets.backwardIndexes.push(...(changePlan.backwardSql || []));
944
+ });
945
+ indexRenameResult.newRemain.forEach((newIndex) => {
946
+ const changePlan = adapter.buildIndexPlan(tableName, table, undefined, newIndex, {
947
+ isNewTable: false,
948
+ tableStats: options.currentTableStats?.[tableName],
949
+ largeTableRowThreshold,
950
+ });
951
+ const changePlanSql = getChangePlanSql(changePlan);
952
+ collectMeta(plan, changePlan, tableChange);
953
+ recordAddedIndex(tableChange, newIndex, changePlanSql);
954
+ buckets.forwardIndexes.push(...(changePlan.forwardSql || []));
955
+ buckets.backwardIndexes.push(...(changePlan.backwardSql || []));
956
+ });
957
+ if (compareForeignKeys) {
958
+ const oldForeignKeys = getForeignKeyMap(currentEntry.key, currentEntry.tableDef, currentSchema, adapter);
959
+ const newForeignKeys = getForeignKeyMap(table, targetDef, targetSchema, adapter);
960
+ const foreignKeyNames = Array.from(new Set([
961
+ ...Object.keys(oldForeignKeys),
962
+ ...Object.keys(newForeignKeys),
963
+ ]));
964
+ foreignKeyNames.forEach((name) => {
965
+ const oldForeignKey = oldForeignKeys[name];
966
+ const newForeignKey = newForeignKeys[name];
967
+ if (oldForeignKey && newForeignKey && isObjectEqual(oldForeignKey, newForeignKey)) {
968
+ return;
969
+ }
970
+ const changePlan = adapter.buildForeignKeyPlan(tableName, oldForeignKey, newForeignKey, false);
971
+ const changePlanSql = getChangePlanSql(changePlan);
972
+ collectMeta(plan, changePlan, tableChange);
973
+ if (!oldForeignKey && newForeignKey) {
974
+ recordAddedForeignKey(tableChange, newForeignKey, changePlanSql);
975
+ }
976
+ else if (oldForeignKey && !newForeignKey) {
977
+ recordRemovedForeignKey(tableChange, oldForeignKey, changePlanSql);
978
+ }
979
+ else {
980
+ recordChangedForeignKey(tableChange, oldForeignKey, newForeignKey, changePlanSql);
981
+ }
982
+ buckets.forwardForeignKeys.push(...(changePlan.forwardSql || []));
983
+ buckets.backwardForeignKeys.push(...(changePlan.backwardSql || []));
984
+ });
985
+ }
986
+ });
987
+ const removedTables = [];
988
+ currentTables.forEach((entry, normalized) => {
989
+ if (!matchedCurrentTables.has(normalized)) {
990
+ removedTables.push(entry);
991
+ }
992
+ });
993
+ // 删除表也分两轮:先删依赖约束,再删表本体。
994
+ // backward 方向需要和 forward 相反,否则很容易被 FK 依赖卡住。
995
+ removedTables.forEach((entry) => {
996
+ const tableChange = ensureTableChange(plan, tableChanges, entry.key, entry.actualName, 'drop');
997
+ const foreignKeys = getForeignKeyMap(entry.key, entry.tableDef, currentSchema, adapter);
998
+ Object.values(foreignKeys).forEach((foreignKey) => {
999
+ const changePlan = adapter.buildForeignKeyPlan(entry.actualName, foreignKey, undefined, false);
1000
+ const changePlanSql = getChangePlanSql(changePlan);
1001
+ collectMeta(plan, changePlan, tableChange);
1002
+ recordRemovedForeignKey(tableChange, foreignKey, changePlanSql);
1003
+ buckets.backwardForeignKeys.push(...(changePlan.backwardSql || []));
1004
+ });
1005
+ });
1006
+ removedTables.forEach((entry) => {
1007
+ const tableChange = ensureTableChange(plan, tableChanges, entry.key, entry.actualName, 'drop');
1008
+ const changePlan = adapter.buildDropTablePlan(entry.actualName);
1009
+ const changePlanSql = getChangePlanSql(changePlan);
1010
+ collectMeta(plan, changePlan, tableChange);
1011
+ Object.entries(entry.tableDef.attributes || {}).forEach(([column, attr]) => {
1012
+ recordRemovedColumn(tableChange, column, attr, changePlanSql, {
1013
+ schema: currentSchema,
1014
+ dialect: adapter.dialect,
1015
+ compareForeignKeys,
1016
+ });
1017
+ });
1018
+ (entry.tableDef.indexes || []).forEach((index) => {
1019
+ recordRemovedIndex(tableChange, index, changePlanSql);
1020
+ });
1021
+ buckets.backwardTables.push(...(changePlan.backwardSql || []));
1022
+ });
1023
+ // 最终 SQL 不按遍历顺序输出,而是按依赖桶统一回放。
1024
+ // forward 方向是“表 -> 列 -> 索引 -> FK”,backward 方向则严格反过来。
1025
+ plan.forwardSql.push(...buckets.forwardTables, ...buckets.forwardColumns, ...buckets.forwardIndexes, ...buckets.forwardForeignKeys);
1026
+ plan.backwardSql.push(...buckets.backwardForeignKeys, ...buckets.backwardIndexes, ...buckets.backwardColumns, ...buckets.backwardTables);
1027
+ plan.tableChanges = plan.tableChanges.filter(hasTableChange);
1028
+ return plan;
1029
+ }