diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index bb6f72e5..46f47ed4 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -40,12 +40,13 @@ const randomBytes = crypto.getRandomValues(new Uint8Array(32)); const secretBase64 = arrayBufferToBase64(randomBytes); // Each migration can include multiple SQL statements (with semicolons) +// NOTE: These should run only once per migration. The migration system tracks +// which migrations have been applied in the 'migrations' table. const MIGRATIONS = [ { name: "001_initial", sql: ` - -- Create accounts table only if it doesn't exist - CREATE TABLE IF NOT EXISTS accounts ( + CREATE TABLE accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, dateCreated TEXT NOT NULL, derivationPath TEXT, @@ -56,19 +57,16 @@ const MIGRATIONS = [ publicKeyHex TEXT NOT NULL ); - CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did); + CREATE INDEX idx_accounts_did ON accounts(did); - -- Create secret table only if it doesn't exist - CREATE TABLE IF NOT EXISTS secret ( + CREATE TABLE secret ( id INTEGER PRIMARY KEY AUTOINCREMENT, secretBase64 TEXT NOT NULL ); - -- Insert secret only if table is empty - INSERT OR IGNORE INTO secret (id, secretBase64) VALUES (1, '${secretBase64}'); + INSERT INTO secret (id, secretBase64) VALUES (1, '${secretBase64}'); - -- Create settings table only if it doesn't exist - CREATE TABLE IF NOT EXISTS settings ( + CREATE TABLE settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, accountDid TEXT, activeDid TEXT, @@ -100,13 +98,11 @@ const MIGRATIONS = [ webPushServer TEXT ); - CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid); + CREATE INDEX idx_settings_accountDid ON settings(accountDid); - -- Insert default settings only if table is empty - INSERT OR IGNORE INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}'); + INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}'); - -- Create contacts table only if it doesn't exist - CREATE TABLE IF NOT EXISTS contacts ( + CREATE TABLE contacts ( id INTEGER PRIMARY KEY AUTOINCREMENT, did TEXT NOT NULL, name TEXT, @@ -119,17 +115,15 @@ const MIGRATIONS = [ registered BOOLEAN ); - CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did); - CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name); + CREATE INDEX idx_contacts_did ON contacts(did); + CREATE INDEX idx_contacts_name ON contacts(name); - -- Create logs table only if it doesn't exist - CREATE TABLE IF NOT EXISTS logs ( + CREATE TABLE logs ( date TEXT NOT NULL, message TEXT NOT NULL ); - -- Create temp table only if it doesn't exist - CREATE TABLE IF NOT EXISTS temp ( + CREATE TABLE temp ( id TEXT PRIMARY KEY, blobB64 TEXT ); @@ -139,7 +133,6 @@ const MIGRATIONS = [ name: "002_add_iViewContent_to_contacts", sql: ` -- Add iViewContent column to contacts table - -- The migration service will check if the column already exists ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE; `, }, diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index 4ff38231..6da03c14 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -87,18 +87,24 @@ export async function runMigrations( console.log("📋 [Migration] Checking migration status..."); // Create migrations table if it doesn't exist + console.log("🔧 [Migration] Creating migrations table if it doesn't exist..."); await sqlExec(` CREATE TABLE IF NOT EXISTS migrations ( name TEXT PRIMARY KEY, applied_at TEXT DEFAULT CURRENT_TIMESTAMP ); `); + console.log("✅ [Migration] Migrations table ready"); // Get list of already applied migrations + console.log("🔍 [Migration] Querying existing migrations..."); const appliedMigrationsResult = await sqlQuery( "SELECT name FROM migrations", ); + console.log("📊 [Migration] Raw query result:", appliedMigrationsResult); + const appliedMigrations = extractMigrationNames(appliedMigrationsResult); + console.log("📋 [Migration] Extracted applied migrations:", Array.from(appliedMigrations)); // Get all registered migrations const migrations = migrationRegistry.getMigrations(); @@ -110,6 +116,7 @@ export async function runMigrations( } console.log(`📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`); + console.log(`📝 [Migration] Registered migrations: ${migrations.map(m => m.name).join(', ')}`); let appliedCount = 0; let skippedCount = 0; @@ -125,18 +132,17 @@ export async function runMigrations( console.log(`🔄 [Migration] Applying migration: ${migration.name}`); try { - // 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); - } + // Execute the migration SQL + console.log(`🔧 [Migration] Executing SQL for ${migration.name}...`); + await sqlExec(migration.sql); + console.log(`✅ [Migration] SQL executed successfully for ${migration.name}`); // Record that the migration was applied - await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ + console.log(`📝 [Migration] Recording migration ${migration.name} as applied...`); + const insertResult = await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ migration.name, ]); + console.log(`✅ [Migration] Migration record inserted:`, insertResult); console.log(`✅ [Migration] Successfully applied: ${migration.name}`); logger.info( @@ -144,14 +150,17 @@ export async function runMigrations( ); appliedCount++; } catch (error) { + console.error(`❌ [Migration] Error applying ${migration.name}:`, error); + // Handle specific cases where the migration might be partially applied const errorMessage = String(error).toLowerCase(); - // Check if it's a duplicate column error - this means the column already exists + // Check if it's a duplicate table/column error - this means the schema already exists if ( errorMessage.includes("duplicate column") || errorMessage.includes("column already exists") || - errorMessage.includes("already exists") + errorMessage.includes("already exists") || + errorMessage.includes("table") && errorMessage.includes("already exists") ) { console.log(`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Marking as complete.`); logger.warn( @@ -160,9 +169,11 @@ export async function runMigrations( // Mark the migration as applied since the schema change already exists try { - await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ + console.log(`📝 [Migration] Attempting to record ${migration.name} as applied despite error...`); + const insertResult = await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ migration.name, ]); + console.log(`✅ [Migration] Migration record inserted after error:`, insertResult); console.log(`✅ [Migration] Marked as applied: ${migration.name}`); logger.info( `[MigrationService] Successfully marked migration as applied: ${migration.name}`, @@ -196,53 +207,4 @@ export async function runMigrations( } } -/** - * 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( - migration: Migration, - sqlExec: (sql: string, params?: unknown[]) => Promise, - sqlQuery: (sql: string, params?: unknown[]) => Promise, -): Promise { - // 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); - } -} diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index c1dca7f2..3c9f4dcf 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -242,17 +242,50 @@ export class CapacitorPlatformService implements PlatformService { throw new Error("Database not initialized"); } - const sqlExec: (sql: string) => Promise = - this.db.execute.bind(this.db); - const sqlQuery: (sql: string) => Promise = - this.db.query.bind(this.db); - const extractMigrationNames: (result: DBSQLiteValues) => Set = ( - result, - ) => { - const names = - result.values?.map((row: { name: string }) => row.name) || []; + const sqlExec = async (sql: string, params?: unknown[]): Promise => { + console.log(`🔧 [CapacitorMigration] Executing SQL:`, sql); + console.log(`📋 [CapacitorMigration] With params:`, params); + + if (params && params.length > 0) { + // Use run method for parameterized queries + const result = await this.db!.run(sql, params); + console.log(`✅ [CapacitorMigration] Run result:`, result); + return result; + } else { + // Use execute method for non-parameterized queries + const result = await this.db!.execute(sql); + console.log(`✅ [CapacitorMigration] Execute result:`, result); + return result; + } + }; + + const sqlQuery = async (sql: string, params?: unknown[]): Promise => { + console.log(`🔍 [CapacitorMigration] Querying SQL:`, sql); + console.log(`📋 [CapacitorMigration] With params:`, params); + + const result = await this.db!.query(sql, params); + console.log(`📊 [CapacitorMigration] Query result:`, result); + return result; + }; + + const extractMigrationNames = (result: DBSQLiteValues): Set => { + console.log(`🔍 [CapacitorMigration] Extracting migration names from:`, result); + + // Handle the Capacitor SQLite result format + const names = result.values?.map((row: any) => { + // The row could be an object with 'name' property or an array where name is first element + if (typeof row === 'object' && row.name !== undefined) { + return row.name; + } else if (Array.isArray(row) && row.length > 0) { + return row[0]; + } + return null; + }).filter(name => name !== null) || []; + + console.log(`📋 [CapacitorMigration] Extracted names:`, names); return new Set(names); }; + await runMigrations(sqlExec, sqlQuery, extractMigrationNames); }