befly 3.8.27 → 3.8.29
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 +4 -4
- package/sync/syncDb/apply.ts +10 -9
- package/sync/syncDb/constants.ts +61 -12
- package/sync/syncDb/ddl.ts +7 -7
- package/sync/syncDb/helpers.ts +3 -3
- package/sync/syncDb/schema.ts +16 -19
- package/sync/syncDb/table.ts +6 -5
- package/sync/syncDb/tableCreate.ts +7 -7
- package/sync/syncDb/types.ts +3 -2
- package/sync/syncDb/version.ts +4 -4
- package/sync/syncDb.ts +5 -0
- package/tests/syncDb-apply.test.ts +3 -2
- package/tests/syncDb-constants.test.ts +3 -2
- package/tests/syncDb-ddl.test.ts +3 -2
- package/tests/syncDb-helpers.test.ts +3 -2
- package/tests/syncDb-schema.test.ts +3 -3
- package/tests/syncDb-types.test.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "befly",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.29",
|
|
4
4
|
"description": "Befly - 为 Bun 专属打造的 TypeScript API 接口框架核心引擎",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -66,12 +66,12 @@
|
|
|
66
66
|
"bun": ">=1.3.0"
|
|
67
67
|
},
|
|
68
68
|
"dependencies": {
|
|
69
|
-
"befly-util": "^1.0.
|
|
69
|
+
"befly-util": "^1.0.7",
|
|
70
70
|
"chalk": "^5.6.2",
|
|
71
|
-
"es-toolkit": "^1.
|
|
71
|
+
"es-toolkit": "^1.42.0",
|
|
72
72
|
"pathe": "^2.0.3"
|
|
73
73
|
},
|
|
74
|
-
"gitHead": "
|
|
74
|
+
"gitHead": "1f3cacbe4134ee60654b75338bcaeda535898a84",
|
|
75
75
|
"devDependencies": {
|
|
76
76
|
"typescript": "^5.9.3"
|
|
77
77
|
}
|
package/sync/syncDb/apply.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Logger } from '../../lib/logger.js';
|
|
10
|
-
import {
|
|
10
|
+
import { isMySQL, isPG, isSQLite, IS_PLAN, CHANGE_TYPE_LABELS, getTypeMapping } from './constants.js';
|
|
11
11
|
import { logFieldChange, resolveDefaultValue, isStringOrArrayType } from './helpers.js';
|
|
12
12
|
import { executeDDLSafely, buildIndexSQL } from './ddl.js';
|
|
13
13
|
import { rebuildSqliteTable } from './sqlite.js';
|
|
@@ -27,7 +27,7 @@ import type { FieldDefinition } from 'befly/types/common';
|
|
|
27
27
|
* @returns 完整的 ALTER TABLE 语句
|
|
28
28
|
*/
|
|
29
29
|
function buildAlterTableSQL(tableName: string, clauses: string[]): string {
|
|
30
|
-
if (
|
|
30
|
+
if (isMySQL()) {
|
|
31
31
|
return `ALTER TABLE \`${tableName}\` ${clauses.join(', ')}, ALGORITHM=INSTANT, LOCK=NONE`;
|
|
32
32
|
}
|
|
33
33
|
return `ALTER TABLE "${tableName}" ${clauses.join(', ')}`;
|
|
@@ -51,7 +51,7 @@ export function compareFieldDefinition(existingColumn: ColumnInfo, fieldDef: Fie
|
|
|
51
51
|
const changes: FieldChange[] = [];
|
|
52
52
|
|
|
53
53
|
// 检查长度变化(string和array类型) - SQLite 不比较长度
|
|
54
|
-
if (!
|
|
54
|
+
if (!isSQLite() && isStringOrArrayType(fieldDef.type)) {
|
|
55
55
|
if (existingColumn.max !== fieldDef.max) {
|
|
56
56
|
changes.push({
|
|
57
57
|
type: 'length',
|
|
@@ -62,7 +62,7 @@ export function compareFieldDefinition(existingColumn: ColumnInfo, fieldDef: Fie
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// 检查注释变化(MySQL/PG 支持列注释)
|
|
65
|
-
if (!
|
|
65
|
+
if (!isSQLite()) {
|
|
66
66
|
const currentComment = existingColumn.comment || '';
|
|
67
67
|
if (currentComment !== fieldDef.name) {
|
|
68
68
|
changes.push({
|
|
@@ -74,6 +74,7 @@ export function compareFieldDefinition(existingColumn: ColumnInfo, fieldDef: Fie
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// 检查数据类型变化(只对比基础类型)
|
|
77
|
+
const typeMapping = getTypeMapping();
|
|
77
78
|
const expectedType = typeMapping[fieldDef.type].toLowerCase();
|
|
78
79
|
const currentType = existingColumn.type.toLowerCase();
|
|
79
80
|
|
|
@@ -127,7 +128,7 @@ export async function applyTablePlan(sql: SQL, tableName: string, fields: Record
|
|
|
127
128
|
if (!plan || !plan.changed) return;
|
|
128
129
|
|
|
129
130
|
// SQLite: 仅支持部分 ALTER;需要时走重建
|
|
130
|
-
if (
|
|
131
|
+
if (isSQLite()) {
|
|
131
132
|
if (plan.modifyClauses.length > 0 || plan.defaultClauses.length > 0) {
|
|
132
133
|
if (IS_PLAN) Logger.debug(`[计划] 重建表 ${tableName} 以应用列修改/默认值变化`);
|
|
133
134
|
else await rebuildSqliteTable(sql, tableName, fields);
|
|
@@ -143,19 +144,19 @@ export async function applyTablePlan(sql: SQL, tableName: string, fields: Record
|
|
|
143
144
|
if (clauses.length > 0) {
|
|
144
145
|
const stmt = buildAlterTableSQL(tableName, clauses);
|
|
145
146
|
if (IS_PLAN) Logger.debug(`[计划] ${stmt}`);
|
|
146
|
-
else if (
|
|
147
|
+
else if (isMySQL()) await executeDDLSafely(sql, stmt);
|
|
147
148
|
else await sql.unsafe(stmt);
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
// 默认值专用 ALTER(SQLite 不支持)
|
|
152
153
|
if (plan.defaultClauses.length > 0) {
|
|
153
|
-
if (
|
|
154
|
+
if (isSQLite()) {
|
|
154
155
|
Logger.warn(`SQLite 不支持修改默认值,表 ${tableName} 的默认值变更已跳过`);
|
|
155
156
|
} else {
|
|
156
157
|
const stmt = buildAlterTableSQL(tableName, plan.defaultClauses);
|
|
157
158
|
if (IS_PLAN) Logger.debug(`[计划] ${stmt}`);
|
|
158
|
-
else if (
|
|
159
|
+
else if (isMySQL()) await executeDDLSafely(sql, stmt);
|
|
159
160
|
else await sql.unsafe(stmt);
|
|
160
161
|
}
|
|
161
162
|
}
|
|
@@ -182,7 +183,7 @@ export async function applyTablePlan(sql: SQL, tableName: string, fields: Record
|
|
|
182
183
|
}
|
|
183
184
|
|
|
184
185
|
// PG 列注释
|
|
185
|
-
if (
|
|
186
|
+
if (isPG() && plan.commentActions && plan.commentActions.length > 0) {
|
|
186
187
|
for (const stmt of plan.commentActions) {
|
|
187
188
|
if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
|
|
188
189
|
else await sql.unsafe(stmt);
|
package/sync/syncDb/constants.ts
CHANGED
|
@@ -63,17 +63,66 @@ export const MYSQL_TABLE_CONFIG = {
|
|
|
63
63
|
// 是否为计划模式(仅输出 SQL 不执行)
|
|
64
64
|
export const IS_PLAN = process.argv.includes('--plan');
|
|
65
65
|
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
export const IS_MYSQL = DB === 'mysql';
|
|
69
|
-
export const IS_PG = DB === 'postgresql' || DB === 'postgres';
|
|
70
|
-
export const IS_SQLITE = DB === 'sqlite';
|
|
66
|
+
// 数据库类型(运行时设置,默认 mysql)
|
|
67
|
+
let _dbType: string = 'mysql';
|
|
71
68
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
69
|
+
/**
|
|
70
|
+
* 设置数据库类型(由 syncDbCommand 调用)
|
|
71
|
+
* @param dbType - 数据库类型(mysql/postgresql/postgres/sqlite)
|
|
72
|
+
*/
|
|
73
|
+
export function setDbType(dbType: string): void {
|
|
74
|
+
_dbType = (dbType || 'mysql').toLowerCase();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 获取当前数据库类型
|
|
79
|
+
*/
|
|
80
|
+
export function getDbType(): string {
|
|
81
|
+
return _dbType;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 数据库类型判断(getter 函数,运行时动态计算)
|
|
85
|
+
export function isMySQL(): boolean {
|
|
86
|
+
return _dbType === 'mysql';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function isPG(): boolean {
|
|
90
|
+
return _dbType === 'postgresql' || _dbType === 'postgres';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isSQLite(): boolean {
|
|
94
|
+
return _dbType === 'sqlite';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 兼容旧代码的静态别名(通过 getter 实现动态获取)
|
|
98
|
+
export const DB_TYPE = {
|
|
99
|
+
get current(): string {
|
|
100
|
+
return _dbType;
|
|
101
|
+
},
|
|
102
|
+
get IS_MYSQL(): boolean {
|
|
103
|
+
return isMySQL();
|
|
104
|
+
},
|
|
105
|
+
get IS_PG(): boolean {
|
|
106
|
+
return isPG();
|
|
107
|
+
},
|
|
108
|
+
get IS_SQLITE(): boolean {
|
|
109
|
+
return isSQLite();
|
|
110
|
+
}
|
|
79
111
|
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 获取字段类型映射(根据当前数据库类型)
|
|
115
|
+
*/
|
|
116
|
+
export function getTypeMapping(): Record<string, string> {
|
|
117
|
+
const isSqlite = isSQLite();
|
|
118
|
+
const isPg = isPG();
|
|
119
|
+
const isMysql = isMySQL();
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
number: isSqlite ? 'INTEGER' : isPg ? 'BIGINT' : 'BIGINT',
|
|
123
|
+
string: isSqlite ? 'TEXT' : isPg ? 'character varying' : 'VARCHAR',
|
|
124
|
+
text: isMysql ? 'MEDIUMTEXT' : 'TEXT',
|
|
125
|
+
array_string: isSqlite ? 'TEXT' : isPg ? 'character varying' : 'VARCHAR',
|
|
126
|
+
array_text: isMysql ? 'MEDIUMTEXT' : 'TEXT'
|
|
127
|
+
};
|
|
128
|
+
}
|
package/sync/syncDb/ddl.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { snakeCase } from 'es-toolkit/string';
|
|
12
12
|
import { Logger } from '../../lib/logger.js';
|
|
13
|
-
import {
|
|
13
|
+
import { isMySQL, isPG, getTypeMapping } from './constants.js';
|
|
14
14
|
import { quoteIdentifier, escapeComment } from './helpers.js';
|
|
15
15
|
import { resolveDefaultValue, generateDefaultSql, getSqlType } from './types.js';
|
|
16
16
|
|
|
@@ -31,7 +31,7 @@ export function buildIndexSQL(tableName: string, indexName: string, fieldName: s
|
|
|
31
31
|
const indexQuoted = quoteIdentifier(indexName);
|
|
32
32
|
const fieldQuoted = quoteIdentifier(fieldName);
|
|
33
33
|
|
|
34
|
-
if (
|
|
34
|
+
if (isMySQL()) {
|
|
35
35
|
const parts = [];
|
|
36
36
|
if (action === 'create') {
|
|
37
37
|
parts.push(`ADD INDEX ${indexQuoted} (${fieldQuoted})`);
|
|
@@ -44,7 +44,7 @@ export function buildIndexSQL(tableName: string, indexName: string, fieldName: s
|
|
|
44
44
|
return `ALTER TABLE ${tableQuoted} ${parts.join(', ')}`;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
if (
|
|
47
|
+
if (isPG()) {
|
|
48
48
|
if (action === 'create') {
|
|
49
49
|
// 始终使用 CONCURRENTLY
|
|
50
50
|
return `CREATE INDEX CONCURRENTLY IF NOT EXISTS ${indexQuoted} ON ${tableQuoted}(${fieldQuoted})`;
|
|
@@ -65,7 +65,7 @@ export function buildIndexSQL(tableName: string, indexName: string, fieldName: s
|
|
|
65
65
|
* @returns 系统字段的列定义数组
|
|
66
66
|
*/
|
|
67
67
|
export function buildSystemColumnDefs(): string[] {
|
|
68
|
-
if (
|
|
68
|
+
if (isMySQL()) {
|
|
69
69
|
return ['`id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT COMMENT "主键ID"', '`created_at` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT "创建时间"', '`updated_at` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT "更新时间"', '`deleted_at` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT "删除时间"', '`state` BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT "状态字段"'];
|
|
70
70
|
}
|
|
71
71
|
return ['"id" INTEGER PRIMARY KEY', '"created_at" INTEGER NOT NULL DEFAULT 0', '"updated_at" INTEGER NOT NULL DEFAULT 0', '"deleted_at" INTEGER NOT NULL DEFAULT 0', '"state" INTEGER NOT NULL DEFAULT 0'];
|
|
@@ -94,7 +94,7 @@ export function buildBusinessColumnDefs(fields: Record<string, FieldDefinition>)
|
|
|
94
94
|
const uniqueSql = fieldDef.unique ? ' UNIQUE' : '';
|
|
95
95
|
const nullableSql = fieldDef.nullable ? ' NULL' : ' NOT NULL';
|
|
96
96
|
|
|
97
|
-
if (
|
|
97
|
+
if (isMySQL()) {
|
|
98
98
|
colDefs.push(`\`${dbFieldName}\` ${sqlType}${uniqueSql}${nullableSql}${defaultSql} COMMENT "${escapeComment(fieldDef.name)}"`);
|
|
99
99
|
} else {
|
|
100
100
|
colDefs.push(`"${dbFieldName}" ${sqlType}${uniqueSql}${nullableSql}${defaultSql}`);
|
|
@@ -126,10 +126,10 @@ export function generateDDLClause(fieldKey: string, fieldDef: FieldDefinition, i
|
|
|
126
126
|
const uniqueSql = fieldDef.unique ? ' UNIQUE' : '';
|
|
127
127
|
const nullableSql = fieldDef.nullable ? ' NULL' : ' NOT NULL';
|
|
128
128
|
|
|
129
|
-
if (
|
|
129
|
+
if (isMySQL()) {
|
|
130
130
|
return `${isAdd ? 'ADD COLUMN' : 'MODIFY COLUMN'} \`${dbFieldName}\` ${sqlType}${uniqueSql}${nullableSql}${defaultSql} COMMENT "${escapeComment(fieldDef.name)}"`;
|
|
131
131
|
}
|
|
132
|
-
if (
|
|
132
|
+
if (isPG()) {
|
|
133
133
|
if (isAdd) return `ADD COLUMN IF NOT EXISTS "${dbFieldName}" ${sqlType}${uniqueSql}${nullableSql}${defaultSql}`;
|
|
134
134
|
// PG 修改:类型与非空可分条执行,生成 TYPE 改变;非空另由上层统一控制
|
|
135
135
|
return `ALTER COLUMN "${dbFieldName}" TYPE ${sqlType}`;
|
package/sync/syncDb/helpers.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 字段默认值应用
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { isMySQL, isPG } from './constants.js';
|
|
11
11
|
import { Logger } from '../../lib/logger.js';
|
|
12
12
|
|
|
13
13
|
// 从 types.ts 重新导出,保持向后兼容
|
|
@@ -26,8 +26,8 @@ export { isStringOrArrayType, getSqlType, resolveDefaultValue, generateDefaultSq
|
|
|
26
26
|
* // SQLite: user_table
|
|
27
27
|
*/
|
|
28
28
|
export function quoteIdentifier(identifier: string): string {
|
|
29
|
-
if (
|
|
30
|
-
if (
|
|
29
|
+
if (isMySQL()) return `\`${identifier}\``;
|
|
30
|
+
if (isPG()) return `"${identifier}"`;
|
|
31
31
|
return identifier; // SQLite 无需引用
|
|
32
32
|
}
|
|
33
33
|
|
package/sync/syncDb/schema.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 获取表的索引信息
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { isMySQL, isPG, isSQLite } from './constants.js';
|
|
11
11
|
import type { ColumnInfo, IndexInfo } from '../../types.js';
|
|
12
12
|
import type { SQL } from 'bun';
|
|
13
13
|
|
|
@@ -19,22 +19,21 @@ import type { SQL } from 'bun';
|
|
|
19
19
|
* @param dbName - 数据库名称
|
|
20
20
|
* @returns 表是否存在
|
|
21
21
|
*/
|
|
22
|
-
export async function tableExists(sql: SQL, tableName: string, dbName
|
|
22
|
+
export async function tableExists(sql: SQL, tableName: string, dbName: string): Promise<boolean> {
|
|
23
23
|
if (!sql) throw new Error('SQL 客户端未初始化');
|
|
24
|
-
const database = dbName || process.env.DB_NAME;
|
|
25
24
|
|
|
26
25
|
try {
|
|
27
|
-
if (
|
|
28
|
-
const res = await sql`SELECT COUNT(*) AS count FROM information_schema.TABLES WHERE TABLE_SCHEMA = ${
|
|
26
|
+
if (isMySQL()) {
|
|
27
|
+
const res = await sql`SELECT COUNT(*) AS count FROM information_schema.TABLES WHERE TABLE_SCHEMA = ${dbName} AND TABLE_NAME = ${tableName}`;
|
|
29
28
|
return (res[0]?.count || 0) > 0;
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
if (
|
|
31
|
+
if (isPG()) {
|
|
33
32
|
const res = await sql`SELECT COUNT(*)::int AS count FROM information_schema.tables WHERE table_schema = 'public' AND table_name = ${tableName}`;
|
|
34
33
|
return (res[0]?.count || 0) > 0;
|
|
35
34
|
}
|
|
36
35
|
|
|
37
|
-
if (
|
|
36
|
+
if (isSQLite()) {
|
|
38
37
|
const res = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name = ${tableName}`;
|
|
39
38
|
return res.length > 0;
|
|
40
39
|
}
|
|
@@ -61,16 +60,15 @@ export async function tableExists(sql: SQL, tableName: string, dbName?: string):
|
|
|
61
60
|
* @param dbName - 数据库名称
|
|
62
61
|
* @returns 列信息对象,键为列名,值为列详情
|
|
63
62
|
*/
|
|
64
|
-
export async function getTableColumns(sql: SQL, tableName: string, dbName
|
|
63
|
+
export async function getTableColumns(sql: SQL, tableName: string, dbName: string): Promise<{ [key: string]: ColumnInfo }> {
|
|
65
64
|
const columns: { [key: string]: ColumnInfo } = {};
|
|
66
|
-
const database = dbName || process.env.DB_NAME;
|
|
67
65
|
|
|
68
66
|
try {
|
|
69
|
-
if (
|
|
67
|
+
if (isMySQL()) {
|
|
70
68
|
const result = await sql`
|
|
71
69
|
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT, COLUMN_TYPE
|
|
72
70
|
FROM information_schema.COLUMNS
|
|
73
|
-
WHERE TABLE_SCHEMA = ${
|
|
71
|
+
WHERE TABLE_SCHEMA = ${dbName} AND TABLE_NAME = ${tableName}
|
|
74
72
|
ORDER BY ORDINAL_POSITION
|
|
75
73
|
`;
|
|
76
74
|
for (const row of result) {
|
|
@@ -91,7 +89,7 @@ export async function getTableColumns(sql: SQL, tableName: string, dbName?: stri
|
|
|
91
89
|
comment: row.COLUMN_COMMENT
|
|
92
90
|
};
|
|
93
91
|
}
|
|
94
|
-
} else if (
|
|
92
|
+
} else if (isPG()) {
|
|
95
93
|
const result = await sql`
|
|
96
94
|
SELECT column_name, data_type, character_maximum_length, is_nullable, column_default
|
|
97
95
|
FROM information_schema.columns
|
|
@@ -119,7 +117,7 @@ export async function getTableColumns(sql: SQL, tableName: string, dbName?: stri
|
|
|
119
117
|
comment: commentMap[row.column_name] ?? null
|
|
120
118
|
};
|
|
121
119
|
}
|
|
122
|
-
} else if (
|
|
120
|
+
} else if (isSQLite()) {
|
|
123
121
|
const result = await sql.unsafe(`PRAGMA table_info(${tableName})`);
|
|
124
122
|
for (const row of result) {
|
|
125
123
|
let baseType = String(row.type || '').toUpperCase();
|
|
@@ -154,16 +152,15 @@ export async function getTableColumns(sql: SQL, tableName: string, dbName?: stri
|
|
|
154
152
|
* @param dbName - 数据库名称
|
|
155
153
|
* @returns 索引信息对象,键为索引名,值为列名数组
|
|
156
154
|
*/
|
|
157
|
-
export async function getTableIndexes(sql: SQL, tableName: string, dbName
|
|
155
|
+
export async function getTableIndexes(sql: SQL, tableName: string, dbName: string): Promise<IndexInfo> {
|
|
158
156
|
const indexes: IndexInfo = {};
|
|
159
|
-
const database = dbName || process.env.DB_NAME;
|
|
160
157
|
|
|
161
158
|
try {
|
|
162
|
-
if (
|
|
159
|
+
if (isMySQL()) {
|
|
163
160
|
const result = await sql`
|
|
164
161
|
SELECT INDEX_NAME, COLUMN_NAME
|
|
165
162
|
FROM information_schema.STATISTICS
|
|
166
|
-
WHERE TABLE_SCHEMA = ${
|
|
163
|
+
WHERE TABLE_SCHEMA = ${dbName}
|
|
167
164
|
AND TABLE_NAME = ${tableName}
|
|
168
165
|
AND INDEX_NAME != 'PRIMARY'
|
|
169
166
|
ORDER BY INDEX_NAME
|
|
@@ -172,7 +169,7 @@ export async function getTableIndexes(sql: SQL, tableName: string, dbName?: stri
|
|
|
172
169
|
if (!indexes[row.INDEX_NAME]) indexes[row.INDEX_NAME] = [];
|
|
173
170
|
indexes[row.INDEX_NAME].push(row.COLUMN_NAME);
|
|
174
171
|
}
|
|
175
|
-
} else if (
|
|
172
|
+
} else if (isPG()) {
|
|
176
173
|
const result = await sql`
|
|
177
174
|
SELECT indexname, indexdef
|
|
178
175
|
FROM pg_indexes
|
|
@@ -185,7 +182,7 @@ export async function getTableIndexes(sql: SQL, tableName: string, dbName?: stri
|
|
|
185
182
|
indexes[row.indexname] = [col];
|
|
186
183
|
}
|
|
187
184
|
}
|
|
188
|
-
} else if (
|
|
185
|
+
} else if (isSQLite()) {
|
|
189
186
|
const list = await sql.unsafe(`PRAGMA index_list(${tableName})`);
|
|
190
187
|
for (const idx of list) {
|
|
191
188
|
const info = await sql.unsafe(`PRAGMA index_info(${idx.name})`);
|
package/sync/syncDb/table.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { snakeCase } from 'es-toolkit/string';
|
|
11
11
|
import { Logger } from '../../lib/logger.js';
|
|
12
|
-
import {
|
|
12
|
+
import { isMySQL, isPG, CHANGE_TYPE_LABELS, getTypeMapping } from './constants.js';
|
|
13
13
|
import { logFieldChange, resolveDefaultValue, generateDefaultSql, isStringOrArrayType } from './helpers.js';
|
|
14
14
|
import { generateDDLClause, isPgCompatibleTypeChange } from './ddl.js';
|
|
15
15
|
import { getTableColumns, getTableIndexes } from './schema.js';
|
|
@@ -92,9 +92,9 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
if (v !== null && v !== '') {
|
|
95
|
-
if (
|
|
95
|
+
if (isPG()) {
|
|
96
96
|
defaultClauses.push(`ALTER COLUMN "${dbFieldName}" SET DEFAULT ${v}`);
|
|
97
|
-
} else if (
|
|
97
|
+
} else if (isMySQL() && onlyDefaultChanged) {
|
|
98
98
|
// MySQL 的 TEXT/BLOB 不允许 DEFAULT,跳过 text 类型
|
|
99
99
|
if (fieldDef.type !== 'text') {
|
|
100
100
|
defaultClauses.push(`ALTER COLUMN \`${dbFieldName}\` SET DEFAULT ${v}`);
|
|
@@ -112,7 +112,8 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
if (hasTypeChange) {
|
|
115
|
-
|
|
115
|
+
const typeMapping = getTypeMapping();
|
|
116
|
+
if (isPG() && isPgCompatibleTypeChange(existingColumns[dbFieldName].type, typeMapping[fieldDef.type].toLowerCase())) {
|
|
116
117
|
Logger.debug(`[PG兼容类型变更] ${tableName}.${dbFieldName} ${existingColumns[dbFieldName].type} -> ${typeMapping[fieldDef.type].toLowerCase()} 允许执行`);
|
|
117
118
|
}
|
|
118
119
|
}
|
|
@@ -155,7 +156,7 @@ export async function modifyTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
155
156
|
|
|
156
157
|
// PG 列注释处理
|
|
157
158
|
const commentActions = [];
|
|
158
|
-
if (
|
|
159
|
+
if (isPG()) {
|
|
159
160
|
for (const [fieldKey, fieldDef] of Object.entries(fields)) {
|
|
160
161
|
// 转换字段名为下划线格式
|
|
161
162
|
const dbFieldName = snakeCase(fieldKey);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { snakeCase } from 'es-toolkit/string';
|
|
12
12
|
import { Logger } from '../../lib/logger.js';
|
|
13
|
-
import {
|
|
13
|
+
import { isMySQL, isPG, IS_PLAN, MYSQL_TABLE_CONFIG } from './constants.js';
|
|
14
14
|
import { quoteIdentifier } from './helpers.js';
|
|
15
15
|
import { buildSystemColumnDefs, buildBusinessColumnDefs, buildIndexSQL } from './ddl.js';
|
|
16
16
|
import { getTableIndexes } from './schema.js';
|
|
@@ -73,7 +73,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
|
|
|
73
73
|
|
|
74
74
|
// 获取现有索引(MySQL 不支持 IF NOT EXISTS,需要先检查)
|
|
75
75
|
let existingIndexes: Record<string, string[]> = {};
|
|
76
|
-
if (
|
|
76
|
+
if (isMySQL()) {
|
|
77
77
|
existingIndexes = await getTableIndexes(sql, tableName, dbName);
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -81,7 +81,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
|
|
|
81
81
|
for (const sysField of systemIndexFields) {
|
|
82
82
|
const indexName = `idx_${sysField}`;
|
|
83
83
|
// MySQL 跳过已存在的索引
|
|
84
|
-
if (
|
|
84
|
+
if (isMySQL() && existingIndexes[indexName]) {
|
|
85
85
|
continue;
|
|
86
86
|
}
|
|
87
87
|
const stmt = buildIndexSQL(tableName, indexName, sysField, 'create');
|
|
@@ -100,7 +100,7 @@ async function createTableIndexes(sql: SQL, tableName: string, fields: Record<st
|
|
|
100
100
|
if (fieldDef.index === true) {
|
|
101
101
|
const indexName = `idx_${dbFieldName}`;
|
|
102
102
|
// MySQL 跳过已存在的索引
|
|
103
|
-
if (
|
|
103
|
+
if (isMySQL() && existingIndexes[indexName]) {
|
|
104
104
|
continue;
|
|
105
105
|
}
|
|
106
106
|
const stmt = buildIndexSQL(tableName, indexName, dbFieldName, 'create');
|
|
@@ -135,7 +135,7 @@ export async function createTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
135
135
|
const cols = colDefs.join(',\n ');
|
|
136
136
|
const tableQuoted = quoteIdentifier(tableName);
|
|
137
137
|
const { ENGINE, CHARSET, COLLATE } = MYSQL_TABLE_CONFIG;
|
|
138
|
-
const createSQL =
|
|
138
|
+
const createSQL = isMySQL()
|
|
139
139
|
? `CREATE TABLE ${tableQuoted} (
|
|
140
140
|
${cols}
|
|
141
141
|
) ENGINE=${ENGINE} DEFAULT CHARSET=${CHARSET} COLLATE=${COLLATE}`
|
|
@@ -150,9 +150,9 @@ export async function createTable(sql: SQL, tableName: string, fields: Record<st
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
// PostgreSQL: 添加列注释
|
|
153
|
-
if (
|
|
153
|
+
if (isPG() && !IS_PLAN) {
|
|
154
154
|
await addPostgresComments(sql, tableName, fields);
|
|
155
|
-
} else if (
|
|
155
|
+
} else if (isPG() && IS_PLAN) {
|
|
156
156
|
// 计划模式也要输出注释语句
|
|
157
157
|
await addPostgresComments(sql, tableName, fields);
|
|
158
158
|
}
|
package/sync/syncDb/types.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 类型判断工具
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { isMySQL, getTypeMapping } from './constants.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* 判断是否为字符串或数组类型(需要长度参数)
|
|
@@ -42,12 +42,13 @@ export function isStringOrArrayType(fieldType: string): boolean {
|
|
|
42
42
|
* getSqlType('array_text', null) // => 'MEDIUMTEXT'
|
|
43
43
|
*/
|
|
44
44
|
export function getSqlType(fieldType: string, fieldMax: number | null, unsigned: boolean = false): string {
|
|
45
|
+
const typeMapping = getTypeMapping();
|
|
45
46
|
if (isStringOrArrayType(fieldType)) {
|
|
46
47
|
return `${typeMapping[fieldType]}(${fieldMax})`;
|
|
47
48
|
}
|
|
48
49
|
// 处理 UNSIGNED 修饰符(仅 MySQL number 类型)
|
|
49
50
|
const baseType = typeMapping[fieldType] || 'TEXT';
|
|
50
|
-
if (
|
|
51
|
+
if (isMySQL() && fieldType === 'number' && unsigned) {
|
|
51
52
|
return `${baseType} UNSIGNED`;
|
|
52
53
|
}
|
|
53
54
|
return baseType;
|
package/sync/syncDb/version.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { Logger } from '../../lib/logger.js';
|
|
9
|
-
import { DB_VERSION_REQUIREMENTS,
|
|
9
|
+
import { DB_VERSION_REQUIREMENTS, isMySQL, isPG, isSQLite } from './constants.js';
|
|
10
10
|
import type { SQL } from 'bun';
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -23,7 +23,7 @@ import type { SQL } from 'bun';
|
|
|
23
23
|
export async function ensureDbVersion(sql: SQL): Promise<void> {
|
|
24
24
|
if (!sql) throw new Error('SQL 客户端未初始化');
|
|
25
25
|
|
|
26
|
-
if (
|
|
26
|
+
if (isMySQL()) {
|
|
27
27
|
const r = await sql`SELECT VERSION() AS version`;
|
|
28
28
|
if (!r || r.length === 0 || !r[0]?.version) {
|
|
29
29
|
throw new Error('无法获取 MySQL 版本信息');
|
|
@@ -36,7 +36,7 @@ export async function ensureDbVersion(sql: SQL): Promise<void> {
|
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
if (
|
|
39
|
+
if (isPG()) {
|
|
40
40
|
const r = await sql`SELECT version() AS version`;
|
|
41
41
|
if (!r || r.length === 0 || !r[0]?.version) {
|
|
42
42
|
throw new Error('无法获取 PostgreSQL 版本信息');
|
|
@@ -50,7 +50,7 @@ export async function ensureDbVersion(sql: SQL): Promise<void> {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
if (
|
|
53
|
+
if (isSQLite()) {
|
|
54
54
|
const r = await sql`SELECT sqlite_version() AS version`;
|
|
55
55
|
if (!r || r.length === 0 || !r[0]?.version) {
|
|
56
56
|
throw new Error('无法获取 SQLite 版本信息');
|
package/sync/syncDb.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { tableExists } from './syncDb/schema.js';
|
|
|
23
23
|
import { modifyTable } from './syncDb/table.js';
|
|
24
24
|
import { createTable } from './syncDb/tableCreate.js';
|
|
25
25
|
import { applyFieldDefaults } from './syncDb/helpers.js';
|
|
26
|
+
import { setDbType } from './syncDb/constants.js';
|
|
26
27
|
import type { SQL } from 'bun';
|
|
27
28
|
import type { BeflyOptions, SyncDbOptions } from '../types/index.js';
|
|
28
29
|
|
|
@@ -46,6 +47,10 @@ export async function syncDbCommand(config: BeflyOptions, options: SyncDbOptions
|
|
|
46
47
|
// 清空处理记录
|
|
47
48
|
processedTables.length = 0;
|
|
48
49
|
|
|
50
|
+
// 设置数据库类型(从配置获取)
|
|
51
|
+
const dbType = config.db?.type || 'mysql';
|
|
52
|
+
setDbType(dbType);
|
|
53
|
+
|
|
49
54
|
// 验证表定义文件
|
|
50
55
|
await checkTable();
|
|
51
56
|
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
9
|
+
import { setDbType } from '../sync/syncDb/constants.js';
|
|
9
10
|
|
|
10
|
-
//
|
|
11
|
-
|
|
11
|
+
// 设置数据库类型为 MySQL
|
|
12
|
+
setDbType('mysql');
|
|
12
13
|
|
|
13
14
|
let compareFieldDefinition: any;
|
|
14
15
|
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
14
|
+
import { setDbType } from '../sync/syncDb/constants.js';
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
-
|
|
16
|
+
// 设置数据库类型为 MySQL
|
|
17
|
+
setDbType('mysql');
|
|
17
18
|
|
|
18
19
|
let constants: any;
|
|
19
20
|
|
package/tests/syncDb-ddl.test.ts
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
13
|
+
import { setDbType } from '../sync/syncDb/constants.js';
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
-
|
|
15
|
+
// 设置数据库类型为 MySQL
|
|
16
|
+
setDbType('mysql');
|
|
16
17
|
|
|
17
18
|
let buildIndexSQL: any;
|
|
18
19
|
let buildSystemColumnDefs: any;
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, test, expect, beforeAll } from 'bun:test';
|
|
11
|
+
import { setDbType } from '../sync/syncDb/constants.js';
|
|
11
12
|
|
|
12
|
-
//
|
|
13
|
-
|
|
13
|
+
// 设置数据库类型为 MySQL
|
|
14
|
+
setDbType('mysql');
|
|
14
15
|
|
|
15
16
|
let quoteIdentifier: any;
|
|
16
17
|
let escapeComment: any;
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, test, expect, beforeAll, mock } from 'bun:test';
|
|
13
|
+
import { setDbType } from '../sync/syncDb/constants.js';
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
process.env.DB_NAME = 'test_db';
|
|
15
|
+
// 设置数据库类型为 MySQL
|
|
16
|
+
setDbType('mysql');
|
|
17
17
|
|
|
18
18
|
let tableExists: any;
|
|
19
19
|
let getTableColumns: any;
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
|
|
12
|
+
import { setDbType } from '../sync/syncDb/constants.js';
|
|
12
13
|
|
|
13
|
-
//
|
|
14
|
-
|
|
14
|
+
// 设置数据库类型为 MySQL
|
|
15
|
+
setDbType('mysql');
|
|
15
16
|
|
|
16
17
|
// 动态导入以确保环境变量生效
|
|
17
18
|
let isStringOrArrayType: any;
|