@berthojoris/mcp-mysql-server 1.8.0 → 1.9.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/CHANGELOG.md +102 -0
- package/DOCUMENTATIONS.md +703 -9
- package/README.md +466 -22
- package/dist/config/featureConfig.js +10 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +95 -0
- package/dist/mcp-server.js +218 -0
- package/dist/tools/schemaVersioningTools.d.ts +147 -0
- package/dist/tools/schemaVersioningTools.js +1007 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.SchemaVersioningTools = void 0;
|
|
7
|
+
const connection_1 = __importDefault(require("../db/connection"));
|
|
8
|
+
const config_1 = require("../config/config");
|
|
9
|
+
/**
|
|
10
|
+
* Schema Versioning and Migrations Tools for MySQL MCP Server
|
|
11
|
+
* Provides utilities for managing database schema versions and migrations
|
|
12
|
+
*/
|
|
13
|
+
class SchemaVersioningTools {
|
|
14
|
+
constructor(security) {
|
|
15
|
+
this.migrationsTable = "_schema_migrations";
|
|
16
|
+
this.db = connection_1.default.getInstance();
|
|
17
|
+
this.security = security;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validate database access
|
|
21
|
+
*/
|
|
22
|
+
validateDatabaseAccess(requestedDatabase) {
|
|
23
|
+
const connectedDatabase = config_1.dbConfig.database;
|
|
24
|
+
if (!connectedDatabase) {
|
|
25
|
+
return {
|
|
26
|
+
valid: false,
|
|
27
|
+
database: "",
|
|
28
|
+
error: "No database configured. Please specify a database in your connection settings.",
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (requestedDatabase && requestedDatabase !== connectedDatabase) {
|
|
32
|
+
return {
|
|
33
|
+
valid: false,
|
|
34
|
+
database: "",
|
|
35
|
+
error: `Access denied: You are connected to '${connectedDatabase}' but requested '${requestedDatabase}'. Cross-database access is not permitted.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
valid: true,
|
|
40
|
+
database: connectedDatabase,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Generate a migration version based on timestamp
|
|
45
|
+
*/
|
|
46
|
+
generateVersion() {
|
|
47
|
+
const now = new Date();
|
|
48
|
+
return now.toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Escape string value for SQL
|
|
52
|
+
*/
|
|
53
|
+
escapeValue(value) {
|
|
54
|
+
if (value === null)
|
|
55
|
+
return "NULL";
|
|
56
|
+
if (typeof value === "number")
|
|
57
|
+
return String(value);
|
|
58
|
+
if (typeof value === "boolean")
|
|
59
|
+
return value ? "1" : "0";
|
|
60
|
+
if (value instanceof Date) {
|
|
61
|
+
return `'${value.toISOString().slice(0, 19).replace("T", " ")}'`;
|
|
62
|
+
}
|
|
63
|
+
// Escape string
|
|
64
|
+
const escaped = String(value)
|
|
65
|
+
.replace(/\\/g, "\\\\")
|
|
66
|
+
.replace(/'/g, "\\'")
|
|
67
|
+
.replace(/"/g, '\\"')
|
|
68
|
+
.replace(/\n/g, "\\n")
|
|
69
|
+
.replace(/\r/g, "\\r")
|
|
70
|
+
.replace(/\t/g, "\\t")
|
|
71
|
+
.replace(/\0/g, "\\0");
|
|
72
|
+
return `'${escaped}'`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Initialize the migrations tracking table if it doesn't exist
|
|
76
|
+
*/
|
|
77
|
+
async initMigrationsTable(params) {
|
|
78
|
+
try {
|
|
79
|
+
const { database } = params;
|
|
80
|
+
// Validate database access
|
|
81
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
82
|
+
if (!dbValidation.valid) {
|
|
83
|
+
return { status: "error", error: dbValidation.error };
|
|
84
|
+
}
|
|
85
|
+
const createTableQuery = `
|
|
86
|
+
CREATE TABLE IF NOT EXISTS ${this.migrationsTable} (
|
|
87
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
88
|
+
version VARCHAR(14) NOT NULL UNIQUE,
|
|
89
|
+
name VARCHAR(255) NOT NULL,
|
|
90
|
+
description TEXT,
|
|
91
|
+
up_sql LONGTEXT NOT NULL,
|
|
92
|
+
down_sql LONGTEXT,
|
|
93
|
+
checksum VARCHAR(64),
|
|
94
|
+
applied_at TIMESTAMP NULL DEFAULT NULL,
|
|
95
|
+
applied_by VARCHAR(255),
|
|
96
|
+
execution_time_ms INT,
|
|
97
|
+
status ENUM('pending', 'applied', 'failed', 'rolled_back') DEFAULT 'pending',
|
|
98
|
+
error_message TEXT,
|
|
99
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
100
|
+
INDEX idx_version (version),
|
|
101
|
+
INDEX idx_status (status),
|
|
102
|
+
INDEX idx_applied_at (applied_at)
|
|
103
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
104
|
+
`;
|
|
105
|
+
await this.db.query(createTableQuery);
|
|
106
|
+
return {
|
|
107
|
+
status: "success",
|
|
108
|
+
data: {
|
|
109
|
+
message: `Migrations table '${this.migrationsTable}' initialized successfully`,
|
|
110
|
+
table_name: this.migrationsTable,
|
|
111
|
+
},
|
|
112
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
status: "error",
|
|
118
|
+
error: error.message,
|
|
119
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Create a new migration entry
|
|
125
|
+
*/
|
|
126
|
+
async createMigration(params) {
|
|
127
|
+
try {
|
|
128
|
+
const { name, up_sql, down_sql, description, version, database, } = params;
|
|
129
|
+
// Validate database access
|
|
130
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
131
|
+
if (!dbValidation.valid) {
|
|
132
|
+
return { status: "error", error: dbValidation.error };
|
|
133
|
+
}
|
|
134
|
+
// Validate name
|
|
135
|
+
if (!name || name.trim().length === 0) {
|
|
136
|
+
return { status: "error", error: "Migration name is required" };
|
|
137
|
+
}
|
|
138
|
+
if (!up_sql || up_sql.trim().length === 0) {
|
|
139
|
+
return { status: "error", error: "up_sql is required for migration" };
|
|
140
|
+
}
|
|
141
|
+
// Ensure migrations table exists
|
|
142
|
+
await this.initMigrationsTable({ database });
|
|
143
|
+
// Generate version if not provided
|
|
144
|
+
const migrationVersion = version || this.generateVersion();
|
|
145
|
+
// Generate checksum for the up_sql
|
|
146
|
+
const checksum = this.generateChecksum(up_sql);
|
|
147
|
+
// Check if version already exists
|
|
148
|
+
const existingQuery = `SELECT id FROM ${this.migrationsTable} WHERE version = ?`;
|
|
149
|
+
const existing = await this.db.query(existingQuery, [migrationVersion]);
|
|
150
|
+
if (existing.length > 0) {
|
|
151
|
+
return {
|
|
152
|
+
status: "error",
|
|
153
|
+
error: `Migration version '${migrationVersion}' already exists`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
// Insert the migration
|
|
157
|
+
const insertQuery = `
|
|
158
|
+
INSERT INTO ${this.migrationsTable}
|
|
159
|
+
(version, name, description, up_sql, down_sql, checksum, status)
|
|
160
|
+
VALUES (?, ?, ?, ?, ?, ?, 'pending')
|
|
161
|
+
`;
|
|
162
|
+
await this.db.query(insertQuery, [
|
|
163
|
+
migrationVersion,
|
|
164
|
+
name.trim(),
|
|
165
|
+
description || null,
|
|
166
|
+
up_sql,
|
|
167
|
+
down_sql || null,
|
|
168
|
+
checksum,
|
|
169
|
+
]);
|
|
170
|
+
return {
|
|
171
|
+
status: "success",
|
|
172
|
+
data: {
|
|
173
|
+
message: `Migration '${name}' created successfully`,
|
|
174
|
+
version: migrationVersion,
|
|
175
|
+
name: name.trim(),
|
|
176
|
+
checksum,
|
|
177
|
+
status: "pending",
|
|
178
|
+
},
|
|
179
|
+
queryLog: this.db.getFormattedQueryLogs(3),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
status: "error",
|
|
185
|
+
error: error.message,
|
|
186
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Generate a simple checksum for SQL content
|
|
192
|
+
*/
|
|
193
|
+
generateChecksum(sql) {
|
|
194
|
+
let hash = 0;
|
|
195
|
+
const str = sql.trim();
|
|
196
|
+
for (let i = 0; i < str.length; i++) {
|
|
197
|
+
const char = str.charCodeAt(i);
|
|
198
|
+
hash = ((hash << 5) - hash) + char;
|
|
199
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
200
|
+
}
|
|
201
|
+
return Math.abs(hash).toString(16).padStart(8, '0');
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Apply pending migrations
|
|
205
|
+
*/
|
|
206
|
+
async applyMigrations(params) {
|
|
207
|
+
try {
|
|
208
|
+
const { target_version, dry_run = false, database } = params;
|
|
209
|
+
// Validate database access
|
|
210
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
211
|
+
if (!dbValidation.valid) {
|
|
212
|
+
return { status: "error", error: dbValidation.error };
|
|
213
|
+
}
|
|
214
|
+
// Ensure migrations table exists
|
|
215
|
+
await this.initMigrationsTable({ database });
|
|
216
|
+
let queryCount = 1;
|
|
217
|
+
// Get pending migrations
|
|
218
|
+
let pendingQuery = `
|
|
219
|
+
SELECT id, version, name, up_sql, checksum
|
|
220
|
+
FROM ${this.migrationsTable}
|
|
221
|
+
WHERE status = 'pending'
|
|
222
|
+
`;
|
|
223
|
+
if (target_version) {
|
|
224
|
+
pendingQuery += ` AND version <= ?`;
|
|
225
|
+
}
|
|
226
|
+
pendingQuery += ` ORDER BY version ASC`;
|
|
227
|
+
const pendingMigrations = target_version
|
|
228
|
+
? await this.db.query(pendingQuery, [target_version])
|
|
229
|
+
: await this.db.query(pendingQuery);
|
|
230
|
+
queryCount++;
|
|
231
|
+
if (pendingMigrations.length === 0) {
|
|
232
|
+
return {
|
|
233
|
+
status: "success",
|
|
234
|
+
data: {
|
|
235
|
+
message: "No pending migrations to apply",
|
|
236
|
+
applied_count: 0,
|
|
237
|
+
migrations: [],
|
|
238
|
+
},
|
|
239
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (dry_run) {
|
|
243
|
+
return {
|
|
244
|
+
status: "success",
|
|
245
|
+
data: {
|
|
246
|
+
message: `Dry run: ${pendingMigrations.length} migration(s) would be applied`,
|
|
247
|
+
dry_run: true,
|
|
248
|
+
migrations: pendingMigrations.map((m) => ({
|
|
249
|
+
version: m.version,
|
|
250
|
+
name: m.name,
|
|
251
|
+
up_sql_preview: m.up_sql.substring(0, 200) + (m.up_sql.length > 200 ? "..." : ""),
|
|
252
|
+
})),
|
|
253
|
+
},
|
|
254
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const appliedMigrations = [];
|
|
258
|
+
const failedMigrations = [];
|
|
259
|
+
const currentUser = config_1.dbConfig.user || "unknown";
|
|
260
|
+
for (const migration of pendingMigrations) {
|
|
261
|
+
const startTime = Date.now();
|
|
262
|
+
try {
|
|
263
|
+
// Split SQL by semicolons and execute each statement
|
|
264
|
+
const statements = this.splitSqlStatements(migration.up_sql);
|
|
265
|
+
for (const statement of statements) {
|
|
266
|
+
if (statement.trim()) {
|
|
267
|
+
await this.db.query(statement);
|
|
268
|
+
queryCount++;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const executionTime = Date.now() - startTime;
|
|
272
|
+
// Update migration status to applied
|
|
273
|
+
const updateQuery = `
|
|
274
|
+
UPDATE ${this.migrationsTable}
|
|
275
|
+
SET status = 'applied',
|
|
276
|
+
applied_at = NOW(),
|
|
277
|
+
applied_by = ?,
|
|
278
|
+
execution_time_ms = ?,
|
|
279
|
+
error_message = NULL
|
|
280
|
+
WHERE id = ?
|
|
281
|
+
`;
|
|
282
|
+
await this.db.query(updateQuery, [currentUser, executionTime, migration.id]);
|
|
283
|
+
queryCount++;
|
|
284
|
+
appliedMigrations.push({
|
|
285
|
+
version: migration.version,
|
|
286
|
+
name: migration.name,
|
|
287
|
+
execution_time_ms: executionTime,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
const executionTime = Date.now() - startTime;
|
|
292
|
+
// Update migration status to failed
|
|
293
|
+
const updateQuery = `
|
|
294
|
+
UPDATE ${this.migrationsTable}
|
|
295
|
+
SET status = 'failed',
|
|
296
|
+
execution_time_ms = ?,
|
|
297
|
+
error_message = ?
|
|
298
|
+
WHERE id = ?
|
|
299
|
+
`;
|
|
300
|
+
await this.db.query(updateQuery, [
|
|
301
|
+
executionTime,
|
|
302
|
+
error.message,
|
|
303
|
+
migration.id,
|
|
304
|
+
]);
|
|
305
|
+
queryCount++;
|
|
306
|
+
failedMigrations.push({
|
|
307
|
+
version: migration.version,
|
|
308
|
+
name: migration.name,
|
|
309
|
+
error: error.message,
|
|
310
|
+
execution_time_ms: executionTime,
|
|
311
|
+
});
|
|
312
|
+
// Stop applying further migrations on failure
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
status: failedMigrations.length > 0 ? "partial" : "success",
|
|
318
|
+
data: {
|
|
319
|
+
message: failedMigrations.length > 0
|
|
320
|
+
? `Applied ${appliedMigrations.length} migration(s), ${failedMigrations.length} failed`
|
|
321
|
+
: `Successfully applied ${appliedMigrations.length} migration(s)`,
|
|
322
|
+
applied_count: appliedMigrations.length,
|
|
323
|
+
failed_count: failedMigrations.length,
|
|
324
|
+
applied_migrations: appliedMigrations,
|
|
325
|
+
failed_migrations: failedMigrations,
|
|
326
|
+
},
|
|
327
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
return {
|
|
332
|
+
status: "error",
|
|
333
|
+
error: error.message,
|
|
334
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Split SQL content into individual statements
|
|
340
|
+
*/
|
|
341
|
+
splitSqlStatements(sql) {
|
|
342
|
+
const statements = [];
|
|
343
|
+
let currentStatement = "";
|
|
344
|
+
let inString = false;
|
|
345
|
+
let stringChar = "";
|
|
346
|
+
let inComment = false;
|
|
347
|
+
let inMultiLineComment = false;
|
|
348
|
+
for (let i = 0; i < sql.length; i++) {
|
|
349
|
+
const char = sql[i];
|
|
350
|
+
const nextChar = sql[i + 1] || "";
|
|
351
|
+
// Handle multi-line comments
|
|
352
|
+
if (!inString && char === "/" && nextChar === "*") {
|
|
353
|
+
inMultiLineComment = true;
|
|
354
|
+
currentStatement += char;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (inMultiLineComment && char === "*" && nextChar === "/") {
|
|
358
|
+
inMultiLineComment = false;
|
|
359
|
+
currentStatement += char + nextChar;
|
|
360
|
+
i++;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (inMultiLineComment) {
|
|
364
|
+
currentStatement += char;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
// Handle single-line comments
|
|
368
|
+
if (!inString && char === "-" && nextChar === "-") {
|
|
369
|
+
inComment = true;
|
|
370
|
+
currentStatement += char;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (inComment && char === "\n") {
|
|
374
|
+
inComment = false;
|
|
375
|
+
currentStatement += char;
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (inComment) {
|
|
379
|
+
currentStatement += char;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
// Handle strings
|
|
383
|
+
if ((char === "'" || char === '"') && sql[i - 1] !== "\\") {
|
|
384
|
+
if (!inString) {
|
|
385
|
+
inString = true;
|
|
386
|
+
stringChar = char;
|
|
387
|
+
}
|
|
388
|
+
else if (char === stringChar) {
|
|
389
|
+
inString = false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Handle statement separator
|
|
393
|
+
if (char === ";" && !inString) {
|
|
394
|
+
currentStatement += char;
|
|
395
|
+
const trimmed = currentStatement.trim();
|
|
396
|
+
if (trimmed && trimmed !== ";") {
|
|
397
|
+
statements.push(trimmed);
|
|
398
|
+
}
|
|
399
|
+
currentStatement = "";
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
currentStatement += char;
|
|
403
|
+
}
|
|
404
|
+
// Add any remaining statement
|
|
405
|
+
const trimmed = currentStatement.trim();
|
|
406
|
+
if (trimmed && trimmed !== ";") {
|
|
407
|
+
statements.push(trimmed);
|
|
408
|
+
}
|
|
409
|
+
return statements;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Rollback the last applied migration or to a specific version
|
|
413
|
+
*/
|
|
414
|
+
async rollbackMigration(params) {
|
|
415
|
+
try {
|
|
416
|
+
const { target_version, steps = 1, dry_run = false, database } = params;
|
|
417
|
+
// Validate database access
|
|
418
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
419
|
+
if (!dbValidation.valid) {
|
|
420
|
+
return { status: "error", error: dbValidation.error };
|
|
421
|
+
}
|
|
422
|
+
let queryCount = 0;
|
|
423
|
+
// Get applied migrations to rollback
|
|
424
|
+
let rollbackQuery;
|
|
425
|
+
let queryParams = [];
|
|
426
|
+
if (target_version) {
|
|
427
|
+
rollbackQuery = `
|
|
428
|
+
SELECT id, version, name, down_sql
|
|
429
|
+
FROM ${this.migrationsTable}
|
|
430
|
+
WHERE status = 'applied' AND version > ?
|
|
431
|
+
ORDER BY version DESC
|
|
432
|
+
`;
|
|
433
|
+
queryParams = [target_version];
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
rollbackQuery = `
|
|
437
|
+
SELECT id, version, name, down_sql
|
|
438
|
+
FROM ${this.migrationsTable}
|
|
439
|
+
WHERE status = 'applied'
|
|
440
|
+
ORDER BY version DESC
|
|
441
|
+
LIMIT ?
|
|
442
|
+
`;
|
|
443
|
+
queryParams = [steps];
|
|
444
|
+
}
|
|
445
|
+
const migrationsToRollback = await this.db.query(rollbackQuery, queryParams);
|
|
446
|
+
queryCount++;
|
|
447
|
+
if (migrationsToRollback.length === 0) {
|
|
448
|
+
return {
|
|
449
|
+
status: "success",
|
|
450
|
+
data: {
|
|
451
|
+
message: "No migrations to rollback",
|
|
452
|
+
rolled_back_count: 0,
|
|
453
|
+
migrations: [],
|
|
454
|
+
},
|
|
455
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
// Check if all migrations have down_sql
|
|
459
|
+
const migrationsWithoutDown = migrationsToRollback.filter((m) => !m.down_sql);
|
|
460
|
+
if (migrationsWithoutDown.length > 0 && !dry_run) {
|
|
461
|
+
return {
|
|
462
|
+
status: "error",
|
|
463
|
+
error: `Cannot rollback: ${migrationsWithoutDown.length} migration(s) do not have down_sql defined: ${migrationsWithoutDown.map((m) => m.version).join(", ")}`,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
if (dry_run) {
|
|
467
|
+
return {
|
|
468
|
+
status: "success",
|
|
469
|
+
data: {
|
|
470
|
+
message: `Dry run: ${migrationsToRollback.length} migration(s) would be rolled back`,
|
|
471
|
+
dry_run: true,
|
|
472
|
+
migrations: migrationsToRollback.map((m) => ({
|
|
473
|
+
version: m.version,
|
|
474
|
+
name: m.name,
|
|
475
|
+
has_down_sql: !!m.down_sql,
|
|
476
|
+
down_sql_preview: m.down_sql
|
|
477
|
+
? m.down_sql.substring(0, 200) + (m.down_sql.length > 200 ? "..." : "")
|
|
478
|
+
: null,
|
|
479
|
+
})),
|
|
480
|
+
},
|
|
481
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const rolledBackMigrations = [];
|
|
485
|
+
const failedRollbacks = [];
|
|
486
|
+
for (const migration of migrationsToRollback) {
|
|
487
|
+
const startTime = Date.now();
|
|
488
|
+
try {
|
|
489
|
+
// Execute down_sql statements
|
|
490
|
+
const statements = this.splitSqlStatements(migration.down_sql);
|
|
491
|
+
for (const statement of statements) {
|
|
492
|
+
if (statement.trim()) {
|
|
493
|
+
await this.db.query(statement);
|
|
494
|
+
queryCount++;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const executionTime = Date.now() - startTime;
|
|
498
|
+
// Update migration status to rolled_back
|
|
499
|
+
const updateQuery = `
|
|
500
|
+
UPDATE ${this.migrationsTable}
|
|
501
|
+
SET status = 'rolled_back',
|
|
502
|
+
applied_at = NULL,
|
|
503
|
+
applied_by = NULL,
|
|
504
|
+
execution_time_ms = ?,
|
|
505
|
+
error_message = NULL
|
|
506
|
+
WHERE id = ?
|
|
507
|
+
`;
|
|
508
|
+
await this.db.query(updateQuery, [executionTime, migration.id]);
|
|
509
|
+
queryCount++;
|
|
510
|
+
rolledBackMigrations.push({
|
|
511
|
+
version: migration.version,
|
|
512
|
+
name: migration.name,
|
|
513
|
+
execution_time_ms: executionTime,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
const executionTime = Date.now() - startTime;
|
|
518
|
+
failedRollbacks.push({
|
|
519
|
+
version: migration.version,
|
|
520
|
+
name: migration.name,
|
|
521
|
+
error: error.message,
|
|
522
|
+
execution_time_ms: executionTime,
|
|
523
|
+
});
|
|
524
|
+
// Stop rolling back on failure
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
status: failedRollbacks.length > 0 ? "partial" : "success",
|
|
530
|
+
data: {
|
|
531
|
+
message: failedRollbacks.length > 0
|
|
532
|
+
? `Rolled back ${rolledBackMigrations.length} migration(s), ${failedRollbacks.length} failed`
|
|
533
|
+
: `Successfully rolled back ${rolledBackMigrations.length} migration(s)`,
|
|
534
|
+
rolled_back_count: rolledBackMigrations.length,
|
|
535
|
+
failed_count: failedRollbacks.length,
|
|
536
|
+
rolled_back_migrations: rolledBackMigrations,
|
|
537
|
+
failed_rollbacks: failedRollbacks,
|
|
538
|
+
},
|
|
539
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
return {
|
|
544
|
+
status: "error",
|
|
545
|
+
error: error.message,
|
|
546
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Get migration history and status
|
|
552
|
+
*/
|
|
553
|
+
async getMigrationStatus(params) {
|
|
554
|
+
try {
|
|
555
|
+
const { version, status, limit = 50, database } = params;
|
|
556
|
+
// Validate database access
|
|
557
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
558
|
+
if (!dbValidation.valid) {
|
|
559
|
+
return { status: "error", error: dbValidation.error };
|
|
560
|
+
}
|
|
561
|
+
// Ensure migrations table exists
|
|
562
|
+
await this.initMigrationsTable({ database });
|
|
563
|
+
let queryCount = 1;
|
|
564
|
+
// Build query
|
|
565
|
+
let query = `
|
|
566
|
+
SELECT
|
|
567
|
+
id,
|
|
568
|
+
version,
|
|
569
|
+
name,
|
|
570
|
+
description,
|
|
571
|
+
checksum,
|
|
572
|
+
status,
|
|
573
|
+
applied_at,
|
|
574
|
+
applied_by,
|
|
575
|
+
execution_time_ms,
|
|
576
|
+
error_message,
|
|
577
|
+
created_at
|
|
578
|
+
FROM ${this.migrationsTable}
|
|
579
|
+
WHERE 1=1
|
|
580
|
+
`;
|
|
581
|
+
const queryParams = [];
|
|
582
|
+
if (version) {
|
|
583
|
+
query += ` AND version = ?`;
|
|
584
|
+
queryParams.push(version);
|
|
585
|
+
}
|
|
586
|
+
if (status) {
|
|
587
|
+
query += ` AND status = ?`;
|
|
588
|
+
queryParams.push(status);
|
|
589
|
+
}
|
|
590
|
+
query += ` ORDER BY version DESC LIMIT ?`;
|
|
591
|
+
queryParams.push(limit);
|
|
592
|
+
const migrations = await this.db.query(query, queryParams);
|
|
593
|
+
queryCount++;
|
|
594
|
+
// Get summary statistics
|
|
595
|
+
const summaryQuery = `
|
|
596
|
+
SELECT
|
|
597
|
+
status,
|
|
598
|
+
COUNT(*) as count
|
|
599
|
+
FROM ${this.migrationsTable}
|
|
600
|
+
GROUP BY status
|
|
601
|
+
`;
|
|
602
|
+
const summary = await this.db.query(summaryQuery);
|
|
603
|
+
queryCount++;
|
|
604
|
+
const summaryMap = {};
|
|
605
|
+
for (const row of summary) {
|
|
606
|
+
summaryMap[row.status] = row.count;
|
|
607
|
+
}
|
|
608
|
+
// Get current schema version
|
|
609
|
+
const currentVersionQuery = `
|
|
610
|
+
SELECT version
|
|
611
|
+
FROM ${this.migrationsTable}
|
|
612
|
+
WHERE status = 'applied'
|
|
613
|
+
ORDER BY version DESC
|
|
614
|
+
LIMIT 1
|
|
615
|
+
`;
|
|
616
|
+
const currentVersionResult = await this.db.query(currentVersionQuery);
|
|
617
|
+
queryCount++;
|
|
618
|
+
const currentVersion = currentVersionResult.length > 0
|
|
619
|
+
? currentVersionResult[0].version
|
|
620
|
+
: null;
|
|
621
|
+
return {
|
|
622
|
+
status: "success",
|
|
623
|
+
data: {
|
|
624
|
+
current_version: currentVersion,
|
|
625
|
+
summary: {
|
|
626
|
+
total: Object.values(summaryMap).reduce((a, b) => a + b, 0),
|
|
627
|
+
pending: summaryMap.pending || 0,
|
|
628
|
+
applied: summaryMap.applied || 0,
|
|
629
|
+
failed: summaryMap.failed || 0,
|
|
630
|
+
rolled_back: summaryMap.rolled_back || 0,
|
|
631
|
+
},
|
|
632
|
+
migrations: migrations.map((m) => ({
|
|
633
|
+
...m,
|
|
634
|
+
applied_at: m.applied_at ? m.applied_at.toISOString() : null,
|
|
635
|
+
created_at: m.created_at ? m.created_at.toISOString() : null,
|
|
636
|
+
})),
|
|
637
|
+
},
|
|
638
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
return {
|
|
643
|
+
status: "error",
|
|
644
|
+
error: error.message,
|
|
645
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Get the current schema version
|
|
651
|
+
*/
|
|
652
|
+
async getSchemaVersion(params) {
|
|
653
|
+
try {
|
|
654
|
+
const { database } = params;
|
|
655
|
+
// Validate database access
|
|
656
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
657
|
+
if (!dbValidation.valid) {
|
|
658
|
+
return { status: "error", error: dbValidation.error };
|
|
659
|
+
}
|
|
660
|
+
// Check if migrations table exists
|
|
661
|
+
const tableExistsQuery = `
|
|
662
|
+
SELECT COUNT(*) as cnt
|
|
663
|
+
FROM information_schema.tables
|
|
664
|
+
WHERE table_schema = DATABASE()
|
|
665
|
+
AND table_name = ?
|
|
666
|
+
`;
|
|
667
|
+
const tableExists = await this.db.query(tableExistsQuery, [this.migrationsTable]);
|
|
668
|
+
if (tableExists[0].cnt === 0) {
|
|
669
|
+
return {
|
|
670
|
+
status: "success",
|
|
671
|
+
data: {
|
|
672
|
+
current_version: null,
|
|
673
|
+
message: "No migrations have been tracked yet",
|
|
674
|
+
migrations_table_exists: false,
|
|
675
|
+
},
|
|
676
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
// Get current version
|
|
680
|
+
const versionQuery = `
|
|
681
|
+
SELECT
|
|
682
|
+
version,
|
|
683
|
+
name,
|
|
684
|
+
applied_at
|
|
685
|
+
FROM ${this.migrationsTable}
|
|
686
|
+
WHERE status = 'applied'
|
|
687
|
+
ORDER BY version DESC
|
|
688
|
+
LIMIT 1
|
|
689
|
+
`;
|
|
690
|
+
const versionResult = await this.db.query(versionQuery);
|
|
691
|
+
if (versionResult.length === 0) {
|
|
692
|
+
return {
|
|
693
|
+
status: "success",
|
|
694
|
+
data: {
|
|
695
|
+
current_version: null,
|
|
696
|
+
message: "No migrations have been applied yet",
|
|
697
|
+
migrations_table_exists: true,
|
|
698
|
+
},
|
|
699
|
+
queryLog: this.db.getFormattedQueryLogs(2),
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
const current = versionResult[0];
|
|
703
|
+
// Count pending migrations
|
|
704
|
+
const pendingQuery = `
|
|
705
|
+
SELECT COUNT(*) as cnt
|
|
706
|
+
FROM ${this.migrationsTable}
|
|
707
|
+
WHERE status = 'pending'
|
|
708
|
+
`;
|
|
709
|
+
const pendingResult = await this.db.query(pendingQuery);
|
|
710
|
+
return {
|
|
711
|
+
status: "success",
|
|
712
|
+
data: {
|
|
713
|
+
current_version: current.version,
|
|
714
|
+
current_migration_name: current.name,
|
|
715
|
+
applied_at: current.applied_at ? current.applied_at.toISOString() : null,
|
|
716
|
+
pending_migrations: pendingResult[0].cnt,
|
|
717
|
+
migrations_table_exists: true,
|
|
718
|
+
},
|
|
719
|
+
queryLog: this.db.getFormattedQueryLogs(3),
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
catch (error) {
|
|
723
|
+
return {
|
|
724
|
+
status: "error",
|
|
725
|
+
error: error.message,
|
|
726
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Validate pending migrations (check for conflicts or issues)
|
|
732
|
+
*/
|
|
733
|
+
async validateMigrations(params) {
|
|
734
|
+
try {
|
|
735
|
+
const { database } = params;
|
|
736
|
+
// Validate database access
|
|
737
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
738
|
+
if (!dbValidation.valid) {
|
|
739
|
+
return { status: "error", error: dbValidation.error };
|
|
740
|
+
}
|
|
741
|
+
// Ensure migrations table exists
|
|
742
|
+
await this.initMigrationsTable({ database });
|
|
743
|
+
let queryCount = 1;
|
|
744
|
+
// Get all migrations
|
|
745
|
+
const migrationsQuery = `
|
|
746
|
+
SELECT
|
|
747
|
+
id,
|
|
748
|
+
version,
|
|
749
|
+
name,
|
|
750
|
+
up_sql,
|
|
751
|
+
down_sql,
|
|
752
|
+
checksum,
|
|
753
|
+
status
|
|
754
|
+
FROM ${this.migrationsTable}
|
|
755
|
+
ORDER BY version ASC
|
|
756
|
+
`;
|
|
757
|
+
const migrations = await this.db.query(migrationsQuery);
|
|
758
|
+
queryCount++;
|
|
759
|
+
const issues = [];
|
|
760
|
+
const warnings = [];
|
|
761
|
+
// Check for duplicate versions
|
|
762
|
+
const versionCounts = new Map();
|
|
763
|
+
for (const m of migrations) {
|
|
764
|
+
versionCounts.set(m.version, (versionCounts.get(m.version) || 0) + 1);
|
|
765
|
+
}
|
|
766
|
+
for (const [version, count] of versionCounts) {
|
|
767
|
+
if (count > 1) {
|
|
768
|
+
issues.push({
|
|
769
|
+
type: "duplicate_version",
|
|
770
|
+
version,
|
|
771
|
+
message: `Version '${version}' appears ${count} times`,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// Check for missing down_sql
|
|
776
|
+
for (const m of migrations) {
|
|
777
|
+
if (!m.down_sql) {
|
|
778
|
+
warnings.push({
|
|
779
|
+
type: "missing_down_sql",
|
|
780
|
+
version: m.version,
|
|
781
|
+
name: m.name,
|
|
782
|
+
message: `Migration '${m.name}' (${m.version}) has no down_sql - rollback will not be possible`,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// Verify checksums for applied migrations
|
|
787
|
+
for (const m of migrations) {
|
|
788
|
+
if (m.status === "applied") {
|
|
789
|
+
const expectedChecksum = this.generateChecksum(m.up_sql);
|
|
790
|
+
if (m.checksum !== expectedChecksum) {
|
|
791
|
+
issues.push({
|
|
792
|
+
type: "checksum_mismatch",
|
|
793
|
+
version: m.version,
|
|
794
|
+
name: m.name,
|
|
795
|
+
message: `Migration '${m.name}' (${m.version}) checksum mismatch - migration may have been modified after being applied`,
|
|
796
|
+
stored_checksum: m.checksum,
|
|
797
|
+
calculated_checksum: expectedChecksum,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// Check for failed migrations that block pending ones
|
|
803
|
+
const failedMigrations = migrations.filter((m) => m.status === "failed");
|
|
804
|
+
if (failedMigrations.length > 0) {
|
|
805
|
+
const pendingAfterFailed = migrations.filter((m) => m.status === "pending" && m.version > failedMigrations[0].version);
|
|
806
|
+
if (pendingAfterFailed.length > 0) {
|
|
807
|
+
warnings.push({
|
|
808
|
+
type: "blocked_migrations",
|
|
809
|
+
message: `${pendingAfterFailed.length} pending migration(s) are blocked by failed migration '${failedMigrations[0].name}' (${failedMigrations[0].version})`,
|
|
810
|
+
failed_version: failedMigrations[0].version,
|
|
811
|
+
blocked_versions: pendingAfterFailed.map((m) => m.version),
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const isValid = issues.length === 0;
|
|
816
|
+
return {
|
|
817
|
+
status: "success",
|
|
818
|
+
data: {
|
|
819
|
+
valid: isValid,
|
|
820
|
+
total_migrations: migrations.length,
|
|
821
|
+
issues_count: issues.length,
|
|
822
|
+
warnings_count: warnings.length,
|
|
823
|
+
issues,
|
|
824
|
+
warnings,
|
|
825
|
+
summary: {
|
|
826
|
+
pending: migrations.filter((m) => m.status === "pending").length,
|
|
827
|
+
applied: migrations.filter((m) => m.status === "applied").length,
|
|
828
|
+
failed: migrations.filter((m) => m.status === "failed").length,
|
|
829
|
+
rolled_back: migrations.filter((m) => m.status === "rolled_back").length,
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
queryLog: this.db.getFormattedQueryLogs(queryCount),
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
return {
|
|
837
|
+
status: "error",
|
|
838
|
+
error: error.message,
|
|
839
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Mark a failed migration as resolved (reset to pending status)
|
|
845
|
+
*/
|
|
846
|
+
async resetFailedMigration(params) {
|
|
847
|
+
try {
|
|
848
|
+
const { version, database } = params;
|
|
849
|
+
// Validate database access
|
|
850
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
851
|
+
if (!dbValidation.valid) {
|
|
852
|
+
return { status: "error", error: dbValidation.error };
|
|
853
|
+
}
|
|
854
|
+
// Check if migration exists and is failed
|
|
855
|
+
const checkQuery = `
|
|
856
|
+
SELECT id, name, status
|
|
857
|
+
FROM ${this.migrationsTable}
|
|
858
|
+
WHERE version = ?
|
|
859
|
+
`;
|
|
860
|
+
const migration = await this.db.query(checkQuery, [version]);
|
|
861
|
+
if (migration.length === 0) {
|
|
862
|
+
return {
|
|
863
|
+
status: "error",
|
|
864
|
+
error: `Migration version '${version}' not found`,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
if (migration[0].status !== "failed") {
|
|
868
|
+
return {
|
|
869
|
+
status: "error",
|
|
870
|
+
error: `Migration '${version}' is not in failed status (current status: ${migration[0].status})`,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
// Reset to pending
|
|
874
|
+
const updateQuery = `
|
|
875
|
+
UPDATE ${this.migrationsTable}
|
|
876
|
+
SET status = 'pending',
|
|
877
|
+
error_message = NULL,
|
|
878
|
+
execution_time_ms = NULL
|
|
879
|
+
WHERE version = ?
|
|
880
|
+
`;
|
|
881
|
+
await this.db.query(updateQuery, [version]);
|
|
882
|
+
return {
|
|
883
|
+
status: "success",
|
|
884
|
+
data: {
|
|
885
|
+
message: `Migration '${migration[0].name}' (${version}) has been reset to pending status`,
|
|
886
|
+
version,
|
|
887
|
+
name: migration[0].name,
|
|
888
|
+
previous_status: "failed",
|
|
889
|
+
new_status: "pending",
|
|
890
|
+
},
|
|
891
|
+
queryLog: this.db.getFormattedQueryLogs(2),
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
catch (error) {
|
|
895
|
+
return {
|
|
896
|
+
status: "error",
|
|
897
|
+
error: error.message,
|
|
898
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Generate a migration from table comparison
|
|
904
|
+
*/
|
|
905
|
+
async generateMigrationFromDiff(params) {
|
|
906
|
+
try {
|
|
907
|
+
const { table1, table2, migration_name, database } = params;
|
|
908
|
+
// Validate database access
|
|
909
|
+
const dbValidation = this.validateDatabaseAccess(database);
|
|
910
|
+
if (!dbValidation.valid) {
|
|
911
|
+
return { status: "error", error: dbValidation.error };
|
|
912
|
+
}
|
|
913
|
+
// Validate table names
|
|
914
|
+
const table1Validation = this.security.validateIdentifier(table1);
|
|
915
|
+
if (!table1Validation.valid) {
|
|
916
|
+
return { status: "error", error: `Invalid table1 name: ${table1Validation.error}` };
|
|
917
|
+
}
|
|
918
|
+
const table2Validation = this.security.validateIdentifier(table2);
|
|
919
|
+
if (!table2Validation.valid) {
|
|
920
|
+
return { status: "error", error: `Invalid table2 name: ${table2Validation.error}` };
|
|
921
|
+
}
|
|
922
|
+
const escapedTable1 = this.security.escapeIdentifier(table1);
|
|
923
|
+
const escapedTable2 = this.security.escapeIdentifier(table2);
|
|
924
|
+
// Get columns for both tables
|
|
925
|
+
const cols1 = await this.db.query(`SHOW COLUMNS FROM ${escapedTable1}`);
|
|
926
|
+
const cols2 = await this.db.query(`SHOW COLUMNS FROM ${escapedTable2}`);
|
|
927
|
+
const columns1 = new Map(cols1.map((c) => [c.Field, c]));
|
|
928
|
+
const columns2 = new Map(cols2.map((c) => [c.Field, c]));
|
|
929
|
+
const upStatements = [];
|
|
930
|
+
const downStatements = [];
|
|
931
|
+
// Find columns only in table1 (to add to table2)
|
|
932
|
+
for (const [name, col] of columns1) {
|
|
933
|
+
if (!columns2.has(name)) {
|
|
934
|
+
const nullable = col.Null === "YES" ? "NULL" : "NOT NULL";
|
|
935
|
+
const defaultVal = col.Default !== null ? ` DEFAULT ${this.escapeValue(col.Default)}` : "";
|
|
936
|
+
upStatements.push(`ALTER TABLE ${escapedTable2} ADD COLUMN \`${name}\` ${col.Type} ${nullable}${defaultVal};`);
|
|
937
|
+
downStatements.push(`ALTER TABLE ${escapedTable2} DROP COLUMN \`${name}\`;`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// Find columns only in table2 (to remove from table2 to match table1)
|
|
941
|
+
for (const [name, col] of columns2) {
|
|
942
|
+
if (!columns1.has(name)) {
|
|
943
|
+
upStatements.push(`ALTER TABLE ${escapedTable2} DROP COLUMN \`${name}\`;`);
|
|
944
|
+
const nullable = col.Null === "YES" ? "NULL" : "NOT NULL";
|
|
945
|
+
const defaultVal = col.Default !== null ? ` DEFAULT ${this.escapeValue(col.Default)}` : "";
|
|
946
|
+
downStatements.push(`ALTER TABLE ${escapedTable2} ADD COLUMN \`${name}\` ${col.Type} ${nullable}${defaultVal};`);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
// Find columns with different types
|
|
950
|
+
for (const [name, col1] of columns1) {
|
|
951
|
+
const col2 = columns2.get(name);
|
|
952
|
+
if (col2 && col1.Type !== col2.Type) {
|
|
953
|
+
const nullable1 = col1.Null === "YES" ? "NULL" : "NOT NULL";
|
|
954
|
+
const nullable2 = col2.Null === "YES" ? "NULL" : "NOT NULL";
|
|
955
|
+
upStatements.push(`ALTER TABLE ${escapedTable2} MODIFY COLUMN \`${name}\` ${col1.Type} ${nullable1};`);
|
|
956
|
+
downStatements.push(`ALTER TABLE ${escapedTable2} MODIFY COLUMN \`${name}\` ${col2.Type} ${nullable2};`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
if (upStatements.length === 0) {
|
|
960
|
+
return {
|
|
961
|
+
status: "success",
|
|
962
|
+
data: {
|
|
963
|
+
message: "No differences found between tables - no migration needed",
|
|
964
|
+
table1,
|
|
965
|
+
table2,
|
|
966
|
+
differences: 0,
|
|
967
|
+
},
|
|
968
|
+
queryLog: this.db.getFormattedQueryLogs(2),
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
const upSql = upStatements.join("\n");
|
|
972
|
+
const downSql = downStatements.join("\n");
|
|
973
|
+
// Create the migration
|
|
974
|
+
const createResult = await this.createMigration({
|
|
975
|
+
name: migration_name,
|
|
976
|
+
up_sql: upSql,
|
|
977
|
+
down_sql: downSql,
|
|
978
|
+
description: `Auto-generated migration to transform ${table2} structure to match ${table1}`,
|
|
979
|
+
database,
|
|
980
|
+
});
|
|
981
|
+
if (createResult.status === "error") {
|
|
982
|
+
return createResult;
|
|
983
|
+
}
|
|
984
|
+
return {
|
|
985
|
+
status: "success",
|
|
986
|
+
data: {
|
|
987
|
+
message: `Migration '${migration_name}' generated with ${upStatements.length} change(s)`,
|
|
988
|
+
version: createResult.data?.version,
|
|
989
|
+
changes_count: upStatements.length,
|
|
990
|
+
up_sql: upSql,
|
|
991
|
+
down_sql: downSql,
|
|
992
|
+
source_table: table1,
|
|
993
|
+
target_table: table2,
|
|
994
|
+
},
|
|
995
|
+
queryLog: this.db.getFormattedQueryLogs(4),
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
return {
|
|
1000
|
+
status: "error",
|
|
1001
|
+
error: error.message,
|
|
1002
|
+
queryLog: this.db.getFormattedQueryLogs(1),
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
exports.SchemaVersioningTools = SchemaVersioningTools;
|