feat: Complete Migration 004 Complexity Resolution (Phases 1-4)

- Phase 1: Simplify Migration Definition 
  * Remove duplicate SQL definitions from migration 004
  * Eliminate recovery logic that could cause duplicate execution
  * Establish single source of truth for migration SQL

- Phase 2: Fix Database Result Handling 
  * Remove DatabaseResult type assumptions from migration code
  * Implement database-agnostic result extraction with extractSingleValue()
  * Normalize results from AbsurdSqlDatabaseService and CapacitorPlatformService

- Phase 3: Ensure Atomic Execution 
  * Remove individual statement execution logic
  * Execute migrations as single atomic SQL blocks only
  * Add explicit rollback instructions and failure cause logging
  * Ensure migration tracking is accurate

- Phase 4: Remove Excessive Debugging 
  * Move detailed logging to development-only mode
  * Preserve essential error logging for production
  * Optimize startup performance by reducing logging overhead
  * Maintain full debugging capability in development

Migration system now follows single-source, atomic execution principle
with improved performance and comprehensive error handling.

Timestamp: 2025-09-17 05:08:05 UTC
This commit is contained in:
Matthew Raymer
2025-09-17 05:08:26 +00:00
parent 297fe3cec6
commit 0fae8bbda6
5 changed files with 324 additions and 164 deletions

View File

