forked from trent_larson/crowd-funder-for-time-pwa
feat: Enhance database migration system with better logging and schema detection
- Add comprehensive console logging for Electron with emojis for better visibility - Use CREATE TABLE IF NOT EXISTS and INSERT OR IGNORE to prevent duplicate creation errors - Add specialized column existence checking for ALTER TABLE ADD COLUMN operations - Improve migration tracking with detailed status reporting (applied/skipped counts) - Add proper error handling for existing schema scenarios - Enhanced documentation and type safety for migration system This resolves issues where migrations would fail with 'table already exists' or 'duplicate column' errors when the database schema was already properly set up. The enhanced logging makes it clear to users when migrations are being skipped vs. applied, improving the debugging experience in Electron.
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
/**
|
||||
* Manage database migrations as people upgrade their app over time
|
||||
* Database Migration Service for TimeSafari
|
||||
*
|
||||
* Manages database migrations as people upgrade their app over time.
|
||||
* Provides safe, tracked migrations with rollback capabilities and
|
||||
* detailed logging for debugging.
|
||||
*
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
import { logger } from "../utils/logger";
|
||||
@@ -78,6 +84,8 @@ export async function runMigrations<T>(
|
||||
extractMigrationNames: (result: T) => Set<string>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.log("📋 [Migration] Checking migration status...");
|
||||
|
||||
// Create migrations table if it doesn't exist
|
||||
await sqlExec(`
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
@@ -96,28 +104,45 @@ export async function runMigrations<T>(
|
||||
const migrations = migrationRegistry.getMigrations();
|
||||
|
||||
if (migrations.length === 0) {
|
||||
console.log("⚠️ [Migration] No migrations registered");
|
||||
logger.warn("[MigrationService] No migrations registered");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`);
|
||||
|
||||
let appliedCount = 0;
|
||||
let skippedCount = 0;
|
||||
|
||||
// Run each migration that hasn't been applied yet
|
||||
for (const migration of migrations) {
|
||||
if (appliedMigrations.has(migration.name)) {
|
||||
console.log(`⏭️ [Migration] Skipping already applied: ${migration.name}`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`🔄 [Migration] Applying migration: ${migration.name}`);
|
||||
|
||||
try {
|
||||
// Execute the migration SQL
|
||||
await sqlExec(migration.sql);
|
||||
// Special handling for column addition migrations
|
||||
if (migration.sql.includes("ALTER TABLE") && migration.sql.includes("ADD COLUMN")) {
|
||||
await handleColumnAddition(migration, sqlExec, sqlQuery);
|
||||
} else {
|
||||
// Execute the migration SQL
|
||||
await sqlExec(migration.sql);
|
||||
}
|
||||
|
||||
// Record that the migration was applied
|
||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
|
||||
console.log(`✅ [Migration] Successfully applied: ${migration.name}`);
|
||||
logger.info(
|
||||
`[MigrationService] Successfully applied migration: ${migration.name}`,
|
||||
);
|
||||
appliedCount++;
|
||||
} catch (error) {
|
||||
// Handle specific cases where the migration might be partially applied
|
||||
const errorMessage = String(error).toLowerCase();
|
||||
@@ -128,6 +153,7 @@ export async function runMigrations<T>(
|
||||
errorMessage.includes("column already exists") ||
|
||||
errorMessage.includes("already exists")
|
||||
) {
|
||||
console.log(`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Marking as complete.`);
|
||||
logger.warn(
|
||||
`[MigrationService] Migration ${migration.name} appears to be already applied (${errorMessage}). Marking as complete.`,
|
||||
);
|
||||
@@ -137,11 +163,14 @@ export async function runMigrations<T>(
|
||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||
migration.name,
|
||||
]);
|
||||
console.log(`✅ [Migration] Marked as applied: ${migration.name}`);
|
||||
logger.info(
|
||||
`[MigrationService] Successfully marked migration as applied: ${migration.name}`,
|
||||
);
|
||||
appliedCount++;
|
||||
} catch (insertError) {
|
||||
// If we can't insert the migration record, log it but don't fail
|
||||
console.warn(`⚠️ [Migration] Could not record ${migration.name} as applied:`, insertError);
|
||||
logger.warn(
|
||||
`[MigrationService] Could not record migration ${migration.name} as applied:`,
|
||||
insertError,
|
||||
@@ -149,6 +178,7 @@ export async function runMigrations<T>(
|
||||
}
|
||||
} else {
|
||||
// For other types of errors, still fail the migration
|
||||
console.error(`❌ [Migration] Failed to apply ${migration.name}:`, error);
|
||||
logger.error(
|
||||
`[MigrationService] Failed to apply migration ${migration.name}:`,
|
||||
error,
|
||||
@@ -157,8 +187,62 @@ export async function runMigrations<T>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🎉 [Migration] Migration process complete! Applied: ${appliedCount}, Skipped: ${skippedCount}`);
|
||||
} catch (error) {
|
||||
console.error("💥 [Migration] Migration process failed:", error);
|
||||
logger.error("[MigrationService] Migration process failed:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle column addition migrations with proper existence checking
|
||||
*
|
||||
* @param migration - The migration containing column addition
|
||||
* @param sqlExec - Function to execute SQL statements
|
||||
* @param sqlQuery - Function to query SQL data
|
||||
*/
|
||||
async function handleColumnAddition<T>(
|
||||
migration: Migration,
|
||||
sqlExec: (sql: string, params?: unknown[]) => Promise<unknown>,
|
||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
||||
): Promise<void> {
|
||||
// Extract table name and column name from ALTER TABLE statement
|
||||
const alterMatch = migration.sql.match(/ALTER TABLE\s+(\w+)\s+ADD COLUMN\s+(\w+)/i);
|
||||
|
||||
if (!alterMatch) {
|
||||
// If we can't parse it, just try to execute normally
|
||||
await sqlExec(migration.sql);
|
||||
return;
|
||||
}
|
||||
|
||||
const [, tableName, columnName] = alterMatch;
|
||||
|
||||
try {
|
||||
// Check if column already exists
|
||||
const columnCheckResult = await sqlQuery(
|
||||
`SELECT COUNT(*) as count FROM pragma_table_info('${tableName}') WHERE name = ?`,
|
||||
[columnName]
|
||||
) as any;
|
||||
|
||||
const columnExists = columnCheckResult?.values?.[0]?.count > 0 ||
|
||||
columnCheckResult?.count > 0 ||
|
||||
(Array.isArray(columnCheckResult) && columnCheckResult.length > 0);
|
||||
|
||||
if (columnExists) {
|
||||
console.log(`⏭️ [Migration] Column ${columnName} already exists in table ${tableName}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Column doesn't exist, so add it
|
||||
console.log(`🔄 [Migration] Adding column ${columnName} to table ${tableName}`);
|
||||
await sqlExec(migration.sql);
|
||||
console.log(`✅ [Migration] Successfully added column ${columnName} to table ${tableName}`);
|
||||
|
||||
} catch (error) {
|
||||
// If the column check fails, try the original migration and let error handling catch duplicates
|
||||
console.log(`⚠️ [Migration] Column check failed, attempting migration anyway: ${error}`);
|
||||
await sqlExec(migration.sql);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user