befly 3.9.2 → 3.9.3
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/package.json +2 -2
- package/sync/syncDb/ddl.ts +40 -7
- package/sync/syncDb/table.ts +11 -11
- package/tests/syncDb-ddl.test.ts +33 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.3",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"pino": "^10.1.0",
|
|
74
74
|
"pino-roll": "^4.0.0"
|
|
75
75
|
},
|
|
76
|
-
"gitHead": "
|
|
76
|
+
"gitHead": "4baa6420da1d505fb6a7cd82169d597456d9a99f",
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"typescript": "^5.9.3"
|
|
79
79
|
}
|
package/sync/syncDb/ddl.ts
CHANGED
|
@@ -203,17 +203,50 @@ export async function executeDDLSafely(sql: SQL, stmt: string): Promise<boolean>
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
*
|
|
206
|
+
* 判断是否为兼容的类型变更(宽化型变更,无数据丢失风险)
|
|
207
207
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
208
|
+
* 允许的变更:
|
|
209
|
+
* - MySQL: INT -> BIGINT, TINYINT -> INT/BIGINT, etc.
|
|
210
|
+
* - MySQL: VARCHAR -> TEXT/MEDIUMTEXT
|
|
211
|
+
* - PG: INTEGER -> BIGINT
|
|
212
|
+
* - PG: VARCHAR -> TEXT
|
|
213
|
+
*
|
|
214
|
+
* @param currentType - 当前数据库中的类型
|
|
215
|
+
* @param newType - 目标类型
|
|
210
216
|
* @returns 是否为兼容变更
|
|
211
217
|
*/
|
|
212
|
-
export function
|
|
218
|
+
export function isCompatibleTypeChange(currentType: string, newType: string): boolean {
|
|
213
219
|
const c = String(currentType || '').toLowerCase();
|
|
214
220
|
const n = String(newType || '').toLowerCase();
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
221
|
+
|
|
222
|
+
// 相同类型不算变更
|
|
223
|
+
if (c === n) return false;
|
|
224
|
+
|
|
225
|
+
// 提取基础类型(去掉 unsigned、长度等修饰)
|
|
226
|
+
const extractBaseType = (t: string): string => {
|
|
227
|
+
// 移除 unsigned 和括号内容
|
|
228
|
+
return t
|
|
229
|
+
.replace(/\s*unsigned/gi, '')
|
|
230
|
+
.replace(/\([^)]*\)/g, '')
|
|
231
|
+
.trim();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const cBase = extractBaseType(c);
|
|
235
|
+
const nBase = extractBaseType(n);
|
|
236
|
+
|
|
237
|
+
// MySQL/通用 整数类型宽化(小 -> 大)
|
|
238
|
+
const intTypes = ['tinyint', 'smallint', 'mediumint', 'int', 'integer', 'bigint'];
|
|
239
|
+
const cIntIdx = intTypes.indexOf(cBase);
|
|
240
|
+
const nIntIdx = intTypes.indexOf(nBase);
|
|
241
|
+
if (cIntIdx !== -1 && nIntIdx !== -1 && nIntIdx > cIntIdx) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 字符串类型宽化
|
|
246
|
+
// MySQL: varchar -> text/mediumtext/longtext
|
|
247
|
+
if (cBase === 'varchar' && (nBase === 'text' || nBase === 'mediumtext' || nBase === 'longtext')) return true;
|
|
248
|
+
// PG: character varying -> text
|
|
249
|
+
if (cBase === 'character varying' && nBase === 'text') return true;
|
|
250
|
+
|
|
218
251
|
return false;
|
|
219
252
|
}
|
package/sync/syncDb/table.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { snakeCase } from 'es-toolkit/string';
|
|
|
11
11
|
import { Logger } from '../../lib/logger.js';
|
|
12
12
|
import { isMySQL, isPG, CHANGE_TYPE_LABELS, getTypeMapping, SYSTEM_INDEX_FIELDS } from './constants.js';
|
|
13
13
|
import { logFieldChange, resolveDefaultValue, generateDefaultSql, isStringOrArrayType } from './helpers.js';
|
|
14
|
-
import { generateDDLClause,
|
|
14
|
+
import { generateDDLClause, getSystemColumnDef, isCompatibleTypeChange } from './ddl.js';
|
|
15
15
|
import { getTableColumns, getTableIndexes } from './schema.js';
|
|
16
16
|
import { compareFieldDefinition, applyTablePlan } from './apply.js';
|
|
17
17
|
import type { TablePlan } from '../../types/sync.js';
|
|
@@ -71,11 +71,18 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
71
71
|
const onlyDefaultChanged = comparison.every((c) => c.type === 'default');
|
|
72
72
|
const defaultChanged = comparison.some((c) => c.type === 'default');
|
|
73
73
|
|
|
74
|
-
//
|
|
74
|
+
// 类型变更检查:只允许兼容的宽化型变更(如 INT -> BIGINT)
|
|
75
75
|
if (hasTypeChange) {
|
|
76
76
|
const typeChange = comparison.find((c) => c.type === 'datatype');
|
|
77
|
-
const
|
|
78
|
-
|
|
77
|
+
const currentType = String(typeChange?.current || '').toLowerCase();
|
|
78
|
+
const typeMapping = getTypeMapping();
|
|
79
|
+
const expectedType = typeMapping[fieldDef.type]?.toLowerCase() || '';
|
|
80
|
+
|
|
81
|
+
if (!isCompatibleTypeChange(currentType, expectedType)) {
|
|
82
|
+
const errorMsg = [`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${typeChange?.current}`, `目标类型: ${typeChange?.expected}`, `说明: 仅允许宽化型变更(如 INT->BIGINT, VARCHAR->TEXT),其他类型变更需要手动处理`].join('\n');
|
|
83
|
+
throw new Error(errorMsg);
|
|
84
|
+
}
|
|
85
|
+
Logger.debug(`[兼容类型变更] ${tableName}.${dbFieldName} ${currentType} -> ${expectedType}`);
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
// 默认值变化处理
|
|
@@ -111,13 +118,6 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
111
118
|
if (isShrink && !force) skipModify = true;
|
|
112
119
|
}
|
|
113
120
|
|
|
114
|
-
if (hasTypeChange) {
|
|
115
|
-
const typeMapping = getTypeMapping();
|
|
116
|
-
if (isPG() && isPgCompatibleTypeChange(existingColumns[dbFieldName].type, typeMapping[fieldDef.type].toLowerCase())) {
|
|
117
|
-
Logger.debug(`[PG兼容类型变更] ${tableName}.${dbFieldName} ${existingColumns[dbFieldName].type} -> ${typeMapping[fieldDef.type].toLowerCase()} 允许执行`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
121
|
if (!skipModify) modifyClauses.push(generateDDLClause(fieldKey, fieldDef, false));
|
|
122
122
|
}
|
|
123
123
|
changed = true;
|
package/tests/syncDb-ddl.test.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - buildSystemColumnDefs
|
|
7
7
|
* - buildBusinessColumnDefs
|
|
8
8
|
* - generateDDLClause
|
|
9
|
-
* -
|
|
9
|
+
* - isCompatibleTypeChange
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
@@ -19,7 +19,7 @@ let buildIndexSQL: any;
|
|
|
19
19
|
let buildSystemColumnDefs: any;
|
|
20
20
|
let buildBusinessColumnDefs: any;
|
|
21
21
|
let generateDDLClause: any;
|
|
22
|
-
let
|
|
22
|
+
let isCompatibleTypeChange: any;
|
|
23
23
|
|
|
24
24
|
beforeAll(async () => {
|
|
25
25
|
const ddl = await import('../sync/syncDb/ddl.js');
|
|
@@ -27,7 +27,7 @@ beforeAll(async () => {
|
|
|
27
27
|
buildSystemColumnDefs = ddl.buildSystemColumnDefs;
|
|
28
28
|
buildBusinessColumnDefs = ddl.buildBusinessColumnDefs;
|
|
29
29
|
generateDDLClause = ddl.generateDDLClause;
|
|
30
|
-
|
|
30
|
+
isCompatibleTypeChange = ddl.isCompatibleTypeChange;
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
describe('buildIndexSQL (MySQL)', () => {
|
|
@@ -186,21 +186,45 @@ describe('generateDDLClause (MySQL)', () => {
|
|
|
186
186
|
});
|
|
187
187
|
});
|
|
188
188
|
|
|
189
|
-
describe('
|
|
189
|
+
describe('isCompatibleTypeChange', () => {
|
|
190
190
|
test('varchar -> text 是兼容变更', () => {
|
|
191
|
-
expect(
|
|
191
|
+
expect(isCompatibleTypeChange('character varying', 'text')).toBe(true);
|
|
192
|
+
expect(isCompatibleTypeChange('varchar(100)', 'text')).toBe(true);
|
|
193
|
+
expect(isCompatibleTypeChange('varchar(100)', 'mediumtext')).toBe(true);
|
|
192
194
|
});
|
|
193
195
|
|
|
194
196
|
test('text -> varchar 不是兼容变更', () => {
|
|
195
|
-
expect(
|
|
197
|
+
expect(isCompatibleTypeChange('text', 'character varying')).toBe(false);
|
|
198
|
+
expect(isCompatibleTypeChange('text', 'varchar(100)')).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('int -> bigint 是兼容变更', () => {
|
|
202
|
+
expect(isCompatibleTypeChange('int', 'bigint')).toBe(true);
|
|
203
|
+
expect(isCompatibleTypeChange('int unsigned', 'bigint unsigned')).toBe(true);
|
|
204
|
+
expect(isCompatibleTypeChange('tinyint', 'int')).toBe(true);
|
|
205
|
+
expect(isCompatibleTypeChange('tinyint', 'bigint')).toBe(true);
|
|
206
|
+
expect(isCompatibleTypeChange('smallint', 'int')).toBe(true);
|
|
207
|
+
expect(isCompatibleTypeChange('mediumint', 'bigint')).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('bigint -> int 不是兼容变更(收缩)', () => {
|
|
211
|
+
expect(isCompatibleTypeChange('bigint', 'int')).toBe(false);
|
|
212
|
+
expect(isCompatibleTypeChange('int', 'tinyint')).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('PG integer -> bigint 是兼容变更', () => {
|
|
216
|
+
expect(isCompatibleTypeChange('integer', 'bigint')).toBe(true);
|
|
217
|
+
expect(isCompatibleTypeChange('smallint', 'integer')).toBe(true);
|
|
218
|
+
expect(isCompatibleTypeChange('smallint', 'bigint')).toBe(true);
|
|
196
219
|
});
|
|
197
220
|
|
|
198
221
|
test('相同类型不是变更', () => {
|
|
199
|
-
expect(
|
|
222
|
+
expect(isCompatibleTypeChange('text', 'text')).toBe(false);
|
|
223
|
+
expect(isCompatibleTypeChange('bigint', 'bigint')).toBe(false);
|
|
200
224
|
});
|
|
201
225
|
|
|
202
226
|
test('空值处理', () => {
|
|
203
|
-
expect(
|
|
204
|
-
expect(
|
|
227
|
+
expect(isCompatibleTypeChange(null, 'text')).toBe(false);
|
|
228
|
+
expect(isCompatibleTypeChange('text', null)).toBe(false);
|
|
205
229
|
});
|
|
206
230
|
});
|