@@ -6,12 +6,6 @@ import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { arrayBufferToBase64 } from "@/libs/crypto";
import { logger } from "@/utils/logger";
// Database result interface for SQLite queries
interface DatabaseResult {
values?: unknown[][];
[key: string]: unknown;
}
// Generate a random secret for the secret table
// It's not really secure to maintain the secret next to the user's data.
@@ -183,30 +177,33 @@ const MIGRATIONS = [
DELETE FROM settings WHERE accountDid IS NULL;
UPDATE settings SET activeDid = NULL;
`,
// Split into individual statements for better error handling
statements: [
"PRAGMA foreign_keys = ON",
"CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did)",
`CREATE TABLE IF NOT EXISTS active_identity (
id INTEGER PRIMARY KEY CHECK (id = 1),
activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT,
lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
)`,
"CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id)",
`INSERT INTO active_identity (id, activeDid, lastUpdated)
SELECT 1, NULL, datetime('now')
WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1)`,
`UPDATE active_identity
SET activeDid = (SELECT activeDid FROM settings WHERE id = 1),
lastUpdated = datetime('now')
WHERE id = 1
AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '')`,
"DELETE FROM settings WHERE accountDid IS NULL",
"UPDATE settings SET activeDid = NULL",
],
},
];
/**
* Extract single value from database query result
* Works with different database service result formats
*/
function extractSingleValue<T>(result: T): string | number | null {
if (!result) return null;
// Handle AbsurdSQL format: QueryExecResult[]
if (Array.isArray(result) && result.length > 0 && result[0]?.values) {
const values = result[0].values;
return values.length > 0 ? values[0][0] : null;
}
// Handle Capacitor SQLite format: { values: unknown[][] }
if (typeof result === "object" && result !== null && "values" in result) {
const values = (result as { values: unknown[][] }).values;
return values && values.length > 0
? (values[0][0] as string | number)
: null;
}
return null;
}
/**
* @param sqlExec - A function that executes a SQL statement and returns the result
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
@@ -216,26 +213,36 @@ export async function runMigrations<T>(
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
logger.debug("[Migration] Starting database migrations");
// Only log migration start in development
const isDevelopment = process.env.VITE_PLATFORM === "development";
if (isDevelopment) {
logger.debug("[Migration] Starting database migrations");
}
for (const migration of MIGRATIONS) {
logger.debug("[Migration] Registering migration:", migration.name);
if (isDevelopment) {
logger.debug("[Migration] Registering migration:", migration.name);
}
registerMigration(migration);
}
logger.debug("[Migration] Running migration service");
if (isDevelopment) {
logger.debug("[Migration] Running migration service");
}
await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
logger.debug("[Migration] Database migrations completed");
if (isDevelopment) {
logger.debug("[Migration] Database migrations completed");
}
// Bootstrapping: Ensure active account is selected after migrations
logger.debug("[Migration] Running bootstrapping hooks");
if (isDevelopment) {
logger.debug("[Migration] Running bootstrapping hooks");
}
try {
// Check if we have accounts but no active selection
const accountsResult = await sqlQuery("SELECT COUNT(*) FROM accounts");
const accountsCount =
accountsResult && (accountsResult as DatabaseResult).values
? ((accountsResult as DatabaseResult).values?.[0]?.[0] as number)
: 0;
const accountsCount = (extractSingleValue(accountsResult) as number) || 0;
// Check if active_identity table exists, and if not, try to recover
let activeDid: string | null = null;
@@ -243,90 +250,26 @@ export async function runMigrations<T>(
const activeResult = await sqlQuery(
"SELECT activeDid FROM active_identity WHERE id = 1",
);
activeDid =
activeResult && (activeResult as DatabaseResult).values
? ((activeResult as DatabaseResult).values?.[0]?.[0] as string)
: null;
activeDid = (extractSingleValue(activeResult) as string) || null;
} catch (error) {
// Table doesn't exist - this means migration 004 failed but was marked as applied
logger.warn(
"[Migration] active_identity table missing, attempting recovery",
);
// Check if migration 004 is marked as applied
const migrationResult = await sqlQuery(
"SELECT name FROM migrations WHERE name = '004_active_identity_management'",
);
const isMigrationMarked =
migrationResult && (migrationResult as DatabaseResult).values
? ((migrationResult as DatabaseResult).values?.length ?? 0) > 0
: false;
if (isMigrationMarked) {
logger.warn(
"[Migration] Migration 004 marked as applied but table missing - recreating table",
// Table doesn't exist - migration 004 may not have run yet
if (isDevelopment) {
logger.debug(
"[Migration] active_identity table not found - migration may not have run",
);
// Recreate the active_identity table using the individual statements
const statements = [
"PRAGMA foreign_keys = ON",
"CREATE UNIQUE INDEX IF NOT EXISTS idx_accounts_did_unique ON accounts(did)",
`CREATE TABLE IF NOT EXISTS active_identity (
id INTEGER PRIMARY KEY CHECK (id = 1),
activeDid TEXT REFERENCES accounts(did) ON DELETE RESTRICT,
lastUpdated TEXT NOT NULL DEFAULT (datetime('now'))
)`,
"CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id)",
`INSERT INTO active_identity (id, activeDid, lastUpdated)
SELECT 1, NULL, datetime('now')
WHERE NOT EXISTS (SELECT 1 FROM active_identity WHERE id = 1)`,
`UPDATE active_identity
SET activeDid = (SELECT activeDid FROM settings WHERE id = 1),
lastUpdated = datetime('now')
WHERE id = 1
AND EXISTS (SELECT 1 FROM settings WHERE id = 1 AND activeDid IS NOT NULL AND activeDid != '')`,
"DELETE FROM settings WHERE accountDid IS NULL",
"UPDATE settings SET activeDid = NULL",
];
for (const statement of statements) {
try {
await sqlExec(statement);
} catch (stmtError) {
logger.warn(
`[Migration] Recovery statement failed: ${statement}`,
stmtError,
);
}
}
// Try to get activeDid again after recovery
try {
const activeResult = await sqlQuery(
"SELECT activeDid FROM active_identity WHERE id = 1",
);
activeDid =
activeResult && (activeResult as DatabaseResult).values
? ((activeResult as DatabaseResult).values?.[0]?.[0] as string)
: null;
} catch (recoveryError) {
logger.error(
"[Migration] Recovery failed - active_identity table still not accessible",
recoveryError,
);
}
}
activeDid = null;
}
if (accountsCount > 0 && (!activeDid || activeDid === "")) {
logger.debug("[Migration] Auto-selecting first account as active");
if (isDevelopment) {
logger.debug("[Migration] Auto-selecting first account as active");
}
const firstAccountResult = await sqlQuery(
"SELECT did FROM accounts ORDER BY dateCreated, did LIMIT 1",
);
const firstAccountDid =
firstAccountResult && (firstAccountResult as DatabaseResult).values
? ((firstAccountResult as DatabaseResult).values?.[0]?.[0] as string)
: null;
(extractSingleValue(firstAccountResult) as string) || null;
if (firstAccountDid) {
await sqlExec(