@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.
@@ -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;