codecrucible-synth
Version:
Production-Ready AI Development Platform with Multi-Voice Synthesis, Smithery MCP Integration, Enterprise Security, and Zero-Timeout Reliability
433 lines (363 loc) • 12 kB
text/typescript
/**
* Database Migration Manager - Enterprise Grade Versioning System
* Extends existing database-manager.ts with migration capabilities
*/
import { ProductionDatabaseManager as DatabaseManager } from './production-database-manager.js';
import { logger } from '../core/logger.js';
import { readFileSync, readdirSync } from 'fs';
import { join } from 'path';
import { createHash } from 'crypto';
export interface Migration {
version: string;
name: string;
description: string;
up: string;
down: string;
checksum: string;
appliedAt?: Date;
}
export interface MigrationResult {
success: boolean;
version: string;
error?: string;
duration: number;
}
/**
* Migration Manager for CodeCrucible Database
* Works WITH existing DatabaseManager, not against it
*/
export class MigrationManager {
private dbManager: DatabaseManager;
private migrationsPath: string;
constructor(dbManager: DatabaseManager, migrationsPath?: string) {
this.dbManager = dbManager;
this.migrationsPath = migrationsPath || join(process.cwd(), 'migrations');
}
/**
* Initialize migration tracking table
*/
async initialize(): Promise<void> {
if (!this.dbManager.isInitialized()) {
throw new Error('Database manager must be initialized first');
}
const db = this.dbManager.getRawDb();
if (!db) throw new Error('Database connection not available');
// Create migrations tracking table
db.exec(`
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
checksum TEXT NOT NULL,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
execution_time_ms INTEGER
)
`);
logger.info('Migration tracking initialized');
}
/**
* Get all pending migrations
*/
async getPendingMigrations(): Promise<Migration[]> {
const allMigrations = await this.loadMigrations();
const appliedMigrations = await this.getAppliedMigrations();
const appliedVersions = new Set(appliedMigrations.map(m => m.version));
return allMigrations.filter(m => !appliedVersions.has(m.version));
}
/**
* Get applied migrations from database
*/
async getAppliedMigrations(): Promise<Migration[]> {
const db = this.dbManager.getRawDb();
if (!db) throw new Error('Database connection not available');
const stmt = db.prepare(`
SELECT version, name, description, checksum, applied_at, execution_time_ms
FROM schema_migrations
ORDER BY version ASC
`);
const rows = stmt.all() as any[];
return rows.map(row => ({
version: row.version,
name: row.name,
description: row.description,
checksum: row.checksum,
appliedAt: new Date(row.applied_at),
up: '', // Not stored in DB
down: '', // Not stored in DB
}));
}
/**
* Load migration files from disk
*/
private async loadMigrations(): Promise<Migration[]> {
try {
const files = readdirSync(this.migrationsPath)
.filter(f => f.endsWith('.sql'))
.sort();
const migrations: Migration[] = [];
for (const file of files) {
const filePath = join(this.migrationsPath, file);
const content = readFileSync(filePath, 'utf-8');
// Parse migration file format:
// -- Migration: version
// -- Name: name
// -- Description: description
// -- Up
// SQL statements...
// -- Down
// SQL statements...
const lines = content.split('\n');
let version = '';
let name = '';
let description = '';
let up = '';
let down = '';
let section = '';
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('-- Migration:')) {
version = trimmed.replace('-- Migration:', '').trim();
} else if (trimmed.startsWith('-- Name:')) {
name = trimmed.replace('-- Name:', '').trim();
} else if (trimmed.startsWith('-- Description:')) {
description = trimmed.replace('-- Description:', '').trim();
} else if (trimmed === '-- Up') {
section = 'up';
} else if (trimmed === '-- Down') {
section = 'down';
} else if (section === 'up' && !trimmed.startsWith('--')) {
up += `${line}\n`;
} else if (section === 'down' && !trimmed.startsWith('--')) {
down += `${line}\n`;
}
}
if (version && name) {
migrations.push({
version,
name,
description,
up: up.trim(),
down: down.trim(),
checksum: this.calculateChecksum(up),
});
}
}
return migrations;
} catch (error) {
logger.error('Failed to load migrations:', error);
return [];
}
}
/**
* Run all pending migrations
*/
async migrate(): Promise<MigrationResult[]> {
const pending = await this.getPendingMigrations();
const results: MigrationResult[] = [];
logger.info(`Running ${pending.length} pending migrations`);
for (const migration of pending) {
const result = await this.runMigration(migration);
results.push(result);
if (!result.success) {
logger.error(`Migration failed: ${migration.version} - ${result.error}`);
break; // Stop on first failure
}
}
return results;
}
/**
* Run a single migration
*/
private async runMigration(migration: Migration): Promise<MigrationResult> {
const startTime = Date.now();
const db = this.dbManager.getRawDb();
if (!db) throw new Error('Database connection not available');
try {
logger.info(`Applying migration: ${migration.version} - ${migration.name}`);
// Start transaction
db.exec('BEGIN TRANSACTION');
try {
// Execute migration SQL
db.exec(migration.up);
// Record migration in tracking table
const stmt = db.prepare(`
INSERT INTO schema_migrations
(version, name, description, checksum, execution_time_ms)
VALUES (?, ?, ?, ?, ?)
`);
const duration = Date.now() - startTime;
stmt.run(
migration.version,
migration.name,
migration.description,
migration.checksum,
duration
);
// Commit transaction
db.exec('COMMIT');
logger.info(`Migration completed: ${migration.version} (${duration}ms)`);
return {
success: true,
version: migration.version,
duration,
};
} catch (error) {
// Rollback on error
db.exec('ROLLBACK');
throw error;
}
} catch (error) {
const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`Migration failed: ${migration.version}`, error);
return {
success: false,
version: migration.version,
error: errorMessage,
duration,
};
}
}
/**
* Rollback last migration
*/
async rollback(): Promise<MigrationResult> {
const applied = await this.getAppliedMigrations();
if (applied.length === 0) {
throw new Error('No migrations to rollback');
}
const lastMigration = applied[applied.length - 1];
const allMigrations = await this.loadMigrations();
const migrationDef = allMigrations.find(m => m.version === lastMigration.version);
if (!migrationDef) {
throw new Error(`Migration definition not found for version: ${lastMigration.version}`);
}
const startTime = Date.now();
const db = this.dbManager.getRawDb();
if (!db) throw new Error('Database connection not available');
try {
logger.info(`Rolling back migration: ${lastMigration.version} - ${lastMigration.name}`);
// Start transaction
db.exec('BEGIN TRANSACTION');
try {
// Execute rollback SQL
if (migrationDef.down) {
db.exec(migrationDef.down);
}
// Remove migration record
const stmt = db.prepare('DELETE FROM schema_migrations WHERE version = ?');
stmt.run(lastMigration.version);
// Commit transaction
db.exec('COMMIT');
const duration = Date.now() - startTime;
logger.info(`Migration rolled back: ${lastMigration.version} (${duration}ms)`);
return {
success: true,
version: lastMigration.version,
duration,
};
} catch (error) {
// Rollback on error
db.exec('ROLLBACK');
throw error;
}
} catch (error) {
const duration = Date.now() - startTime;
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.error(`Rollback failed: ${lastMigration.version}`, error);
return {
success: false,
version: lastMigration.version,
error: errorMessage,
duration,
};
}
}
/**
* Get migration status
*/
async getStatus(): Promise<{
currentVersion: string | null;
pendingCount: number;
appliedCount: number;
appliedMigrations: Migration[];
pendingMigrations: Migration[];
}> {
const applied = await this.getAppliedMigrations();
const pending = await this.getPendingMigrations();
return {
currentVersion: applied.length > 0 ? applied[applied.length - 1].version : null,
pendingCount: pending.length,
appliedCount: applied.length,
appliedMigrations: applied,
pendingMigrations: pending,
};
}
/**
* Validate all applied migrations
*/
async validateMigrations(): Promise<{
valid: boolean;
issues: string[];
}> {
const applied = await this.getAppliedMigrations();
const allMigrations = await this.loadMigrations();
const issues: string[] = [];
for (const appliedMigration of applied) {
const migrationDef = allMigrations.find(m => m.version === appliedMigration.version);
if (!migrationDef) {
issues.push(`Missing migration file for applied version: ${appliedMigration.version}`);
continue;
}
// Validate checksum
const currentChecksum = this.calculateChecksum(migrationDef.up);
if (currentChecksum !== appliedMigration.checksum) {
issues.push(`Checksum mismatch for migration: ${appliedMigration.version}`);
}
}
return {
valid: issues.length === 0,
issues,
};
}
/**
* Create a new migration file template
*/
async createMigration(name: string, description?: string): Promise<string> {
const timestamp = new Date().toISOString().replace(/[:-]/g, '').replace(/\..+/, '');
const version = `${timestamp}_${name.toLowerCase().replace(/\s+/g, '_')}`;
const filename = `${version}.sql`;
const filepath = join(this.migrationsPath, filename);
const template = `-- Migration: ${version}
-- Name: ${name}
-- Description: ${description || 'Add description here'}
-- Up
-- Add your migration SQL here
-- Down
-- Add your rollback SQL here
`;
try {
const fs = await import('fs/promises');
await fs.mkdir(this.migrationsPath, { recursive: true });
await fs.writeFile(filepath, template);
logger.info(`Created migration: ${filename}`);
return filepath;
} catch (error) {
logger.error('Failed to create migration:', error);
throw error;
}
}
/**
* Calculate checksum for migration content
*/
private calculateChecksum(content: string): string {
return createHash('sha256').update(content).digest('hex');
}
/**
* Get database schema version
*/
async getSchemaVersion(): Promise<string | null> {
const applied = await this.getAppliedMigrations();
return applied.length > 0 ? applied[applied.length - 1].version : null;
}
}