befly 2.3.3 → 3.0.1
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/checks/conflict.ts +329 -0
- package/checks/table.ts +252 -0
- package/config/env.ts +218 -0
- package/config/fields.ts +55 -0
- package/config/regexAliases.ts +51 -0
- package/config/reserved.ts +96 -0
- package/main.ts +47 -0
- package/package.json +26 -11
- package/plugins/db.ts +60 -0
- package/plugins/logger.ts +28 -0
- package/plugins/redis.ts +47 -0
- package/scripts/syncDb/apply.ts +171 -0
- package/scripts/syncDb/constants.ts +71 -0
- package/scripts/syncDb/ddl.ts +189 -0
- package/scripts/syncDb/helpers.ts +173 -0
- package/scripts/syncDb/index.ts +203 -0
- package/scripts/syncDb/schema.ts +199 -0
- package/scripts/syncDb/sqlite.ts +50 -0
- package/scripts/syncDb/state.ts +106 -0
- package/scripts/syncDb/table.ts +214 -0
- package/scripts/syncDb/tableCreate.ts +148 -0
- package/scripts/syncDb/tests/constants.test.ts +105 -0
- package/scripts/syncDb/tests/ddl.test.ts +134 -0
- package/scripts/syncDb/tests/helpers.test.ts +70 -0
- package/scripts/syncDb/types.ts +92 -0
- package/scripts/syncDb/version.ts +73 -0
- package/scripts/syncDb.ts +10 -0
- package/tsconfig.json +58 -0
- package/types/addon.d.ts +53 -0
- package/types/api.d.ts +249 -0
- package/types/befly.d.ts +230 -0
- package/types/common.d.ts +215 -0
- package/types/context.d.ts +7 -0
- package/types/crypto.d.ts +23 -0
- package/types/database.d.ts +273 -0
- package/types/index.d.ts +450 -0
- package/types/index.ts +438 -0
- package/types/jwt.d.ts +99 -0
- package/types/logger.d.ts +43 -0
- package/types/plugin.d.ts +109 -0
- package/types/redis.d.ts +46 -0
- package/types/tool.d.ts +67 -0
- package/types/validator.d.ts +43 -0
- package/types/validator.ts +43 -0
- package/utils/colors.ts +221 -0
- package/utils/crypto.ts +308 -0
- package/utils/database.ts +348 -0
- package/utils/dbHelper.ts +713 -0
- package/utils/helper.ts +812 -0
- package/utils/index.ts +33 -0
- package/utils/jwt.ts +493 -0
- package/utils/logger.ts +191 -0
- package/utils/redisHelper.ts +321 -0
- package/utils/requestContext.ts +167 -0
- package/utils/sqlBuilder.ts +611 -0
- package/utils/validate.ts +493 -0
- package/utils/{xml.js → xml.ts} +100 -74
- package/.npmrc +0 -3
- package/.prettierignore +0 -2
- package/.prettierrc +0 -11
- package/apis/health/info.js +0 -49
- package/apis/tool/tokenCheck.js +0 -29
- package/bin/befly.js +0 -109
- package/bunfig.toml +0 -3
- package/checks/table.js +0 -206
- package/config/env.js +0 -64
- package/main.js +0 -579
- package/plugins/db.js +0 -46
- package/plugins/logger.js +0 -14
- package/plugins/redis.js +0 -32
- package/plugins/tool.js +0 -8
- package/scripts/syncDb.js +0 -752
- package/scripts/syncDev.js +0 -96
- package/system.js +0 -118
- package/tables/common.json +0 -16
- package/tables/tool.json +0 -6
- package/utils/api.js +0 -27
- package/utils/colors.js +0 -83
- package/utils/crypto.js +0 -260
- package/utils/index.js +0 -334
- package/utils/jwt.js +0 -387
- package/utils/logger.js +0 -143
- package/utils/redisHelper.js +0 -74
- package/utils/sqlBuilder.js +0 -498
- package/utils/sqlManager.js +0 -471
- package/utils/tool.js +0 -31
- package/utils/validate.js +0 -226
package/scripts/syncDb.js
DELETED
|
@@ -1,752 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 数据库表结构同步脚本 - 支持 sqlite / mysql / postgresql
|
|
3
|
-
* 注意:MySQL 提供更完整的在线 ALTER 能力;SQLite/PG 的修改能力有差异,部分操作将跳过或分解。
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { Env } from '../config/env.js';
|
|
8
|
-
import { Logger } from '../utils/logger.js';
|
|
9
|
-
import { createSqlClient, toSnakeTableName, isType, parseRule } from '../utils/index.js';
|
|
10
|
-
import { __dirtables, getProjectDir } from '../system.js';
|
|
11
|
-
import { checkTable } from '../checks/table.js';
|
|
12
|
-
|
|
13
|
-
// 顶部管理数据库客户端(按需求使用 Bun SQL 模板,不使用 exec 辅助)
|
|
14
|
-
let sql = null;
|
|
15
|
-
|
|
16
|
-
// 方言与类型映射
|
|
17
|
-
const DB = (Env.DB_TYPE || 'mysql').toLowerCase();
|
|
18
|
-
const IS_MYSQL = DB === 'mysql';
|
|
19
|
-
const IS_PG = DB === 'postgresql' || DB === 'postgres';
|
|
20
|
-
const IS_SQLITE = DB === 'sqlite'; // 命令行参数
|
|
21
|
-
const ARGV = Array.isArray(process.argv) ? process.argv : [];
|
|
22
|
-
const IS_PLAN = ARGV.includes('--plan');
|
|
23
|
-
|
|
24
|
-
// 字段类型映射(按方言)
|
|
25
|
-
const typeMapping = {
|
|
26
|
-
number: IS_SQLITE ? 'INTEGER' : 'BIGINT',
|
|
27
|
-
string: IS_SQLITE ? 'TEXT' : IS_PG ? 'character varying' : 'VARCHAR',
|
|
28
|
-
text: IS_MYSQL ? 'MEDIUMTEXT' : 'TEXT',
|
|
29
|
-
array: IS_SQLITE ? 'TEXT' : IS_PG ? 'character varying' : 'VARCHAR'
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// 全局统计
|
|
33
|
-
const globalCount = {
|
|
34
|
-
// 表级
|
|
35
|
-
processedTables: 0,
|
|
36
|
-
createdTables: 0,
|
|
37
|
-
modifiedTables: 0,
|
|
38
|
-
// 字段与索引级
|
|
39
|
-
addFields: 0,
|
|
40
|
-
typeChanges: 0,
|
|
41
|
-
maxChanges: 0, // 映射为长度变化
|
|
42
|
-
minChanges: 0, // 最小值不参与 DDL,比对保留为0
|
|
43
|
-
defaultChanges: 0,
|
|
44
|
-
nameChanges: 0, // 字段显示名(注释)变更
|
|
45
|
-
indexCreate: 0,
|
|
46
|
-
indexDrop: 0
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// PG 兼容类型变更识别:无需数据重写的宽化型变更
|
|
50
|
-
const isPgCompatibleTypeChange = (currentType, newType) => {
|
|
51
|
-
const c = String(currentType || '').toLowerCase();
|
|
52
|
-
const n = String(newType || '').toLowerCase();
|
|
53
|
-
// varchar -> text 视为宽化
|
|
54
|
-
if (c === 'character varying' && n === 'text') return true;
|
|
55
|
-
// text -> character varying 非宽化(可能截断),不兼容
|
|
56
|
-
return false;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// 数据库版本检查(按方言)
|
|
60
|
-
const ensureDbVersion = async () => {
|
|
61
|
-
if (!sql) throw new Error('SQL 客户端未初始化');
|
|
62
|
-
if (IS_MYSQL) {
|
|
63
|
-
const r = await sql`SELECT VERSION() AS version`;
|
|
64
|
-
const version = r[0].version;
|
|
65
|
-
const majorVersion = parseInt(String(version).split('.')[0], 10);
|
|
66
|
-
if (!Number.isFinite(majorVersion) || majorVersion < 8) {
|
|
67
|
-
throw new Error(`此脚本仅支持 MySQL 8.0+,当前版本: ${version}`);
|
|
68
|
-
}
|
|
69
|
-
Logger.info(`MySQL 版本: ${version}`);
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (IS_PG) {
|
|
73
|
-
const r = await sql`SELECT version() AS version`;
|
|
74
|
-
const versionText = r[0].version;
|
|
75
|
-
Logger.info(`PostgreSQL 版本: ${versionText}`);
|
|
76
|
-
const m = /PostgreSQL\s+(\d+)/i.exec(versionText);
|
|
77
|
-
const major = m ? parseInt(m[1], 10) : NaN;
|
|
78
|
-
if (!Number.isFinite(major) || major < 17) {
|
|
79
|
-
throw new Error(`此脚本要求 PostgreSQL >= 17,当前: ${versionText}`);
|
|
80
|
-
}
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
if (IS_SQLITE) {
|
|
84
|
-
const r = await sql`SELECT sqlite_version() AS version`;
|
|
85
|
-
const version = r[0].version;
|
|
86
|
-
Logger.info(`SQLite 版本: ${version}`);
|
|
87
|
-
// 强制最低版本:SQLite ≥ 3.50.0
|
|
88
|
-
const [maj, min, patch] = String(version)
|
|
89
|
-
.split('.')
|
|
90
|
-
.map((v) => parseInt(v, 10) || 0);
|
|
91
|
-
const vnum = maj * 10000 + min * 100 + patch; // 3.50.0 -> 35000
|
|
92
|
-
if (!Number.isFinite(vnum) || vnum < 35000) {
|
|
93
|
-
throw new Error(`此脚本要求 SQLite >= 3.50.0,当前: ${version}`);
|
|
94
|
-
}
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// 判断表是否存在(返回布尔值)
|
|
100
|
-
const tableExists = async (tableName) => {
|
|
101
|
-
if (!sql) throw new Error('SQL 客户端未初始化');
|
|
102
|
-
if (IS_MYSQL) {
|
|
103
|
-
const res = await sql`SELECT COUNT(*) AS count FROM information_schema.TABLES WHERE TABLE_SCHEMA = ${Env.DB_NAME} AND TABLE_NAME = ${tableName}`;
|
|
104
|
-
return (res[0]?.count || 0) > 0;
|
|
105
|
-
}
|
|
106
|
-
if (IS_PG) {
|
|
107
|
-
const res = await sql`SELECT COUNT(*)::int AS count FROM information_schema.tables WHERE table_schema = 'public' AND table_name = ${tableName}`;
|
|
108
|
-
return (res[0]?.count || 0) > 0;
|
|
109
|
-
}
|
|
110
|
-
if (IS_SQLITE) {
|
|
111
|
-
const res = await sql`SELECT name FROM sqlite_master WHERE type='table' AND name = ${tableName}`;
|
|
112
|
-
return res.length > 0;
|
|
113
|
-
}
|
|
114
|
-
return false;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// 获取表的现有列信息(按方言)
|
|
118
|
-
const getTableColumns = async (tableName) => {
|
|
119
|
-
const columns = {};
|
|
120
|
-
if (IS_MYSQL) {
|
|
121
|
-
const result = await sql`
|
|
122
|
-
SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, IS_NULLABLE, COLUMN_DEFAULT, COLUMN_COMMENT, COLUMN_TYPE
|
|
123
|
-
FROM information_schema.COLUMNS
|
|
124
|
-
WHERE TABLE_SCHEMA = ${Env.DB_NAME} AND TABLE_NAME = ${tableName}
|
|
125
|
-
ORDER BY ORDINAL_POSITION
|
|
126
|
-
`;
|
|
127
|
-
for (const row of result) {
|
|
128
|
-
columns[row.COLUMN_NAME] = {
|
|
129
|
-
type: row.DATA_TYPE,
|
|
130
|
-
columnType: row.COLUMN_TYPE,
|
|
131
|
-
length: row.CHARACTER_MAXIMUM_LENGTH,
|
|
132
|
-
nullable: row.IS_NULLABLE === 'YES',
|
|
133
|
-
defaultValue: row.COLUMN_DEFAULT,
|
|
134
|
-
comment: row.COLUMN_COMMENT
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
} else if (IS_PG) {
|
|
138
|
-
const result = await sql`SELECT column_name, data_type, character_maximum_length, is_nullable, column_default
|
|
139
|
-
FROM information_schema.columns
|
|
140
|
-
WHERE table_schema = 'public' AND table_name = ${tableName}
|
|
141
|
-
ORDER BY ordinal_position`;
|
|
142
|
-
// 获取列注释
|
|
143
|
-
const comments = await sql`SELECT a.attname AS column_name, col_description(c.oid, a.attnum) AS column_comment
|
|
144
|
-
FROM pg_class c
|
|
145
|
-
JOIN pg_attribute a ON a.attrelid = c.oid
|
|
146
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
147
|
-
WHERE c.relkind = 'r' AND n.nspname = 'public' AND c.relname = ${tableName} AND a.attnum > 0`;
|
|
148
|
-
const commentMap = {};
|
|
149
|
-
for (const r of comments) commentMap[r.column_name] = r.column_comment;
|
|
150
|
-
for (const row of result) {
|
|
151
|
-
columns[row.column_name] = {
|
|
152
|
-
type: row.data_type,
|
|
153
|
-
columnType: row.data_type,
|
|
154
|
-
length: row.character_maximum_length,
|
|
155
|
-
nullable: String(row.is_nullable).toUpperCase() === 'YES',
|
|
156
|
-
defaultValue: row.column_default,
|
|
157
|
-
comment: commentMap[row.column_name] ?? null
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
} else if (IS_SQLITE) {
|
|
161
|
-
const result = await sql`PRAGMA table_info(${sql(tableName)})`;
|
|
162
|
-
for (const row of result) {
|
|
163
|
-
let baseType = String(row.type || '').toUpperCase();
|
|
164
|
-
let length = null;
|
|
165
|
-
const m = /^(\w+)\s*\((\d+)\)/.exec(baseType);
|
|
166
|
-
if (m) {
|
|
167
|
-
baseType = m[1];
|
|
168
|
-
length = Number(m[2]);
|
|
169
|
-
}
|
|
170
|
-
columns[row.name] = {
|
|
171
|
-
type: baseType.toLowerCase(),
|
|
172
|
-
columnType: baseType.toLowerCase(),
|
|
173
|
-
length: length,
|
|
174
|
-
nullable: row.notnull === 0,
|
|
175
|
-
defaultValue: row.dflt_value,
|
|
176
|
-
comment: null
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return columns;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
// 获取表的现有索引信息(单列索引)
|
|
184
|
-
const getTableIndexes = async (tableName) => {
|
|
185
|
-
const indexes = {};
|
|
186
|
-
if (IS_MYSQL) {
|
|
187
|
-
const result = await sql`
|
|
188
|
-
SELECT INDEX_NAME, COLUMN_NAME
|
|
189
|
-
FROM information_schema.STATISTICS
|
|
190
|
-
WHERE TABLE_SCHEMA = ${Env.DB_NAME}
|
|
191
|
-
AND TABLE_NAME = ${tableName}
|
|
192
|
-
AND INDEX_NAME != 'PRIMARY'
|
|
193
|
-
ORDER BY INDEX_NAME
|
|
194
|
-
`;
|
|
195
|
-
for (const row of result) {
|
|
196
|
-
if (!indexes[row.INDEX_NAME]) indexes[row.INDEX_NAME] = [];
|
|
197
|
-
indexes[row.INDEX_NAME].push(row.COLUMN_NAME);
|
|
198
|
-
}
|
|
199
|
-
} else if (IS_PG) {
|
|
200
|
-
const result = await sql`SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' AND tablename = ${tableName}`;
|
|
201
|
-
for (const row of result) {
|
|
202
|
-
const m = /\(([^)]+)\)/.exec(row.indexdef);
|
|
203
|
-
if (m) {
|
|
204
|
-
const col = m[1].replace(/\"/g, '').replace(/"/g, '').trim();
|
|
205
|
-
indexes[row.indexname] = [col];
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
} else if (IS_SQLITE) {
|
|
209
|
-
const list = await sql`PRAGMA index_list(${sql(tableName)})`;
|
|
210
|
-
for (const idx of list) {
|
|
211
|
-
const info = await sql`PRAGMA index_info(${sql(idx.name)})`;
|
|
212
|
-
const cols = info.map((r) => r.name);
|
|
213
|
-
if (cols.length === 1) indexes[idx.name] = cols;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
return indexes;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
// 构建索引操作 SQL(统一使用在线策略)
|
|
220
|
-
const buildIndexSQL = (tableName, indexName, fieldName, action) => {
|
|
221
|
-
if (IS_MYSQL) {
|
|
222
|
-
const parts = [];
|
|
223
|
-
action === 'create' ? parts.push(`ADD INDEX \`${indexName}\` (\`${fieldName}\`)`) : parts.push(`DROP INDEX \`${indexName}\``);
|
|
224
|
-
// 始终使用在线算法
|
|
225
|
-
parts.push('ALGORITHM=INPLACE');
|
|
226
|
-
parts.push('LOCK=NONE');
|
|
227
|
-
return `ALTER TABLE \`${tableName}\` ${parts.join(', ')}`;
|
|
228
|
-
}
|
|
229
|
-
if (IS_PG) {
|
|
230
|
-
if (action === 'create') {
|
|
231
|
-
// 始终使用 CONCURRENTLY
|
|
232
|
-
return `CREATE INDEX CONCURRENTLY IF NOT EXISTS "${indexName}" ON "${tableName}"("${fieldName}")`;
|
|
233
|
-
}
|
|
234
|
-
return `DROP INDEX CONCURRENTLY IF EXISTS "${indexName}"`;
|
|
235
|
-
}
|
|
236
|
-
// SQLite
|
|
237
|
-
if (action === 'create') return `CREATE INDEX IF NOT EXISTS "${indexName}" ON "${tableName}"("${fieldName}")`;
|
|
238
|
-
return `DROP INDEX IF EXISTS "${indexName}"`;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
// 创建表(尽量精简但保持既有行为)
|
|
242
|
-
const createTable = async (tableName, fields) => {
|
|
243
|
-
// 统一列定义数组:包含系统字段与业务字段
|
|
244
|
-
const colDefs = [];
|
|
245
|
-
|
|
246
|
-
// 1) 固定字段
|
|
247
|
-
if (IS_MYSQL) {
|
|
248
|
-
colDefs.push('`id` BIGINT PRIMARY KEY COMMENT "主键ID"');
|
|
249
|
-
colDefs.push('`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT "创建时间"');
|
|
250
|
-
colDefs.push('`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT "更新时间"');
|
|
251
|
-
colDefs.push('`deleted_at` BIGINT NOT NULL DEFAULT 0 COMMENT "删除时间"');
|
|
252
|
-
colDefs.push('`state` BIGINT NOT NULL DEFAULT 0 COMMENT "状态字段"');
|
|
253
|
-
} else {
|
|
254
|
-
colDefs.push('"id" INTEGER PRIMARY KEY');
|
|
255
|
-
colDefs.push('"created_at" INTEGER NOT NULL DEFAULT 0');
|
|
256
|
-
colDefs.push('"updated_at" INTEGER NOT NULL DEFAULT 0');
|
|
257
|
-
colDefs.push('"deleted_at" INTEGER NOT NULL DEFAULT 0');
|
|
258
|
-
colDefs.push('"state" INTEGER NOT NULL DEFAULT 0');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// 2) 业务字段
|
|
262
|
-
for (const [fieldKey, fieldRule] of Object.entries(fields)) {
|
|
263
|
-
const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = parseRule(fieldRule);
|
|
264
|
-
const sqlType = ['string', 'array'].includes(fieldType) ? `${typeMapping[fieldType]}(${fieldMax})` : typeMapping[fieldType];
|
|
265
|
-
const defaultSql = ['number', 'string', 'array'].includes(fieldType) ? (isType(fieldDefault, 'number') ? ` DEFAULT ${fieldDefault}` : ` DEFAULT '${fieldDefault}'`) : '';
|
|
266
|
-
if (IS_MYSQL) {
|
|
267
|
-
colDefs.push(`\`${fieldKey}\` ${sqlType} NOT NULL${defaultSql} COMMENT "${String(fieldName).replace(/"/g, '\\"')}"`);
|
|
268
|
-
} else {
|
|
269
|
-
colDefs.push(`"${fieldKey}" ${sqlType} NOT NULL${defaultSql}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// 3) CREATE TABLE 语句
|
|
274
|
-
const cols = colDefs.join(',\n ');
|
|
275
|
-
let createSQL;
|
|
276
|
-
if (IS_MYSQL) {
|
|
277
|
-
createSQL = `CREATE TABLE \`${tableName}\` (\n ${cols}\n ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_as_cs`;
|
|
278
|
-
} else {
|
|
279
|
-
createSQL = `CREATE TABLE "${tableName}" (\n ${cols}\n )`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (IS_PLAN) {
|
|
283
|
-
Logger.info(`[计划] ${createSQL.replace(/\n+/g, ' ')}`);
|
|
284
|
-
} else {
|
|
285
|
-
await sql.unsafe(createSQL);
|
|
286
|
-
Logger.info(`[新建表] ${tableName}`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// 4) PG: 列注释(SQLite 不支持;MySQL 已在列定义中)
|
|
290
|
-
if (IS_PG) {
|
|
291
|
-
const commentPairs = [
|
|
292
|
-
['id', '主键ID'],
|
|
293
|
-
['created_at', '创建时间'],
|
|
294
|
-
['updated_at', '更新时间'],
|
|
295
|
-
['deleted_at', '删除时间'],
|
|
296
|
-
['state', '状态字段']
|
|
297
|
-
];
|
|
298
|
-
for (const [name, cmt] of commentPairs) {
|
|
299
|
-
const stmt = `COMMENT ON COLUMN "${tableName}"."${name}" IS '${cmt}'`;
|
|
300
|
-
if (IS_PLAN) {
|
|
301
|
-
Logger.info(`[计划] ${stmt}`);
|
|
302
|
-
} else {
|
|
303
|
-
await sql.unsafe(stmt);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
for (const [fieldKey, fieldRule] of Object.entries(fields)) {
|
|
307
|
-
const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = parseRule(fieldRule);
|
|
308
|
-
const stmt = `COMMENT ON COLUMN "${tableName}"."${fieldKey}" IS '${fieldName}'`;
|
|
309
|
-
if (IS_PLAN) {
|
|
310
|
-
Logger.info(`[计划] ${stmt}`);
|
|
311
|
-
} else {
|
|
312
|
-
await sql.unsafe(stmt);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// 5) 索引:系统字段 + 业务字段(按规则)
|
|
318
|
-
for (const sysField of ['created_at', 'updated_at', 'state']) {
|
|
319
|
-
const stmt = buildIndexSQL(tableName, `idx_${sysField}`, sysField, 'create');
|
|
320
|
-
if (IS_PLAN) {
|
|
321
|
-
Logger.info(`[计划] ${stmt}`);
|
|
322
|
-
} else {
|
|
323
|
-
await sql.unsafe(stmt);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
for (const [fieldKey, fieldRule] of Object.entries(fields)) {
|
|
327
|
-
const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = parseRule(fieldRule);
|
|
328
|
-
if (fieldIndex === 1) {
|
|
329
|
-
const stmt = buildIndexSQL(tableName, `idx_${fieldKey}`, fieldKey, 'create');
|
|
330
|
-
if (IS_PLAN) {
|
|
331
|
-
Logger.info(`[计划] ${stmt}`);
|
|
332
|
-
} else {
|
|
333
|
-
await sql.unsafe(stmt);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
// 比较字段定义变化
|
|
340
|
-
const compareFieldDefinition = (existingColumn, newRule, colName) => {
|
|
341
|
-
const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = parseRule(newRule);
|
|
342
|
-
const changes = [];
|
|
343
|
-
|
|
344
|
-
// 检查长度变化(string和array类型) - SQLite 不比较长度
|
|
345
|
-
if (!IS_SQLITE && (fieldType === 'string' || fieldType === 'array')) {
|
|
346
|
-
if (existingColumn.length !== fieldMax) {
|
|
347
|
-
changes.push({
|
|
348
|
-
type: 'length',
|
|
349
|
-
current: existingColumn.length,
|
|
350
|
-
new: fieldMax
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// 检查注释变化(MySQL/PG 支持列注释)
|
|
356
|
-
if (!IS_SQLITE) {
|
|
357
|
-
const currentComment = existingColumn.comment || '';
|
|
358
|
-
if (currentComment !== fieldName) {
|
|
359
|
-
changes.push({
|
|
360
|
-
type: 'comment',
|
|
361
|
-
current: currentComment,
|
|
362
|
-
new: fieldName
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// 检查数据类型变化(按方言)
|
|
368
|
-
if (existingColumn.type.toLowerCase() !== typeMapping[fieldType].toLowerCase()) {
|
|
369
|
-
changes.push({
|
|
370
|
-
type: 'datatype',
|
|
371
|
-
current: existingColumn.type,
|
|
372
|
-
new: typeMapping[fieldType].toLowerCase()
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// 检查默认值变化(按照生成规则推导期望默认值)
|
|
377
|
-
if (String(existingColumn.defaultValue) !== String(fieldDefault)) {
|
|
378
|
-
changes.push({
|
|
379
|
-
type: 'default',
|
|
380
|
-
current: existingColumn.defaultValue,
|
|
381
|
-
new: fieldDefault
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return changes;
|
|
386
|
-
};
|
|
387
|
-
|
|
388
|
-
// 生成字段 DDL 子句(不含 ALTER TABLE 前缀)
|
|
389
|
-
const generateDDLClause = (fieldKey, fieldRule, isAdd = false) => {
|
|
390
|
-
const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = parseRule(fieldRule);
|
|
391
|
-
const sqlType = ['string', 'array'].includes(fieldType) ? `${typeMapping[fieldType]}(${fieldMax})` : typeMapping[fieldType];
|
|
392
|
-
const defaultSql = ['number', 'string', 'array'].includes(fieldType) ? (isType(fieldDefault, 'number') ? ` DEFAULT ${fieldDefault}` : ` DEFAULT '${fieldDefault}'`) : '';
|
|
393
|
-
if (IS_MYSQL) {
|
|
394
|
-
return `${isAdd ? 'ADD COLUMN' : 'MODIFY COLUMN'} \`${fieldKey}\` ${sqlType} NOT NULL${defaultSql} COMMENT "${String(fieldName).replace(/"/g, '\\"')}"`;
|
|
395
|
-
}
|
|
396
|
-
if (IS_PG) {
|
|
397
|
-
if (isAdd) return `ADD COLUMN IF NOT EXISTS "${fieldKey}" ${sqlType} NOT NULL${defaultSql}`;
|
|
398
|
-
// PG 修改:类型与非空可分条执行,生成 TYPE 改变;非空另由上层统一控制
|
|
399
|
-
return `ALTER COLUMN "${fieldKey}" TYPE ${sqlType}`;
|
|
400
|
-
}
|
|
401
|
-
// SQLite 仅支持 ADD COLUMN(>=3.50.0:支持 IF NOT EXISTS)
|
|
402
|
-
if (isAdd) return `ADD COLUMN IF NOT EXISTS "${fieldKey}" ${sqlType} NOT NULL${defaultSql}`;
|
|
403
|
-
return '';
|
|
404
|
-
};
|
|
405
|
-
|
|
406
|
-
// 安全执行DDL语句
|
|
407
|
-
const executeDDLSafely = async (stmt) => {
|
|
408
|
-
try {
|
|
409
|
-
await sql.unsafe(stmt);
|
|
410
|
-
return true;
|
|
411
|
-
} catch (error) {
|
|
412
|
-
// MySQL 专用降级路径
|
|
413
|
-
if (stmt.includes('ALGORITHM=INSTANT')) {
|
|
414
|
-
const inplaceSql = stmt.replace(/ALGORITHM=INSTANT/g, 'ALGORITHM=INPLACE');
|
|
415
|
-
try {
|
|
416
|
-
await sql.unsafe(inplaceSql);
|
|
417
|
-
return true;
|
|
418
|
-
} catch (inplaceError) {
|
|
419
|
-
// 最后尝试传统DDL:移除 ALGORITHM/LOCK 附加子句后执行
|
|
420
|
-
const traditionSql = stmt
|
|
421
|
-
.replace(/,\s*ALGORITHM=INPLACE/g, '')
|
|
422
|
-
.replace(/,\s*ALGORITHM=INSTANT/g, '')
|
|
423
|
-
.replace(/,\s*LOCK=(NONE|SHARED|EXCLUSIVE)/g, '');
|
|
424
|
-
await sql.unsafe(traditionSql);
|
|
425
|
-
return true;
|
|
426
|
-
}
|
|
427
|
-
} else {
|
|
428
|
-
throw error;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
// SQLite 重建表迁移(简化版:仅处理新增/修改字段,不处理复杂约束与复合索引)
|
|
434
|
-
const rebuildSqliteTable = async (tableName, fields) => {
|
|
435
|
-
// 1. 读取现有列顺序
|
|
436
|
-
const info = await sql`PRAGMA table_info(${sql(tableName)})`;
|
|
437
|
-
const existingCols = info.map((r) => r.name);
|
|
438
|
-
const targetCols = ['id', 'created_at', 'updated_at', 'deleted_at', 'state', ...Object.keys(fields)];
|
|
439
|
-
const tmpTable = `${tableName}__tmp__${Date.now()}`;
|
|
440
|
-
|
|
441
|
-
// 2. 创建新表(使用当前定义)
|
|
442
|
-
await createTable(tmpTable, fields);
|
|
443
|
-
|
|
444
|
-
// 3. 拷贝数据(按交集列)
|
|
445
|
-
const commonCols = targetCols.filter((c) => existingCols.includes(c));
|
|
446
|
-
if (commonCols.length > 0) {
|
|
447
|
-
const colsSql = commonCols.map((c) => `"${c}"`).join(', ');
|
|
448
|
-
await sql.unsafe(`INSERT INTO "${tmpTable}" (${colsSql}) SELECT ${colsSql} FROM "${tableName}"`);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// 4. 删除旧表并重命名
|
|
452
|
-
await sql.unsafe(`DROP TABLE "${tableName}"`);
|
|
453
|
-
await sql.unsafe(`ALTER TABLE "${tmpTable}" RENAME TO "${tableName}"`);
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
// 将表结构计划应用到数据库(执行 DDL/索引/注释等)
|
|
457
|
-
const applyTablePlan = async (tableName, fields, plan) => {
|
|
458
|
-
if (!plan || !plan.changed) return;
|
|
459
|
-
|
|
460
|
-
// SQLite: 仅支持部分 ALTER;需要时走重建
|
|
461
|
-
if (IS_SQLITE) {
|
|
462
|
-
if (plan.modifyClauses.length > 0 || plan.defaultClauses.length > 0) {
|
|
463
|
-
if (IS_PLAN) Logger.info(`[计划] 重建表 ${tableName} 以应用列修改/默认值变化`);
|
|
464
|
-
else await rebuildSqliteTable(tableName, fields);
|
|
465
|
-
} else {
|
|
466
|
-
for (const c of plan.addClauses) {
|
|
467
|
-
const stmt = `ALTER TABLE "${tableName}" ${c}`;
|
|
468
|
-
if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
|
|
469
|
-
else await sql.unsafe(stmt);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
} else {
|
|
473
|
-
const clauses = [...plan.addClauses, ...plan.modifyClauses];
|
|
474
|
-
if (clauses.length > 0) {
|
|
475
|
-
const suffix = IS_MYSQL ? ', ALGORITHM=INSTANT, LOCK=NONE' : '';
|
|
476
|
-
const stmt = IS_MYSQL ? `ALTER TABLE \`${tableName}\` ${clauses.join(', ')}${suffix}` : `ALTER TABLE "${tableName}" ${clauses.join(', ')}`;
|
|
477
|
-
if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
|
|
478
|
-
else if (IS_MYSQL) await executeDDLSafely(stmt);
|
|
479
|
-
else await sql.unsafe(stmt);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// 默认值专用 ALTER(SQLite 不支持)
|
|
484
|
-
if (plan.defaultClauses.length > 0) {
|
|
485
|
-
if (IS_SQLITE) {
|
|
486
|
-
Logger.warn(`SQLite 不支持修改默认值,表 ${tableName} 的默认值变更已跳过`);
|
|
487
|
-
} else {
|
|
488
|
-
const suffix = IS_MYSQL ? ', ALGORITHM=INSTANT, LOCK=NONE' : '';
|
|
489
|
-
const stmt = IS_MYSQL ? `ALTER TABLE \`${tableName}\` ${plan.defaultClauses.join(', ')}${suffix}` : `ALTER TABLE "${tableName}" ${plan.defaultClauses.join(', ')}`;
|
|
490
|
-
if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
|
|
491
|
-
else if (IS_MYSQL) await executeDDLSafely(stmt);
|
|
492
|
-
else await sql.unsafe(stmt);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// 索引操作
|
|
497
|
-
for (const act of plan.indexActions) {
|
|
498
|
-
const stmt = buildIndexSQL(tableName, act.indexName, act.fieldName, act.action);
|
|
499
|
-
if (IS_PLAN) {
|
|
500
|
-
Logger.info(`[计划] ${stmt}`);
|
|
501
|
-
} else {
|
|
502
|
-
try {
|
|
503
|
-
await sql.unsafe(stmt);
|
|
504
|
-
if (act.action === 'create') {
|
|
505
|
-
Logger.info(`[索引变化] 新建索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
506
|
-
} else {
|
|
507
|
-
Logger.info(`[索引变化] 删除索引 ${tableName}.${act.indexName} (${act.fieldName})`);
|
|
508
|
-
}
|
|
509
|
-
} catch (error) {
|
|
510
|
-
Logger.error(`${act.action === 'create' ? '创建' : '删除'}索引失败: ${error.message}`);
|
|
511
|
-
throw error;
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// PG 列注释
|
|
517
|
-
if (IS_PG && plan.commentActions && plan.commentActions.length > 0) {
|
|
518
|
-
for (const stmt of plan.commentActions) {
|
|
519
|
-
if (IS_PLAN) Logger.info(`[计划] ${stmt}`);
|
|
520
|
-
else await sql.unsafe(stmt);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// 计数
|
|
525
|
-
globalCount.modifiedTables++;
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
// 同步表结构
|
|
529
|
-
const modifyTable = async (tableName, fields) => {
|
|
530
|
-
const existingColumns = await getTableColumns(tableName);
|
|
531
|
-
const existingIndexes = await getTableIndexes(tableName);
|
|
532
|
-
let changed = false;
|
|
533
|
-
|
|
534
|
-
const addClauses = [];
|
|
535
|
-
const modifyClauses = [];
|
|
536
|
-
const defaultClauses = [];
|
|
537
|
-
const indexActions = [];
|
|
538
|
-
|
|
539
|
-
for (const [fieldKey, fieldRule] of Object.entries(fields)) {
|
|
540
|
-
if (existingColumns[fieldKey]) {
|
|
541
|
-
const comparison = compareFieldDefinition(existingColumns[fieldKey], fieldRule, fieldKey);
|
|
542
|
-
if (comparison.length > 0) {
|
|
543
|
-
for (const c of comparison) {
|
|
544
|
-
const label =
|
|
545
|
-
{
|
|
546
|
-
length: '长度',
|
|
547
|
-
datatype: '类型',
|
|
548
|
-
comment: '注释',
|
|
549
|
-
default: '默认值'
|
|
550
|
-
}[c.type] || c.type;
|
|
551
|
-
Logger.info(`[字段变更] ${tableName}.${fieldKey} ${label}: ${c.current ?? 'NULL'} -> ${c.new ?? 'NULL'}`);
|
|
552
|
-
// 全量计数:全局累加
|
|
553
|
-
if (c.type === 'datatype') globalCount.typeChanges++;
|
|
554
|
-
else if (c.type === 'length') globalCount.maxChanges++;
|
|
555
|
-
else if (c.type === 'default') globalCount.defaultChanges++;
|
|
556
|
-
else if (c.type === 'comment') globalCount.nameChanges++;
|
|
557
|
-
}
|
|
558
|
-
const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = parseRule(fieldRule);
|
|
559
|
-
if ((fieldType === 'string' || fieldType === 'array') && existingColumns[fieldKey].length) {
|
|
560
|
-
if (existingColumns[fieldKey].length > fieldMax) {
|
|
561
|
-
Logger.warn(`[跳过危险变更] ${tableName}.${fieldKey} 长度收缩 ${existingColumns[fieldKey].length} -> ${fieldMax} 已被跳过(设置 SYNC_DISALLOW_SHRINK=0 可放开)`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
const hasTypeChange = comparison.some((c) => c.type === 'datatype');
|
|
565
|
-
const hasLengthChange = comparison.some((c) => c.type === 'length');
|
|
566
|
-
const onlyDefaultChanged = comparison.every((c) => c.type === 'default');
|
|
567
|
-
const defaultChanged = comparison.some((c) => c.type === 'default');
|
|
568
|
-
|
|
569
|
-
// 严格限制:除 string/array 互转外,禁止任何字段类型变更;一旦发现,立即终止同步
|
|
570
|
-
// 说明:string 与 array 在各方言下映射同为 VARCHAR/character varying/TEXT,compare 不会将其视为类型变更
|
|
571
|
-
if (hasTypeChange) {
|
|
572
|
-
const currentSqlType = String(existingColumns[fieldKey].type || '').toLowerCase();
|
|
573
|
-
const newSqlType = String(typeMapping[fieldType] || '').toLowerCase();
|
|
574
|
-
// 明确抛错,阻止后续任何 DDL 应用
|
|
575
|
-
throw new Error(`禁止字段类型变更: ${tableName}.${fieldKey} ${currentSqlType} -> ${newSqlType}。仅允许 string<->array 互相切换`);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
// 默认值变化处理:
|
|
579
|
-
if (defaultChanged) {
|
|
580
|
-
const v = fieldType === 'number' ? fieldDefault : `'${fieldDefault}'`;
|
|
581
|
-
if (IS_PG) {
|
|
582
|
-
defaultClauses.push(`ALTER COLUMN "${fieldKey}" SET DEFAULT ${v}`);
|
|
583
|
-
} else if (IS_MYSQL && onlyDefaultChanged) {
|
|
584
|
-
// MySQL 的 TEXT/BLOB 不允许 DEFAULT,跳过 text 类型
|
|
585
|
-
if (fieldType !== 'text') {
|
|
586
|
-
defaultClauses.push(`ALTER COLUMN \`${fieldKey}\` SET DEFAULT ${v}`);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// 若不仅仅是默认值变化,继续生成修改子句
|
|
592
|
-
if (!onlyDefaultChanged) {
|
|
593
|
-
let skipModify = false;
|
|
594
|
-
if (hasLengthChange && (fieldType === 'string' || fieldType === 'array') && existingColumns[fieldKey].length) {
|
|
595
|
-
const oldLen = existingColumns[fieldKey].length;
|
|
596
|
-
const isShrink = oldLen > fieldMax;
|
|
597
|
-
if (isShrink) skipModify = true;
|
|
598
|
-
}
|
|
599
|
-
if (hasTypeChange) {
|
|
600
|
-
if (IS_PG && isPgCompatibleTypeChange(existingColumns[fieldKey].type, typeMapping[fieldType].toLowerCase())) {
|
|
601
|
-
Logger.info(`[PG兼容类型变更] ${tableName}.${fieldKey} ${existingColumns[fieldKey].type} -> ${typeMapping[fieldType].toLowerCase()} 允许执行`);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
if (!skipModify) modifyClauses.push(generateDDLClause(fieldKey, fieldRule, false));
|
|
605
|
-
}
|
|
606
|
-
changed = true;
|
|
607
|
-
}
|
|
608
|
-
} else {
|
|
609
|
-
const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = parseRule(fieldRule);
|
|
610
|
-
const lenPart = fieldType === 'string' || fieldType === 'array' ? ` 长度:${parseInt(fieldMax)}` : '';
|
|
611
|
-
Logger.info(`[新增字段] ${tableName}.${fieldKey} 类型:${fieldType}${lenPart} 默认:${fieldDefault ?? 'NULL'}`);
|
|
612
|
-
addClauses.push(generateDDLClause(fieldKey, fieldRule, true));
|
|
613
|
-
changed = true;
|
|
614
|
-
globalCount.addFields++;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
for (const sysField of ['created_at', 'updated_at', 'state']) {
|
|
619
|
-
const idxName = `idx_${sysField}`;
|
|
620
|
-
if (!existingIndexes[idxName]) {
|
|
621
|
-
indexActions.push({ action: 'create', indexName: idxName, fieldName: sysField });
|
|
622
|
-
changed = true;
|
|
623
|
-
globalCount.indexCreate++;
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
for (const [fieldKey, fieldRule] of Object.entries(fields)) {
|
|
627
|
-
const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = parseRule(fieldRule);
|
|
628
|
-
const indexName = `idx_${fieldKey}`;
|
|
629
|
-
if (fieldIndex === 1 && !existingIndexes[indexName]) {
|
|
630
|
-
indexActions.push({ action: 'create', indexName, fieldName: fieldKey });
|
|
631
|
-
changed = true;
|
|
632
|
-
globalCount.indexCreate++;
|
|
633
|
-
} else if (!(fieldIndex === 1) && existingIndexes[indexName] && existingIndexes[indexName].length === 1) {
|
|
634
|
-
indexActions.push({ action: 'drop', indexName, fieldName: fieldKey });
|
|
635
|
-
changed = true;
|
|
636
|
-
globalCount.indexDrop++;
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const commentActions = [];
|
|
641
|
-
if (IS_PG) {
|
|
642
|
-
for (const [fieldKey, fieldRule] of Object.entries(fields)) {
|
|
643
|
-
if (existingColumns[fieldKey]) {
|
|
644
|
-
const [fieldName, fieldType, fieldMin, fieldMax, fieldDefault, fieldIndex, fieldRegx] = parseRule(fieldRule);
|
|
645
|
-
const curr = existingColumns[fieldKey].comment || '';
|
|
646
|
-
const want = fieldName && fieldName !== 'null' ? String(fieldName) : '';
|
|
647
|
-
if (want !== curr) {
|
|
648
|
-
commentActions.push(`COMMENT ON COLUMN "${tableName}"."${fieldKey}" IS ${want ? `'${want}'` : 'NULL'}`);
|
|
649
|
-
changed = true;
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
// 仅当存在实际动作时才认为有变更(避免仅日志的收缩跳过被计为修改)
|
|
655
|
-
changed = addClauses.length > 0 || modifyClauses.length > 0 || defaultClauses.length > 0 || indexActions.length > 0 || commentActions.length > 0;
|
|
656
|
-
const plan = { changed, addClauses, modifyClauses, defaultClauses, indexActions, commentActions };
|
|
657
|
-
// 将计划应用(包含 --plan 情况下仅输出)
|
|
658
|
-
if (plan.changed) {
|
|
659
|
-
await applyTablePlan(tableName, fields, plan);
|
|
660
|
-
}
|
|
661
|
-
return plan;
|
|
662
|
-
};
|
|
663
|
-
|
|
664
|
-
// 主同步函数
|
|
665
|
-
const SyncDb = async () => {
|
|
666
|
-
try {
|
|
667
|
-
Logger.info('开始数据库表结构同步...');
|
|
668
|
-
// 重置全局统计,避免多次调用累加
|
|
669
|
-
for (const k of Object.keys(globalCount)) {
|
|
670
|
-
if (typeof globalCount[k] === 'number') globalCount[k] = 0;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
// 验证表定义文件
|
|
674
|
-
if (!(await checkTable())) {
|
|
675
|
-
throw new Error('表定义验证失败');
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// 建立数据库连接并检查版本(按方言)
|
|
679
|
-
// 在顶层也保留 sql 引用,便于未来需要跨函数访问
|
|
680
|
-
sql = await createSqlClient({ max: 1 });
|
|
681
|
-
await ensureDbVersion();
|
|
682
|
-
|
|
683
|
-
// 扫描并处理表文件
|
|
684
|
-
const tablesGlob = new Bun.Glob('*.json');
|
|
685
|
-
const directories = [__dirtables, getProjectDir('tables')];
|
|
686
|
-
// 统计使用全局 globalCount
|
|
687
|
-
|
|
688
|
-
for (const dir of directories) {
|
|
689
|
-
for await (const file of tablesGlob.scan({ cwd: dir, absolute: true, onlyFiles: true })) {
|
|
690
|
-
const tableName = toSnakeTableName(path.basename(file, '.json'));
|
|
691
|
-
const tableDefinition = await Bun.file(file).json();
|
|
692
|
-
const existsTable = await tableExists(tableName);
|
|
693
|
-
|
|
694
|
-
if (existsTable) {
|
|
695
|
-
await modifyTable(tableName, tableDefinition);
|
|
696
|
-
} else {
|
|
697
|
-
await createTable(tableName, tableDefinition);
|
|
698
|
-
globalCount.createdTables++;
|
|
699
|
-
}
|
|
700
|
-
globalCount.processedTables++;
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// 显示统计信息(扩展维度)
|
|
705
|
-
Logger.info(`统计 - 处理表总数: ${globalCount.processedTables}`);
|
|
706
|
-
Logger.info(`统计 - 创建表: ${globalCount.createdTables}`);
|
|
707
|
-
Logger.info(`统计 - 修改表: ${globalCount.modifiedTables}`);
|
|
708
|
-
Logger.info(`统计 - 字段新增: ${globalCount.addFields}`);
|
|
709
|
-
Logger.info(`统计 - 字段名称变更: ${globalCount.nameChanges}`);
|
|
710
|
-
Logger.info(`统计 - 字段类型变更: ${globalCount.typeChanges}`);
|
|
711
|
-
Logger.info(`统计 - 字段最小值变更: ${globalCount.minChanges}`);
|
|
712
|
-
Logger.info(`统计 - 字段最大值变更: ${globalCount.maxChanges}`);
|
|
713
|
-
Logger.info(`统计 - 字段默认值变更: ${globalCount.defaultChanges}`);
|
|
714
|
-
// 索引新增/删除分别打印
|
|
715
|
-
Logger.info(`统计 - 索引新增: ${globalCount.indexCreate}`);
|
|
716
|
-
Logger.info(`统计 - 索引删除: ${globalCount.indexDrop}`);
|
|
717
|
-
|
|
718
|
-
if (globalCount.processedTables === 0) {
|
|
719
|
-
Logger.warn('没有找到任何表定义文件');
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// 保持单一职责:此处不再触发开发管理员同步
|
|
723
|
-
} catch (error) {
|
|
724
|
-
Logger.error(`数据库同步失败: ${error.message}`);
|
|
725
|
-
Logger.error(`错误详情: ${error.stack}`);
|
|
726
|
-
if (error.code) {
|
|
727
|
-
Logger.error(`错误代码: ${error.code}`);
|
|
728
|
-
}
|
|
729
|
-
if (error.errno) {
|
|
730
|
-
Logger.error(`错误编号: ${error.errno}`);
|
|
731
|
-
}
|
|
732
|
-
process.exit(1);
|
|
733
|
-
} finally {
|
|
734
|
-
if (sql) {
|
|
735
|
-
try {
|
|
736
|
-
await sql.close();
|
|
737
|
-
} catch (error) {
|
|
738
|
-
Logger.warn('关闭数据库连接时出错:', error.message);
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
// 如果直接运行此脚本(Bun 支持 import.meta.main)
|
|
745
|
-
if (import.meta.main) {
|
|
746
|
-
SyncDb().catch((error) => {
|
|
747
|
-
console.error('❌ 数据库同步失败:', error);
|
|
748
|
-
process.exit(1);
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
export { SyncDb };
|