knight-server 1.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.
- package/Server.d.ts +39 -0
- package/Server.d.ts.map +1 -0
- package/Server.js +61 -0
- package/Server.js.map +1 -0
- package/base/AbstractModule.d.ts +25 -0
- package/base/AbstractModule.d.ts.map +1 -0
- package/base/AbstractModule.js +19 -0
- package/base/AbstractModule.js.map +1 -0
- package/base/AbstractObject.d.ts +22 -0
- package/base/AbstractObject.d.ts.map +1 -0
- package/base/AbstractObject.js +48 -0
- package/base/AbstractObject.js.map +1 -0
- package/base/AbstractRouter.d.ts +24 -0
- package/base/AbstractRouter.d.ts.map +1 -0
- package/base/AbstractRouter.js +26 -0
- package/base/AbstractRouter.js.map +1 -0
- package/base/index.d.ts +4 -0
- package/base/index.d.ts.map +1 -0
- package/base/index.js +20 -0
- package/base/index.js.map +1 -0
- package/index.d.ts +5 -0
- package/index.d.ts.map +1 -0
- package/index.js +46 -0
- package/index.js.map +1 -0
- package/module/database/DataBaseModule.d.ts +133 -0
- package/module/database/DataBaseModule.d.ts.map +1 -0
- package/module/database/DataBaseModule.js +359 -0
- package/module/database/DataBaseModule.js.map +1 -0
- package/module/database/index.d.ts +2 -0
- package/module/database/index.d.ts.map +1 -0
- package/module/database/index.js +18 -0
- package/module/database/index.js.map +1 -0
- package/module/http/HttpModule.d.ts +26 -0
- package/module/http/HttpModule.d.ts.map +1 -0
- package/module/http/HttpModule.js +52 -0
- package/module/http/HttpModule.js.map +1 -0
- package/module/http/HttpRouter.d.ts +34 -0
- package/module/http/HttpRouter.d.ts.map +1 -0
- package/module/http/HttpRouter.js +63 -0
- package/module/http/HttpRouter.js.map +1 -0
- package/module/http/index.d.ts +3 -0
- package/module/http/index.d.ts.map +1 -0
- package/module/http/index.js +19 -0
- package/module/http/index.js.map +1 -0
- package/module/index.d.ts +4 -0
- package/module/index.d.ts.map +1 -0
- package/module/index.js +20 -0
- package/module/index.js.map +1 -0
- package/module/websocket/WebSocketClient.d.ts +56 -0
- package/module/websocket/WebSocketClient.d.ts.map +1 -0
- package/module/websocket/WebSocketClient.js +142 -0
- package/module/websocket/WebSocketClient.js.map +1 -0
- package/module/websocket/WebSocketModule.d.ts +31 -0
- package/module/websocket/WebSocketModule.d.ts.map +1 -0
- package/module/websocket/WebSocketModule.js +100 -0
- package/module/websocket/WebSocketModule.js.map +1 -0
- package/module/websocket/WebSocketRouter.d.ts +32 -0
- package/module/websocket/WebSocketRouter.d.ts.map +1 -0
- package/module/websocket/WebSocketRouter.js +67 -0
- package/module/websocket/WebSocketRouter.js.map +1 -0
- package/module/websocket/index.d.ts +4 -0
- package/module/websocket/index.d.ts.map +1 -0
- package/module/websocket/index.js +20 -0
- package/module/websocket/index.js.map +1 -0
- package/package.json +19 -0
- package/test/MyRouter.d.ts +16 -0
- package/test/MyRouter.d.ts.map +1 -0
- package/test/MyRouter.js +49 -0
- package/test/MyRouter.js.map +1 -0
- package/test/WSRouter.d.ts +10 -0
- package/test/WSRouter.d.ts.map +1 -0
- package/test/WSRouter.js +33 -0
- package/test/WSRouter.js.map +1 -0
- package/test/index.d.ts +3 -0
- package/test/index.d.ts.map +1 -0
- package/test/index.js +19 -0
- package/test/index.js.map +1 -0
- package/utility/DatabaseUtility.d.ts +158 -0
- package/utility/DatabaseUtility.d.ts.map +1 -0
- package/utility/DatabaseUtility.js +903 -0
- package/utility/DatabaseUtility.js.map +1 -0
- package/utility/InterfaceUtility.d.ts +14 -0
- package/utility/InterfaceUtility.d.ts.map +1 -0
- package/utility/InterfaceUtility.js +3 -0
- package/utility/InterfaceUtility.js.map +1 -0
- package/utility/NetworkUtility.d.ts +80 -0
- package/utility/NetworkUtility.d.ts.map +1 -0
- package/utility/NetworkUtility.js +134 -0
- package/utility/NetworkUtility.js.map +1 -0
- package/utility/StringUtility.d.ts +46 -0
- package/utility/StringUtility.d.ts.map +1 -0
- package/utility/StringUtility.js +153 -0
- package/utility/StringUtility.js.map +1 -0
- package/utility/TimeUtility.d.ts +65 -0
- package/utility/TimeUtility.d.ts.map +1 -0
- package/utility/TimeUtility.js +220 -0
- package/utility/TimeUtility.js.map +1 -0
- package/utility/index.d.ts +6 -0
- package/utility/index.d.ts.map +1 -0
- package/utility/index.js +22 -0
- package/utility/index.js.map +1 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DatabaseUtility = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/** 数据库工具 */
|
|
40
|
+
var DatabaseUtility;
|
|
41
|
+
(function (DatabaseUtility) {
|
|
42
|
+
/** 智能表格 — 从 SQL 文件自动同步数据库表结构 */
|
|
43
|
+
class SmartTable {
|
|
44
|
+
constructor(pool) {
|
|
45
|
+
this.pool = pool;
|
|
46
|
+
}
|
|
47
|
+
// ==================== 核心方法 ====================
|
|
48
|
+
/**
|
|
49
|
+
* 自动同步目录下的所有 SQL 文件
|
|
50
|
+
* @param relativePath SQL文件所在目录相对项目根目录的路径
|
|
51
|
+
* @param options 同步选项
|
|
52
|
+
* @param fileFilter 文件过滤函数(可选),返回 true 表示需要处理
|
|
53
|
+
*/
|
|
54
|
+
async syncAllFromDir(relativePath, options = {}, fileFilter) {
|
|
55
|
+
const projectRoot = path.resolve(__dirname, "../../");
|
|
56
|
+
const sqlDir = path.join(projectRoot, relativePath);
|
|
57
|
+
const results = new Map();
|
|
58
|
+
const failed = [];
|
|
59
|
+
try {
|
|
60
|
+
if (!fs.existsSync(sqlDir)) {
|
|
61
|
+
return {
|
|
62
|
+
success: false,
|
|
63
|
+
results,
|
|
64
|
+
failed,
|
|
65
|
+
message: `SQL目录不存在: ${sqlDir}`
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const files = fs.readdirSync(sqlDir).filter(file => file.toLowerCase().endsWith('.sql'));
|
|
69
|
+
if (files.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
success: true,
|
|
72
|
+
results,
|
|
73
|
+
failed,
|
|
74
|
+
message: `目录中没有找到 SQL 文件: ${sqlDir}`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const sortedFiles = await this.sortFilesByDependency(sqlDir, files);
|
|
78
|
+
console.log(`🔍 找到 ${sortedFiles.length} 个 SQL 文件,按依赖顺序同步...`);
|
|
79
|
+
for (const file of sortedFiles) {
|
|
80
|
+
if (fileFilter && !fileFilter(file)) {
|
|
81
|
+
console.log(`⏭️ 跳过文件: ${file}`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const filePath = path.join(sqlDir, file);
|
|
85
|
+
console.log(`📄 正在处理: ${file}`);
|
|
86
|
+
try {
|
|
87
|
+
const result = await this.syncTableFromSql(filePath, options);
|
|
88
|
+
results.set(file, result);
|
|
89
|
+
if (result.success) {
|
|
90
|
+
const actionEmoji = result.action === 'CREATE' ? '🆕' :
|
|
91
|
+
result.action === 'ALTER' ? '📝' :
|
|
92
|
+
result.action === 'NONE' ? '✅' : '➖';
|
|
93
|
+
console.log(` ${actionEmoji} ${result.message}`);
|
|
94
|
+
if (!options.execute && result.statements) {
|
|
95
|
+
console.log(` 需要执行 ${result.statements.length} 条语句`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
failed.push(file);
|
|
100
|
+
console.error(` ❌ 处理失败: ${result.error}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
failed.push(file);
|
|
105
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
106
|
+
console.error(` 💥 处理异常: ${errorMsg}`);
|
|
107
|
+
results.set(file, {
|
|
108
|
+
success: false,
|
|
109
|
+
error: errorMsg
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const successCount = results.size - failed.length;
|
|
114
|
+
const message = failed.length === 0
|
|
115
|
+
? `✅ 所有 SQL 文件处理完成,成功 ${successCount} 个`
|
|
116
|
+
: `⚠️ 处理完成,成功 ${successCount} 个,失败 ${failed.length} 个`;
|
|
117
|
+
console.log(`${message}`);
|
|
118
|
+
if (failed.length > 0) {
|
|
119
|
+
console.log('❌ 失败的文件:', failed.join(', '));
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
success: failed.length === 0,
|
|
123
|
+
results,
|
|
124
|
+
failed,
|
|
125
|
+
message
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
results,
|
|
133
|
+
failed,
|
|
134
|
+
message: `处理过程中发生错误: ${errorMsg}`
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 根据依赖关系排序文件(拓扑排序,被引用的表先处理)
|
|
140
|
+
*/
|
|
141
|
+
async sortFilesByDependency(sqlDir, files) {
|
|
142
|
+
const dependencies = new Map();
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
const filePath = path.join(sqlDir, file);
|
|
145
|
+
const sql = fs.readFileSync(filePath, 'utf8');
|
|
146
|
+
const foreignKeys = this.extractForeignKeys(sql);
|
|
147
|
+
dependencies.set(file, foreignKeys.map(fk => fk.tableName + '.sql'));
|
|
148
|
+
}
|
|
149
|
+
const sorted = [];
|
|
150
|
+
const visited = new Set();
|
|
151
|
+
const visiting = new Set();
|
|
152
|
+
const visit = (file) => {
|
|
153
|
+
if (visited.has(file))
|
|
154
|
+
return;
|
|
155
|
+
if (visiting.has(file)) {
|
|
156
|
+
console.warn(`⚠️ 检测到循环依赖: ${file}`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
visiting.add(file);
|
|
160
|
+
const deps = dependencies.get(file) || [];
|
|
161
|
+
for (const dep of deps) {
|
|
162
|
+
if (files.includes(dep)) {
|
|
163
|
+
visit(dep);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
visiting.delete(file);
|
|
167
|
+
visited.add(file);
|
|
168
|
+
sorted.push(file);
|
|
169
|
+
};
|
|
170
|
+
for (const file of files) {
|
|
171
|
+
visit(file);
|
|
172
|
+
}
|
|
173
|
+
return sorted;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 从 SQL 文件同步数据库结构
|
|
177
|
+
*/
|
|
178
|
+
async syncTableFromSql(sqlFilePath, options = {}) {
|
|
179
|
+
let foreignKeysDisabled = false;
|
|
180
|
+
try {
|
|
181
|
+
const targetSchema = await this.parseSqlTableSchema(sqlFilePath);
|
|
182
|
+
if (!targetSchema) {
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
error: `无法从 SQL 文件解析表结构: ${sqlFilePath}`
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const exists = await this.tableExists(targetSchema.name);
|
|
189
|
+
if (!exists) {
|
|
190
|
+
if (options.execute !== false) {
|
|
191
|
+
await this.executeSqlFile(sqlFilePath);
|
|
192
|
+
return {
|
|
193
|
+
success: true,
|
|
194
|
+
message: `表 ${targetSchema.name} 创建成功`,
|
|
195
|
+
action: 'CREATE'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
message: `需要创建表 ${targetSchema.name}`,
|
|
202
|
+
action: 'CREATE',
|
|
203
|
+
targetSchema
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
const currentSchema = await this.getTableSchema(targetSchema.name);
|
|
208
|
+
const referencingForeignKeys = await this.getReferencingForeignKeys(targetSchema.name);
|
|
209
|
+
const diff = this.compareTableSchemas(currentSchema, targetSchema, options);
|
|
210
|
+
if (diff.isEmpty) {
|
|
211
|
+
return {
|
|
212
|
+
success: true,
|
|
213
|
+
message: `表 ${targetSchema.name} 已是最新,无需更新`,
|
|
214
|
+
action: 'NONE'
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (referencingForeignKeys.length > 0 && this.hasPrimaryKeyChanges(diff)) {
|
|
218
|
+
if (options.execute !== false) {
|
|
219
|
+
await this.disableForeignKeys();
|
|
220
|
+
foreignKeysDisabled = true;
|
|
221
|
+
console.log(` 🔓 临时禁用外键检查,因为表 ${targetSchema.name} 有 ${referencingForeignKeys.length} 个外键引用`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const alterStatements = this.generateAlterStatements(targetSchema.name, diff, options.dropExtraColumns);
|
|
225
|
+
if (alterStatements.length === 0) {
|
|
226
|
+
return {
|
|
227
|
+
success: true,
|
|
228
|
+
message: `表 ${targetSchema.name} 无需修改`,
|
|
229
|
+
action: 'NONE'
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (options.backupData && options.execute !== false) {
|
|
233
|
+
await this.backupTableData(targetSchema.name);
|
|
234
|
+
}
|
|
235
|
+
if (options.execute !== false) {
|
|
236
|
+
console.log(` 🔧 即将执行 ${alterStatements.length} 条修改:`);
|
|
237
|
+
alterStatements.forEach(s => console.log(` ${s}`));
|
|
238
|
+
await this.executeRawQuery('START TRANSACTION');
|
|
239
|
+
let failedStmt = '';
|
|
240
|
+
try {
|
|
241
|
+
for (const statement of alterStatements) {
|
|
242
|
+
failedStmt = statement;
|
|
243
|
+
await this.executeRawQuery(statement);
|
|
244
|
+
}
|
|
245
|
+
await this.executeRawQuery('COMMIT');
|
|
246
|
+
}
|
|
247
|
+
catch (alterError) {
|
|
248
|
+
await this.executeRawQuery('ROLLBACK');
|
|
249
|
+
const msg = alterError instanceof Error ? alterError.message : String(alterError);
|
|
250
|
+
throw new Error(`${msg}\n → 失败语句: ${failedStmt}`);
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
success: true,
|
|
254
|
+
message: `表 ${targetSchema.name} 更新成功,执行了 ${alterStatements.length} 条修改`,
|
|
255
|
+
action: 'ALTER',
|
|
256
|
+
details: alterStatements
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
return {
|
|
261
|
+
success: true,
|
|
262
|
+
message: `表 ${targetSchema.name} 需要更新`,
|
|
263
|
+
action: 'ALTER',
|
|
264
|
+
diff,
|
|
265
|
+
statements: alterStatements
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
return {
|
|
271
|
+
success: false,
|
|
272
|
+
error: error instanceof Error ? error.message : String(error)
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
if (foreignKeysDisabled) {
|
|
277
|
+
await this.enableForeignKeys();
|
|
278
|
+
console.log(` 🔒 恢复外键检查`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// ==================== SQL 执行方法 ====================
|
|
283
|
+
/**
|
|
284
|
+
* 执行原生SQL查询(用于DDL语句,比prepared statement更兼容)
|
|
285
|
+
*/
|
|
286
|
+
async executeRawQuery(sql) {
|
|
287
|
+
const pool = this.pool;
|
|
288
|
+
if (null == pool) {
|
|
289
|
+
throw new Error('数据库连接池未初始化');
|
|
290
|
+
}
|
|
291
|
+
await pool.query(sql);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* 执行SQL查询(用于参数化查询)
|
|
295
|
+
*/
|
|
296
|
+
async executeQuery(sql, params = []) {
|
|
297
|
+
const pool = this.pool;
|
|
298
|
+
if (null == pool) {
|
|
299
|
+
throw new Error('数据库连接池未初始化');
|
|
300
|
+
}
|
|
301
|
+
const [rows, fields] = await pool.execute(sql, params);
|
|
302
|
+
return [rows, fields];
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* 执行 SQL 脚本(支持多条语句,逐条执行)
|
|
306
|
+
*/
|
|
307
|
+
async executeSqlScript(sql) {
|
|
308
|
+
const cleanSql = sql
|
|
309
|
+
.replace(/--.*$/gm, '')
|
|
310
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
311
|
+
.trim();
|
|
312
|
+
if (!cleanSql)
|
|
313
|
+
return;
|
|
314
|
+
const statements = this.splitSqlStatements(cleanSql);
|
|
315
|
+
for (const statement of statements) {
|
|
316
|
+
if (statement.length > 0) {
|
|
317
|
+
await this.executeRawQuery(statement);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* 智能分割 SQL 语句(处理字符串和反引号中的分号)
|
|
323
|
+
*/
|
|
324
|
+
splitSqlStatements(sql) {
|
|
325
|
+
const statements = [];
|
|
326
|
+
let current = '';
|
|
327
|
+
let inSingleQuote = false;
|
|
328
|
+
let inDoubleQuote = false;
|
|
329
|
+
let inBacktick = false;
|
|
330
|
+
for (let i = 0; i < sql.length; i++) {
|
|
331
|
+
const char = sql[i];
|
|
332
|
+
const prevChar = i > 0 ? sql[i - 1] : '';
|
|
333
|
+
if (char === "'" && !inDoubleQuote && !inBacktick && prevChar !== '\\') {
|
|
334
|
+
inSingleQuote = !inSingleQuote;
|
|
335
|
+
}
|
|
336
|
+
else if (char === '"' && !inSingleQuote && !inBacktick && prevChar !== '\\') {
|
|
337
|
+
inDoubleQuote = !inDoubleQuote;
|
|
338
|
+
}
|
|
339
|
+
else if (char === '`' && !inSingleQuote && !inDoubleQuote && prevChar !== '\\') {
|
|
340
|
+
inBacktick = !inBacktick;
|
|
341
|
+
}
|
|
342
|
+
if (char === ';' && !inSingleQuote && !inDoubleQuote && !inBacktick) {
|
|
343
|
+
const trimmed = current.trim();
|
|
344
|
+
if (trimmed) {
|
|
345
|
+
statements.push(trimmed);
|
|
346
|
+
}
|
|
347
|
+
current = '';
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
current += char;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
const trimmed = current.trim();
|
|
354
|
+
if (trimmed) {
|
|
355
|
+
statements.push(trimmed);
|
|
356
|
+
}
|
|
357
|
+
return statements;
|
|
358
|
+
}
|
|
359
|
+
// ==================== 表结构解析与对比 ====================
|
|
360
|
+
/**
|
|
361
|
+
* 解析 SQL 文件中的表结构(使用括号计数处理嵌套括号)
|
|
362
|
+
*/
|
|
363
|
+
async parseSqlTableSchema(sqlFilePath) {
|
|
364
|
+
const sql = fs.readFileSync(sqlFilePath, 'utf8');
|
|
365
|
+
const createMatch = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?\s*\(/i);
|
|
366
|
+
if (!createMatch)
|
|
367
|
+
return null;
|
|
368
|
+
const tableName = createMatch[1];
|
|
369
|
+
const startIdx = (createMatch.index || 0) + createMatch[0].length;
|
|
370
|
+
// 括号计数找到匹配的 )
|
|
371
|
+
let depth = 1;
|
|
372
|
+
let endIdx = startIdx;
|
|
373
|
+
let inStr = false;
|
|
374
|
+
let strChar = '';
|
|
375
|
+
for (let i = startIdx; i < sql.length && depth > 0; i++) {
|
|
376
|
+
const ch = sql[i];
|
|
377
|
+
const prev = i > 0 ? sql[i - 1] : '';
|
|
378
|
+
if ((ch === "'" || ch === '"') && prev !== '\\') {
|
|
379
|
+
if (!inStr) {
|
|
380
|
+
inStr = true;
|
|
381
|
+
strChar = ch;
|
|
382
|
+
}
|
|
383
|
+
else if (strChar === ch) {
|
|
384
|
+
inStr = false;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (!inStr) {
|
|
388
|
+
if (ch === '(')
|
|
389
|
+
depth++;
|
|
390
|
+
else if (ch === ')')
|
|
391
|
+
depth--;
|
|
392
|
+
}
|
|
393
|
+
if (depth === 0)
|
|
394
|
+
endIdx = i;
|
|
395
|
+
}
|
|
396
|
+
const columnsDef = sql.substring(startIdx, endIdx);
|
|
397
|
+
const suffix = sql.substring(endIdx + 1);
|
|
398
|
+
const engineMatch = suffix.match(/ENGINE=(\w+)/i);
|
|
399
|
+
const charsetMatch = suffix.match(/DEFAULT\s+CHARSET=(\w+)/i);
|
|
400
|
+
const commentMatch = suffix.match(/COMMENT\s*=\s*'([^']*)'/i);
|
|
401
|
+
const engine = engineMatch?.[1] || 'InnoDB';
|
|
402
|
+
const charset = charsetMatch?.[1] || 'utf8mb4';
|
|
403
|
+
const comment = commentMatch?.[1] || '';
|
|
404
|
+
const columns = [];
|
|
405
|
+
const indexes = [];
|
|
406
|
+
let primaryKey = null;
|
|
407
|
+
const lines = columnsDef.split('\n')
|
|
408
|
+
.map(line => line.trim())
|
|
409
|
+
.filter(line => line.length > 0 && !line.startsWith('--'));
|
|
410
|
+
for (const line of lines) {
|
|
411
|
+
// 处理主键
|
|
412
|
+
if (line.match(/PRIMARY\s+KEY/i)) {
|
|
413
|
+
const pkMatch = line.match(/PRIMARY\s+KEY\s*\(`?(\w+)`?\)/i);
|
|
414
|
+
if (pkMatch)
|
|
415
|
+
primaryKey = pkMatch[1];
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
// 处理索引
|
|
419
|
+
const indexMatch = line.match(/(?:UNIQUE\s+)?(?:KEY|INDEX)\s+`?(\w+)`?\s*\(([^)]+)\)/i);
|
|
420
|
+
if (indexMatch) {
|
|
421
|
+
const indexName = indexMatch[1];
|
|
422
|
+
const indexColumns = indexMatch[2].split(',').map(c => c.replace(/`/g, '').trim());
|
|
423
|
+
const isUnique = line.toUpperCase().includes('UNIQUE');
|
|
424
|
+
indexes.push({
|
|
425
|
+
name: indexName,
|
|
426
|
+
columns: indexColumns,
|
|
427
|
+
unique: isUnique,
|
|
428
|
+
type: 'BTREE'
|
|
429
|
+
});
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
// 处理普通列
|
|
433
|
+
const colMatch = line.match(/`?(\w+)`?\s+(\w+)(\([\d,\s]+\))?\s*([^\n]*)/);
|
|
434
|
+
if (colMatch) {
|
|
435
|
+
const colName = colMatch[1];
|
|
436
|
+
const colBaseType = colMatch[2];
|
|
437
|
+
const colLengthPart = colMatch[3] || '';
|
|
438
|
+
const colAttrsRaw = colMatch[4] || '';
|
|
439
|
+
// 从属性中提取类型修饰符(UNSIGNED, ZEROFILL, SIGNED)
|
|
440
|
+
const upperAttrs = colAttrsRaw.toUpperCase();
|
|
441
|
+
let typeModifiers = '';
|
|
442
|
+
if (upperAttrs.includes('UNSIGNED')) {
|
|
443
|
+
typeModifiers += ' UNSIGNED';
|
|
444
|
+
}
|
|
445
|
+
if (upperAttrs.includes('ZEROFILL')) {
|
|
446
|
+
typeModifiers += ' ZEROFILL';
|
|
447
|
+
}
|
|
448
|
+
if (!upperAttrs.includes('UNSIGNED') && upperAttrs.includes('SIGNED')) {
|
|
449
|
+
typeModifiers += ' SIGNED';
|
|
450
|
+
}
|
|
451
|
+
const typeStr = colBaseType + colLengthPart + typeModifiers;
|
|
452
|
+
// 检测内联 PRIMARY KEY / UNIQUE KEY
|
|
453
|
+
let key = '';
|
|
454
|
+
if (upperAttrs.includes('PRIMARY KEY') || upperAttrs.includes('PRIMARY')) {
|
|
455
|
+
key = 'PRI';
|
|
456
|
+
}
|
|
457
|
+
else if (upperAttrs.includes('UNIQUE KEY') || upperAttrs.includes('UNIQUE')) {
|
|
458
|
+
key = 'UNI';
|
|
459
|
+
}
|
|
460
|
+
// 提取 extra(AUTO_INCREMENT, ON UPDATE CURRENT_TIMESTAMP 等)
|
|
461
|
+
let extra = '';
|
|
462
|
+
if (/auto_increment/i.test(colAttrsRaw)) {
|
|
463
|
+
extra = 'auto_increment';
|
|
464
|
+
}
|
|
465
|
+
if (/ON\s+UPDATE\s+CURRENT_TIMESTAMP/i.test(colAttrsRaw)) {
|
|
466
|
+
extra = extra ? extra + ' ON UPDATE CURRENT_TIMESTAMP' : 'ON UPDATE CURRENT_TIMESTAMP';
|
|
467
|
+
}
|
|
468
|
+
columns.push({
|
|
469
|
+
name: colName,
|
|
470
|
+
type: typeStr,
|
|
471
|
+
nullable: !/NOT\s+NULL/i.test(colAttrsRaw),
|
|
472
|
+
key,
|
|
473
|
+
default: this.extractDefaultValue(colAttrsRaw),
|
|
474
|
+
extra,
|
|
475
|
+
comment: this.extractComment(colAttrsRaw)
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
// 回填主键列的 key 标记(PRIMARY KEY 声明在单独一行时的情况)
|
|
480
|
+
if (primaryKey) {
|
|
481
|
+
const pkCol = columns.find(c => c.name === primaryKey);
|
|
482
|
+
if (pkCol && !pkCol.key) {
|
|
483
|
+
pkCol.key = 'PRI';
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
name: tableName,
|
|
488
|
+
columns,
|
|
489
|
+
indexes,
|
|
490
|
+
primaryKey,
|
|
491
|
+
engine,
|
|
492
|
+
charset,
|
|
493
|
+
comment
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* 提取默认值(处理字符串、数字、CURRENT_TIMESTAMP 等)
|
|
498
|
+
*/
|
|
499
|
+
extractDefaultValue(attrs) {
|
|
500
|
+
const match = attrs.match(/DEFAULT\s+('(?:[^'\\]|\\.)*'|CURRENT_TIMESTAMP(?:\(\d*\))?|\w+)/i);
|
|
501
|
+
if (!match)
|
|
502
|
+
return undefined;
|
|
503
|
+
let value = match[1];
|
|
504
|
+
// SQL NULL 关键字 → JavaScript null
|
|
505
|
+
if (/^NULL$/i.test(value)) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
if (/^CURRENT_TIMESTAMP(?:\(\d*\))?$/i.test(value)) {
|
|
509
|
+
return value;
|
|
510
|
+
}
|
|
511
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
512
|
+
value = value.substring(1, value.length - 1);
|
|
513
|
+
value = value.replace(/\\'/g, "'");
|
|
514
|
+
}
|
|
515
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
516
|
+
return Number(value);
|
|
517
|
+
}
|
|
518
|
+
return value;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* 提取注释
|
|
522
|
+
*/
|
|
523
|
+
extractComment(attrs) {
|
|
524
|
+
const match = attrs.match(/COMMENT\s+'([^']*)'/i);
|
|
525
|
+
return match ? match[1] : '';
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* 检查表是否存在
|
|
529
|
+
*/
|
|
530
|
+
async tableExists(tableName) {
|
|
531
|
+
try {
|
|
532
|
+
const sql = `
|
|
533
|
+
SELECT COUNT(*) as count
|
|
534
|
+
FROM information_schema.tables
|
|
535
|
+
WHERE table_schema = DATABASE()
|
|
536
|
+
AND table_name = ?
|
|
537
|
+
`;
|
|
538
|
+
const [rows] = await this.executeQuery(sql, [tableName]);
|
|
539
|
+
return rows[0]?.count > 0;
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
console.error('检查表是否存在失败:', error);
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* 获取当前表结构
|
|
548
|
+
*/
|
|
549
|
+
async getTableSchema(tableName) {
|
|
550
|
+
const [columns] = await this.executeQuery(`SHOW FULL COLUMNS FROM \`${tableName}\``);
|
|
551
|
+
const [indexes] = await this.executeQuery(`SHOW INDEX FROM \`${tableName}\``);
|
|
552
|
+
const [tableStatus] = await this.executeQuery(`SHOW TABLE STATUS WHERE Name = ?`, [tableName]);
|
|
553
|
+
const columnSchemas = columns.map((col) => ({
|
|
554
|
+
name: col.Field,
|
|
555
|
+
type: col.Type,
|
|
556
|
+
nullable: col.Null === 'YES',
|
|
557
|
+
key: col.Key,
|
|
558
|
+
default: col.Default,
|
|
559
|
+
extra: col.Extra,
|
|
560
|
+
comment: col.Comment
|
|
561
|
+
}));
|
|
562
|
+
const indexMap = new Map();
|
|
563
|
+
for (const idx of indexes) {
|
|
564
|
+
const indexName = idx.Key_name;
|
|
565
|
+
if (indexName === 'PRIMARY')
|
|
566
|
+
continue;
|
|
567
|
+
if (!indexMap.has(indexName)) {
|
|
568
|
+
indexMap.set(indexName, {
|
|
569
|
+
name: indexName,
|
|
570
|
+
columns: [],
|
|
571
|
+
unique: !idx.Non_unique,
|
|
572
|
+
type: idx.Index_type
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
indexMap.get(indexName).columns.push(idx.Column_name);
|
|
576
|
+
}
|
|
577
|
+
const primaryKey = columns.find((c) => c.Key === 'PRI')?.name || null;
|
|
578
|
+
return {
|
|
579
|
+
name: tableName,
|
|
580
|
+
columns: columnSchemas,
|
|
581
|
+
indexes: Array.from(indexMap.values()),
|
|
582
|
+
primaryKey,
|
|
583
|
+
engine: tableStatus[0]?.Engine || 'InnoDB',
|
|
584
|
+
charset: tableStatus[0]?.Collation?.split('_')[0] || 'utf8mb4',
|
|
585
|
+
comment: tableStatus[0]?.Comment || ''
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* 比较两个表结构的差异
|
|
590
|
+
*/
|
|
591
|
+
compareTableSchemas(current, target, options = {}) {
|
|
592
|
+
const diff = {
|
|
593
|
+
addedColumns: [],
|
|
594
|
+
removedColumns: [],
|
|
595
|
+
modifiedColumns: [],
|
|
596
|
+
addedIndexes: [],
|
|
597
|
+
removedIndexes: [],
|
|
598
|
+
modifiedIndexes: [],
|
|
599
|
+
isEmpty: true
|
|
600
|
+
};
|
|
601
|
+
const currentColMap = new Map(current.columns.map(c => [c.name, c]));
|
|
602
|
+
const targetColMap = new Map(target.columns.map(c => [c.name, c]));
|
|
603
|
+
for (const targetCol of target.columns) {
|
|
604
|
+
const currentCol = currentColMap.get(targetCol.name);
|
|
605
|
+
if (!currentCol) {
|
|
606
|
+
diff.addedColumns.push(targetCol);
|
|
607
|
+
}
|
|
608
|
+
else if (!this.isColumnEqual(currentCol, targetCol)) {
|
|
609
|
+
diff.modifiedColumns.push({
|
|
610
|
+
name: targetCol.name,
|
|
611
|
+
current: currentCol,
|
|
612
|
+
target: targetCol
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (options.dropExtraColumns) {
|
|
617
|
+
for (const currentCol of current.columns) {
|
|
618
|
+
if (!targetColMap.has(currentCol.name)) {
|
|
619
|
+
diff.removedColumns.push(currentCol);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const currentIdxMap = new Map(current.indexes.map(i => [i.name, i]));
|
|
624
|
+
const targetIdxMap = new Map(target.indexes.map(i => [i.name, i]));
|
|
625
|
+
for (const targetIdx of target.indexes) {
|
|
626
|
+
const currentIdx = currentIdxMap.get(targetIdx.name);
|
|
627
|
+
if (!currentIdx) {
|
|
628
|
+
diff.addedIndexes.push(targetIdx);
|
|
629
|
+
}
|
|
630
|
+
else if (!this.isIndexEqual(currentIdx, targetIdx)) {
|
|
631
|
+
diff.modifiedIndexes.push({
|
|
632
|
+
name: targetIdx.name,
|
|
633
|
+
current: currentIdx,
|
|
634
|
+
target: targetIdx
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (options.dropExtraColumns) {
|
|
639
|
+
for (const currentIdx of current.indexes) {
|
|
640
|
+
if (!targetIdxMap.has(currentIdx.name)) {
|
|
641
|
+
diff.removedIndexes.push(currentIdx);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
diff.isEmpty = diff.addedColumns.length === 0 &&
|
|
646
|
+
diff.removedColumns.length === 0 &&
|
|
647
|
+
diff.modifiedColumns.length === 0 &&
|
|
648
|
+
diff.addedIndexes.length === 0 &&
|
|
649
|
+
diff.removedIndexes.length === 0 &&
|
|
650
|
+
diff.modifiedIndexes.length === 0;
|
|
651
|
+
return diff;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* 生成 ALTER 语句(去重、合并)
|
|
655
|
+
*/
|
|
656
|
+
generateAlterStatements(tableName, diff, dropExtra = false) {
|
|
657
|
+
const statements = [];
|
|
658
|
+
const seen = new Set();
|
|
659
|
+
const addStmt = (stmt) => {
|
|
660
|
+
const key = stmt.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
661
|
+
if (!seen.has(key)) {
|
|
662
|
+
seen.add(key);
|
|
663
|
+
statements.push(stmt);
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
for (const col of diff.addedColumns) {
|
|
667
|
+
// NOT NULL 列必须有 DEFAULT,否则已有数据的表会报 "Invalid use of NULL value"
|
|
668
|
+
let colDef = this.generateColumnDefinition(col);
|
|
669
|
+
if (!col.nullable && col.default === undefined) {
|
|
670
|
+
const safeDefault = this.determineSafeDefault(col);
|
|
671
|
+
// 在 NOT NULL 后插入 DEFAULT
|
|
672
|
+
colDef = colDef.replace(' NOT NULL', ` NOT NULL DEFAULT ${safeDefault}`);
|
|
673
|
+
console.log(` ⚠️ 新增 NOT NULL 列 ${col.name} 无默认值,自动补充 DEFAULT ${safeDefault}`);
|
|
674
|
+
}
|
|
675
|
+
addStmt(`ALTER TABLE \`${tableName}\` ADD COLUMN ${colDef}`);
|
|
676
|
+
}
|
|
677
|
+
for (const mod of diff.modifiedColumns) {
|
|
678
|
+
const colDef = this.generateColumnDefinition(mod.target);
|
|
679
|
+
// 如果列从 nullable → NOT NULL,先回填空值,避免 "Invalid use of NULL value"
|
|
680
|
+
if (mod.current.nullable && !mod.target.nullable) {
|
|
681
|
+
const safeDefault = this.determineSafeDefault(mod.target);
|
|
682
|
+
addStmt(`UPDATE \`${tableName}\` SET \`${mod.target.name}\` = ${safeDefault} WHERE \`${mod.target.name}\` IS NULL`);
|
|
683
|
+
console.log(` ⚠️ 列 ${mod.target.name} 改为 NOT NULL,先将 NULL 值替换为 ${safeDefault}`);
|
|
684
|
+
}
|
|
685
|
+
// 如果目标是 AUTO_INCREMENT 列,确保它是索引列(MySQL 要求)
|
|
686
|
+
if (mod.target.extra?.includes('auto_increment') && mod.target.key !== 'PRI' && mod.target.key !== 'UNI') {
|
|
687
|
+
addStmt(`ALTER TABLE \`${tableName}\` ADD UNIQUE INDEX \`uniq_${mod.target.name}\` (\`${mod.target.name}\`)`);
|
|
688
|
+
}
|
|
689
|
+
addStmt(`ALTER TABLE \`${tableName}\` MODIFY COLUMN ${colDef}`);
|
|
690
|
+
}
|
|
691
|
+
if (dropExtra) {
|
|
692
|
+
for (const col of diff.removedColumns) {
|
|
693
|
+
addStmt(`ALTER TABLE \`${tableName}\` DROP COLUMN \`${col.name}\``);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
for (const idx of diff.addedIndexes) {
|
|
697
|
+
const unique = idx.unique ? 'UNIQUE ' : '';
|
|
698
|
+
const cols = idx.columns.map(c => `\`${c}\``).join(', ');
|
|
699
|
+
addStmt(`ALTER TABLE \`${tableName}\` ADD ${unique}INDEX \`${idx.name}\` (${cols})`);
|
|
700
|
+
}
|
|
701
|
+
if (dropExtra) {
|
|
702
|
+
for (const idx of diff.removedIndexes) {
|
|
703
|
+
addStmt(`ALTER TABLE \`${tableName}\` DROP INDEX \`${idx.name}\``);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
for (const mod of diff.modifiedIndexes) {
|
|
707
|
+
addStmt(`ALTER TABLE \`${tableName}\` DROP INDEX \`${mod.name}\``);
|
|
708
|
+
const unique = mod.target.unique ? 'UNIQUE ' : '';
|
|
709
|
+
const cols = mod.target.columns.map(c => `\`${c}\``).join(', ');
|
|
710
|
+
addStmt(`ALTER TABLE \`${tableName}\` ADD ${unique}INDEX \`${mod.name}\` (${cols})`);
|
|
711
|
+
}
|
|
712
|
+
return statements;
|
|
713
|
+
}
|
|
714
|
+
// ==================== 比较与规范化工具 ====================
|
|
715
|
+
normalizeType(type) {
|
|
716
|
+
return type
|
|
717
|
+
.trim()
|
|
718
|
+
.replace(/\s+/g, ' ')
|
|
719
|
+
.toLowerCase();
|
|
720
|
+
}
|
|
721
|
+
isColumnEqual(col1, col2) {
|
|
722
|
+
if (this.normalizeType(col1.type) !== this.normalizeType(col2.type))
|
|
723
|
+
return false;
|
|
724
|
+
if (col1.nullable !== col2.nullable)
|
|
725
|
+
return false;
|
|
726
|
+
const default1 = this.normalizeDefaultValue(col1.default);
|
|
727
|
+
const default2 = this.normalizeDefaultValue(col2.default);
|
|
728
|
+
if (default1 !== default2)
|
|
729
|
+
return false;
|
|
730
|
+
const hasAI1 = col1.extra?.includes('auto_increment') || false;
|
|
731
|
+
const hasAI2 = col2.extra?.includes('auto_increment') || false;
|
|
732
|
+
if (hasAI1 !== hasAI2)
|
|
733
|
+
return false;
|
|
734
|
+
const hasOnUpdate1 = /ON\s+UPDATE\s+CURRENT_TIMESTAMP/i.test(col1.extra || '');
|
|
735
|
+
const hasOnUpdate2 = /ON\s+UPDATE\s+CURRENT_TIMESTAMP/i.test(col2.extra || '');
|
|
736
|
+
if (hasOnUpdate1 !== hasOnUpdate2)
|
|
737
|
+
return false;
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
normalizeDefaultValue(value) {
|
|
741
|
+
if (value === null || value === undefined)
|
|
742
|
+
return '__NULL__';
|
|
743
|
+
let str = String(value).trim();
|
|
744
|
+
if (/^CURRENT_TIMESTAMP(\(\d*\))?$/i.test(str)) {
|
|
745
|
+
return 'CURRENT_TIMESTAMP';
|
|
746
|
+
}
|
|
747
|
+
if ((str.startsWith("'") && str.endsWith("'")) ||
|
|
748
|
+
(str.startsWith('"') && str.endsWith('"'))) {
|
|
749
|
+
str = str.slice(1, -1);
|
|
750
|
+
}
|
|
751
|
+
return str;
|
|
752
|
+
}
|
|
753
|
+
isIndexEqual(idx1, idx2) {
|
|
754
|
+
if (idx1.unique !== idx2.unique)
|
|
755
|
+
return false;
|
|
756
|
+
if (idx1.columns.length !== idx2.columns.length)
|
|
757
|
+
return false;
|
|
758
|
+
const cols1 = [...idx1.columns].sort();
|
|
759
|
+
const cols2 = [...idx2.columns].sort();
|
|
760
|
+
return JSON.stringify(cols1) === JSON.stringify(cols2);
|
|
761
|
+
}
|
|
762
|
+
generateColumnDefinition(col) {
|
|
763
|
+
let def = `\`${col.name}\` ${col.type}`;
|
|
764
|
+
if (!col.nullable) {
|
|
765
|
+
def += ' NOT NULL';
|
|
766
|
+
}
|
|
767
|
+
if (col.extra?.includes('auto_increment')) {
|
|
768
|
+
def += ' AUTO_INCREMENT';
|
|
769
|
+
}
|
|
770
|
+
if (!col.extra?.includes('auto_increment') && col.default !== undefined) {
|
|
771
|
+
if (col.default === null) {
|
|
772
|
+
// SQL NULL 关键字,不加引号
|
|
773
|
+
def += ' DEFAULT NULL';
|
|
774
|
+
}
|
|
775
|
+
else if (/^CURRENT_TIMESTAMP(\(\d*\))?$/i.test(String(col.default))) {
|
|
776
|
+
def += ` DEFAULT ${col.default}`;
|
|
777
|
+
}
|
|
778
|
+
else if (typeof col.default === 'string') {
|
|
779
|
+
def += ` DEFAULT '${col.default}'`;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
def += ` DEFAULT ${col.default}`;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
if (/ON\s+UPDATE\s+CURRENT_TIMESTAMP/i.test(col.extra || '')) {
|
|
786
|
+
def += ' ON UPDATE CURRENT_TIMESTAMP';
|
|
787
|
+
}
|
|
788
|
+
if (col.comment) {
|
|
789
|
+
def += ` COMMENT '${col.comment}'`;
|
|
790
|
+
}
|
|
791
|
+
return def;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* 根据列类型确定安全的非 NULL 默认值(用于 nullable→NOT NULL 转换前回填空值)
|
|
795
|
+
*/
|
|
796
|
+
determineSafeDefault(col) {
|
|
797
|
+
// 优先使用列自身定义的默认值
|
|
798
|
+
if (col.default !== null && col.default !== undefined) {
|
|
799
|
+
if (/^CURRENT_TIMESTAMP/i.test(String(col.default))) {
|
|
800
|
+
return 'CURRENT_TIMESTAMP';
|
|
801
|
+
}
|
|
802
|
+
if (typeof col.default === 'string') {
|
|
803
|
+
return `'${col.default}'`;
|
|
804
|
+
}
|
|
805
|
+
return String(col.default);
|
|
806
|
+
}
|
|
807
|
+
// 无显式默认值时,根据类型推断
|
|
808
|
+
const type = col.type.toLowerCase();
|
|
809
|
+
if (/\b(tinyint|smallint|mediumint|int|bigint|float|double|decimal|bit)\b/.test(type)) {
|
|
810
|
+
return '0';
|
|
811
|
+
}
|
|
812
|
+
if (/\b(datetime|timestamp)\b/.test(type)) {
|
|
813
|
+
return 'CURRENT_TIMESTAMP';
|
|
814
|
+
}
|
|
815
|
+
if (/\b(date|time|year)\b/.test(type)) {
|
|
816
|
+
return "'0000-00-00'";
|
|
817
|
+
}
|
|
818
|
+
// VARCHAR, CHAR, TEXT, BLOB, ENUM, JSON 等
|
|
819
|
+
return "''";
|
|
820
|
+
}
|
|
821
|
+
// ==================== 外键与备份 ====================
|
|
822
|
+
async disableForeignKeys() {
|
|
823
|
+
await this.executeRawQuery('SET FOREIGN_KEY_CHECKS = 0');
|
|
824
|
+
}
|
|
825
|
+
async enableForeignKeys() {
|
|
826
|
+
await this.executeRawQuery('SET FOREIGN_KEY_CHECKS = 1');
|
|
827
|
+
}
|
|
828
|
+
async getForeignKeys(tableName) {
|
|
829
|
+
const sql = `
|
|
830
|
+
SELECT
|
|
831
|
+
CONSTRAINT_NAME,
|
|
832
|
+
COLUMN_NAME,
|
|
833
|
+
REFERENCED_TABLE_NAME,
|
|
834
|
+
REFERENCED_COLUMN_NAME
|
|
835
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
836
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
837
|
+
AND TABLE_NAME = ?
|
|
838
|
+
AND REFERENCED_TABLE_NAME IS NOT NULL
|
|
839
|
+
`;
|
|
840
|
+
const [rows] = await this.executeQuery(sql, [tableName]);
|
|
841
|
+
return rows;
|
|
842
|
+
}
|
|
843
|
+
async getReferencingForeignKeys(tableName) {
|
|
844
|
+
const sql = `
|
|
845
|
+
SELECT
|
|
846
|
+
kcu.CONSTRAINT_NAME,
|
|
847
|
+
kcu.TABLE_NAME,
|
|
848
|
+
kcu.COLUMN_NAME,
|
|
849
|
+
kcu.REFERENCED_TABLE_NAME,
|
|
850
|
+
kcu.REFERENCED_COLUMN_NAME,
|
|
851
|
+
rc.UPDATE_RULE,
|
|
852
|
+
rc.DELETE_RULE
|
|
853
|
+
FROM information_schema.KEY_COLUMN_USAGE kcu
|
|
854
|
+
JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
|
|
855
|
+
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
|
856
|
+
AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
|
|
857
|
+
WHERE kcu.REFERENCED_TABLE_SCHEMA = DATABASE()
|
|
858
|
+
AND kcu.REFERENCED_TABLE_NAME = ?
|
|
859
|
+
AND kcu.REFERENCED_COLUMN_NAME IS NOT NULL
|
|
860
|
+
`;
|
|
861
|
+
const [rows] = await this.executeQuery(sql, [tableName]);
|
|
862
|
+
return rows;
|
|
863
|
+
}
|
|
864
|
+
hasPrimaryKeyChanges(diff) {
|
|
865
|
+
for (const mod of diff.modifiedColumns) {
|
|
866
|
+
if (mod.current.key === 'PRI' || mod.target.key === 'PRI') {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
for (const col of diff.removedColumns) {
|
|
871
|
+
if (col.key === 'PRI') {
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
extractForeignKeys(sql) {
|
|
878
|
+
const foreignKeys = [];
|
|
879
|
+
const fkRegex = /CONSTRAINT\s+`?(\w+)`?\s+FOREIGN\s+KEY\s*\(`?(\w+)`?\)\s+REFERENCES\s+`?(\w+)`?\s*\(`?(\w+)`?\)(?:\s+ON\s+DELETE\s+(\w+))?(?:\s+ON\s+UPDATE\s+(\w+))?/gi;
|
|
880
|
+
let match;
|
|
881
|
+
while ((match = fkRegex.exec(sql)) !== null) {
|
|
882
|
+
foreignKeys.push({
|
|
883
|
+
tableName: match[3],
|
|
884
|
+
constraint: match[0]
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
return foreignKeys;
|
|
888
|
+
}
|
|
889
|
+
async backupTableData(tableName) {
|
|
890
|
+
const timestamp = Date.now();
|
|
891
|
+
const backupTable = `${tableName}_backup_${timestamp}`;
|
|
892
|
+
await this.executeRawQuery(`CREATE TABLE \`${backupTable}\` LIKE \`${tableName}\``);
|
|
893
|
+
await this.executeRawQuery(`INSERT INTO \`${backupTable}\` SELECT * FROM \`${tableName}\``);
|
|
894
|
+
console.log(`✅ 数据已备份到 ${backupTable}`);
|
|
895
|
+
}
|
|
896
|
+
async executeSqlFile(sqlFilePath) {
|
|
897
|
+
const sql = fs.readFileSync(sqlFilePath, 'utf8');
|
|
898
|
+
await this.executeSqlScript(sql);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
DatabaseUtility.SmartTable = SmartTable;
|
|
902
|
+
})(DatabaseUtility || (exports.DatabaseUtility = DatabaseUtility = {}));
|
|
903
|
+
//# sourceMappingURL=DatabaseUtility.js.map
|