befly 3.9.2 → 3.9.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "befly",
3
- "version": "3.9.2",
3
+ "version": "3.9.4",
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": "a311a415ccf02fb5089040883049a02a54f5ec60",
76
+ "gitHead": "19ec26ee9c8cf1f4ca6c4693176272a77a67f673",
77
77
  "devDependencies": {
78
78
  "typescript": "^5.9.3"
79
79
  }
@@ -72,14 +72,14 @@ export function getSystemColumnDef(fieldName: string): string | null {
72
72
  created_at: '`created_at` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT "创建时间"',
73
73
  updated_at: '`updated_at` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT "更新时间"',
74
74
  deleted_at: '`deleted_at` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT "删除时间"',
75
- state: '`state` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT "状态字段"'
75
+ state: '`state` BIGINT UNSIGNED NOT NULL DEFAULT 1 COMMENT "状态字段"'
76
76
  };
77
77
  const pgDefs: Record<string, string> = {
78
78
  id: '"id" INTEGER PRIMARY KEY',
79
79
  created_at: '"created_at" INTEGER NOT NULL DEFAULT 0',
80
80
  updated_at: '"updated_at" INTEGER NOT NULL DEFAULT 0',
81
81
  deleted_at: '"deleted_at" INTEGER NOT NULL DEFAULT 0',
82
- state: '"state" INTEGER NOT NULL DEFAULT 0'
82
+ state: '"state" INTEGER NOT NULL DEFAULT 1'
83
83
  };
84
84
 
85
85
  const defs = isMySQL() ? mysqlDefs : pgDefs;
@@ -203,17 +203,50 @@ export async function executeDDLSafely(sql: SQL, stmt: string): Promise<boolean>
203
203
  }
204
204
 
205
205
  /**
206
- * PG 兼容类型变更识别:无需数据重写的宽化型变更
206
+ * 判断是否为兼容的类型变更(宽化型变更,无数据丢失风险)
207
207
  *
208
- * @param currentType - 当前类型
209
- * @param newType - 新类型
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 isPgCompatibleTypeChange(currentType: string, newType: string): boolean {
218
+ export function isCompatibleTypeChange(currentType: string, newType: string): boolean {
213
219
  const c = String(currentType || '').toLowerCase();
214
220
  const n = String(newType || '').toLowerCase();
215
- // varchar -> text 视为宽化
216
- if (c === 'character varying' && n === 'text') return true;
217
- // text -> character varying 非宽化(可能截断),不兼容
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
  }
@@ -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, isPgCompatibleTypeChange, getSystemColumnDef } from './ddl.js';
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
- // 严格限制:除 string/array 互转外,禁止任何字段类型变更
74
+ // 类型变更检查:只允许兼容的宽化型变更(如 INT -> BIGINT)
75
75
  if (hasTypeChange) {
76
76
  const typeChange = comparison.find((c) => c.type === 'datatype');
77
- const errorMsg = [`禁止字段类型变更: ${tableName}.${dbFieldName}`, `当前类型: ${typeChange?.current}`, `目标类型: ${typeChange?.expected}`, `说明: 仅允许 string<->array 互相切换,其他类型变更需要手动处理`].join('\n');
78
- throw new Error(errorMsg);
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;
@@ -6,7 +6,7 @@
6
6
  * - buildSystemColumnDefs
7
7
  * - buildBusinessColumnDefs
8
8
  * - generateDDLClause
9
- * - isPgCompatibleTypeChange
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 isPgCompatibleTypeChange: any;
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
- isPgCompatibleTypeChange = ddl.isPgCompatibleTypeChange;
30
+ isCompatibleTypeChange = ddl.isCompatibleTypeChange;
31
31
  });
32
32
 
33
33
  describe('buildIndexSQL (MySQL)', () => {
@@ -74,7 +74,7 @@ describe('buildSystemColumnDefs (MySQL)', () => {
74
74
  const def = defs.find((d: string) => d.includes('`state`'));
75
75
  expect(def).toContain('BIGINT UNSIGNED');
76
76
  expect(def).toContain('NOT NULL');
77
- expect(def).toContain('DEFAULT 0');
77
+ expect(def).toContain('DEFAULT 1');
78
78
  });
79
79
  });
80
80
 
@@ -186,21 +186,45 @@ describe('generateDDLClause (MySQL)', () => {
186
186
  });
187
187
  });
188
188
 
189
- describe('isPgCompatibleTypeChange', () => {
189
+ describe('isCompatibleTypeChange', () => {
190
190
  test('varchar -> text 是兼容变更', () => {
191
- expect(isPgCompatibleTypeChange('character varying', 'text')).toBe(true);
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(isPgCompatibleTypeChange('text', 'character varying')).toBe(false);
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(isPgCompatibleTypeChange('text', 'text')).toBe(false);
222
+ expect(isCompatibleTypeChange('text', 'text')).toBe(false);
223
+ expect(isCompatibleTypeChange('bigint', 'bigint')).toBe(false);
200
224
  });
201
225
 
202
226
  test('空值处理', () => {
203
- expect(isPgCompatibleTypeChange(null, 'text')).toBe(false);
204
- expect(isPgCompatibleTypeChange('text', null)).toBe(false);
227
+ expect(isCompatibleTypeChange(null, 'text')).toBe(false);
228
+ expect(isCompatibleTypeChange('text', null)).toBe(false);
205
229
  });
206
230
  });