refactor(platforms/capacitor): improve database initialization robustness
Refactored _initialize() method to eliminate brittle error message parsing, add transactional migration safety, and improve observability. Key improvements: - Replaced error message parsing with deterministic capability checks (_ensureConnection helper) - Wrapped migrations in BEGIN IMMEDIATE transaction with rollback - Added platform-aware PRAGMA application (WAL on native, not web) - Added structured phase timing logs (connect, open, pragmas, migrate, verify) - Enhanced error cleanup with best-effort connection closure - Updated integrity verification to throw on critical failures - Extracted helper methods (_asMessage, _ensureConnection, _applyPragmas) - Added configuration properties (dbVersion, encryption) to replace literals Removed single-flight logic from _initialize() (handled by initializeDatabase()). All connection state handling now uses isConnection() checks instead of string matching, making initialization deterministic across JS/native boundaries.
This commit is contained in:
@@ -50,6 +50,8 @@ export class CapacitorPlatformService
|
|||||||
private sqlite: SQLiteConnection;
|
private sqlite: SQLiteConnection;
|
||||||
private db: SQLiteDBConnection | null = null;
|
private db: SQLiteDBConnection | null = null;
|
||||||
private dbName = "timesafari.sqlite";
|
private dbName = "timesafari.sqlite";
|
||||||
|
private dbVersion: number = 1;
|
||||||
|
private encryption: string = "no-encryption";
|
||||||
private initialized = false;
|
private initialized = false;
|
||||||
private initializationPromise: Promise<void> | null = null;
|
private initializationPromise: Promise<void> | null = null;
|
||||||
private operationQueue: Array<QueuedOperation> = [];
|
private operationQueue: Array<QueuedOperation> = [];
|
||||||
@@ -85,120 +87,396 @@ export class CapacitorPlatformService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _initialize(): Promise<void> {
|
/**
|
||||||
if (this.initialized) {
|
* Extract error message from unknown error type.
|
||||||
|
*
|
||||||
|
* Provides consistent error text extraction for logging and
|
||||||
|
* error handling across different error formats.
|
||||||
|
*
|
||||||
|
* @param e - Unknown error object
|
||||||
|
* @returns String representation of error message
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private _asMessage(e: unknown): string {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
return e.message;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof e === "object" &&
|
||||||
|
e !== null &&
|
||||||
|
("errorMessage" in e || "message" in e)
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
(e as { errorMessage?: string; message?: string }).errorMessage ||
|
||||||
|
(e as { errorMessage?: string; message?: string }).message ||
|
||||||
|
String(e)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return String(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure database connection exists, handling JS/native map desyncs.
|
||||||
|
*
|
||||||
|
* Uses capability checks instead of error message parsing to
|
||||||
|
* deterministically handle connection state across native/JS
|
||||||
|
* boundaries.
|
||||||
|
*
|
||||||
|
* @returns Promise resolving when connection is established
|
||||||
|
* @throws Error if connection cannot be established
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async _ensureConnection(): Promise<void> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Check if connection exists in JavaScript Map
|
||||||
|
const isConnResult = await this.sqlite.isConnection(this.dbName, false);
|
||||||
|
|
||||||
|
if (isConnResult.result) {
|
||||||
|
// Connection exists in Map, retrieve it
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Connection exists in Map, retrieving (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
this.db = await this.sqlite.retrieveConnection(this.dbName, false);
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Successfully retrieved connection (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Connection doesn't exist in Map, try to create
|
||||||
try {
|
try {
|
||||||
// Try to create/Open database connection
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Creating new connection (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
this.db = await this.sqlite.createConnection(
|
||||||
|
this.dbName,
|
||||||
|
false,
|
||||||
|
this.encryption,
|
||||||
|
this.dbVersion,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Successfully created connection (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
} catch (createError: unknown) {
|
||||||
|
// Connection might exist natively but not in JS Map
|
||||||
|
// Best-effort cleanup then retry once
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Create failed, attempting cleanup (${Date.now() - startTime}ms):`,
|
||||||
|
this._asMessage(createError),
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
this.db = await this.sqlite.createConnection(
|
await this.sqlite.closeConnection(this.dbName, false);
|
||||||
this.dbName,
|
} catch (closeError) {
|
||||||
false,
|
// Ignore close errors - connection might not be properly tracked
|
||||||
"no-encryption",
|
logger.debug(
|
||||||
1,
|
`[CapacitorPlatformService] Cleanup close error (may be expected):`,
|
||||||
false,
|
this._asMessage(closeError),
|
||||||
);
|
);
|
||||||
} catch (createError: unknown) {
|
|
||||||
// If connection already exists, try to retrieve it or handle gracefully
|
|
||||||
const errorMessage =
|
|
||||||
createError instanceof Error
|
|
||||||
? createError.message
|
|
||||||
: String(createError);
|
|
||||||
const errorObj =
|
|
||||||
typeof createError === "object" && createError !== null
|
|
||||||
? (createError as { errorMessage?: string; message?: string })
|
|
||||||
: {};
|
|
||||||
|
|
||||||
const fullErrorMessage =
|
|
||||||
errorObj.errorMessage || errorObj.message || errorMessage;
|
|
||||||
|
|
||||||
if (fullErrorMessage.includes("already exists")) {
|
|
||||||
logger.debug(
|
|
||||||
"[CapacitorPlatformService] Connection already exists on native side, attempting to retrieve",
|
|
||||||
);
|
|
||||||
// Check if connection exists in JavaScript Map
|
|
||||||
const isConnResult = await this.sqlite.isConnection(
|
|
||||||
this.dbName,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
if (isConnResult.result) {
|
|
||||||
// Connection exists in Map, retrieve it
|
|
||||||
this.db = await this.sqlite.retrieveConnection(this.dbName, false);
|
|
||||||
logger.debug(
|
|
||||||
"[CapacitorPlatformService] Successfully retrieved existing connection from Map",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Connection exists on native side but not in JavaScript Map
|
|
||||||
// This can happen when the app is restarted but native connections persist
|
|
||||||
// Try to close the native connection first, then create a new one
|
|
||||||
logger.debug(
|
|
||||||
"[CapacitorPlatformService] Connection exists natively but not in Map, closing and recreating",
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
await this.sqlite.closeConnection(this.dbName, false);
|
|
||||||
} catch (closeError) {
|
|
||||||
// Ignore close errors - connection might not be properly tracked
|
|
||||||
logger.debug(
|
|
||||||
"[CapacitorPlatformService] Error closing connection (may be expected):",
|
|
||||||
closeError,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Now try to create the connection again
|
|
||||||
this.db = await this.sqlite.createConnection(
|
|
||||||
this.dbName,
|
|
||||||
false,
|
|
||||||
"no-encryption",
|
|
||||||
1,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
logger.debug(
|
|
||||||
"[CapacitorPlatformService] Successfully created connection after cleanup",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Re-throw if it's a different error
|
|
||||||
throw createError;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the connection if it's not already open
|
// Retry create once
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Retrying connection creation (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
this.db = await this.sqlite.createConnection(
|
||||||
|
this.dbName,
|
||||||
|
false,
|
||||||
|
this.encryption,
|
||||||
|
this.dbVersion,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Successfully created connection after retry (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply SQLite PRAGMAs with platform-aware configuration.
|
||||||
|
*
|
||||||
|
* Sets foreign keys, journal mode (WAL on native), synchronous
|
||||||
|
* mode, and busy timeout. Handles platform differences gracefully.
|
||||||
|
*
|
||||||
|
* @param db - Database connection (non-null)
|
||||||
|
* @returns Promise resolving when PRAGMAs are applied
|
||||||
|
* @throws Error if PRAGMA application fails
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async _applyPragmas(db: SQLiteDBConnection): Promise<string> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const platform = Capacitor.getPlatform();
|
||||||
|
let journalMode = "unknown";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Always enable foreign keys
|
||||||
|
await db.execute("PRAGMA foreign_keys = ON;");
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Applied foreign_keys PRAGMA (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set busy timeout before any operations
|
||||||
|
await db.execute("PRAGMA busy_timeout = 5000;");
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Applied busy_timeout PRAGMA (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Prefer WAL on native platforms
|
||||||
|
if (platform !== "web") {
|
||||||
|
try {
|
||||||
|
await db.execute("PRAGMA journal_mode = WAL;");
|
||||||
|
// Verify actual journal mode
|
||||||
|
const modeResult = await db.query("PRAGMA journal_mode;");
|
||||||
|
if (
|
||||||
|
modeResult.values &&
|
||||||
|
modeResult.values.length > 0 &&
|
||||||
|
typeof modeResult.values[0] === "object" &&
|
||||||
|
modeResult.values[0] !== null
|
||||||
|
) {
|
||||||
|
const row = modeResult.values[0] as Record<string, unknown>;
|
||||||
|
journalMode =
|
||||||
|
(row.journal_mode as string) ||
|
||||||
|
(Array.isArray(modeResult.values[0])
|
||||||
|
? (modeResult.values[0][0] as string)
|
||||||
|
: "unknown");
|
||||||
|
}
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Applied WAL journal_mode: ${journalMode} (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db.execute("PRAGMA synchronous = NORMAL;");
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Applied synchronous PRAGMA (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
} catch (walError) {
|
||||||
|
logger.warn(
|
||||||
|
`[CapacitorPlatformService] WAL mode failed, continuing with default:`,
|
||||||
|
this._asMessage(walError),
|
||||||
|
);
|
||||||
|
// Verify actual mode even if WAL failed
|
||||||
|
try {
|
||||||
|
const modeResult = await db.query("PRAGMA journal_mode;");
|
||||||
|
if (
|
||||||
|
modeResult.values &&
|
||||||
|
modeResult.values.length > 0 &&
|
||||||
|
typeof modeResult.values[0] === "object" &&
|
||||||
|
modeResult.values[0] !== null
|
||||||
|
) {
|
||||||
|
const row = modeResult.values[0] as Record<string, unknown>;
|
||||||
|
journalMode =
|
||||||
|
(row.journal_mode as string) ||
|
||||||
|
(Array.isArray(modeResult.values[0])
|
||||||
|
? (modeResult.values[0][0] as string)
|
||||||
|
: "unknown");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
journalMode = "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Skipping WAL on web platform (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
journalMode = "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] PRAGMAs applied successfully, journal_mode: ${journalMode} (${Date.now() - startTime}ms)`,
|
||||||
|
);
|
||||||
|
return journalMode;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`[CapacitorPlatformService] Failed to apply PRAGMAs (${Date.now() - startTime}ms):`,
|
||||||
|
this._asMessage(error),
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize database connection, apply PRAGMAs, run migrations,
|
||||||
|
* and verify integrity.
|
||||||
|
*
|
||||||
|
* This method orchestrates the complete database initialization
|
||||||
|
* process with proper error handling, transaction management, and
|
||||||
|
* observability. It relies on `initializeDatabase()` for
|
||||||
|
* single-flight concurrency control.
|
||||||
|
*
|
||||||
|
* ## Initialization Phases:
|
||||||
|
*
|
||||||
|
* 1. **Connection**: Establish connection using capability checks
|
||||||
|
* 2. **Open**: Open connection with tolerant "already open" handling
|
||||||
|
* 3. **PRAGMAs**: Apply platform-aware SQLite configuration
|
||||||
|
* 4. **Migrations**: Execute migrations within transaction
|
||||||
|
* 5. **Verification**: Run integrity checks post-migration
|
||||||
|
* 6. **Queue Start**: Begin processing queued operations
|
||||||
|
*
|
||||||
|
* ## Error Handling:
|
||||||
|
*
|
||||||
|
* - All phases are timed and logged
|
||||||
|
* - Failures trigger cleanup (close connection, reset state)
|
||||||
|
* - Errors include original cause for upstream logging
|
||||||
|
* - Single-flight promise is reset only on failure
|
||||||
|
*
|
||||||
|
* ## Transaction Management:
|
||||||
|
*
|
||||||
|
* Migrations run within `BEGIN IMMEDIATE` transaction with
|
||||||
|
* automatic rollback on failure. This ensures database consistency
|
||||||
|
* even if migrations fail mid-way.
|
||||||
|
*
|
||||||
|
* @throws Error if initialization fails at any phase
|
||||||
|
* @private Internal method called only from initializeDatabase()
|
||||||
|
*/
|
||||||
|
private async _initialize(): Promise<void> {
|
||||||
|
const initStartTime = Date.now();
|
||||||
|
const phaseTimings: Record<string, number> = {};
|
||||||
|
let connectionEstablished = false;
|
||||||
|
let dbOpened = false;
|
||||||
|
let journalMode = "unknown";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Phase 1: Connection establishment
|
||||||
|
const connectStart = Date.now();
|
||||||
|
await this._ensureConnection();
|
||||||
|
phaseTimings.connect = Date.now() - connectStart;
|
||||||
|
connectionEstablished = true;
|
||||||
|
|
||||||
|
if (!this.db) {
|
||||||
|
throw new Error("Connection established but db is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Narrow type for subsequent operations
|
||||||
|
const db = this.db;
|
||||||
|
|
||||||
|
// Phase 2: Open connection
|
||||||
|
const openStart = Date.now();
|
||||||
try {
|
try {
|
||||||
await this.db.open();
|
await db.open();
|
||||||
|
dbOpened = true;
|
||||||
} catch (openError: unknown) {
|
} catch (openError: unknown) {
|
||||||
const openErrorMessage =
|
const openMsg = this._asMessage(openError);
|
||||||
openError instanceof Error ? openError.message : String(openError);
|
// Swallow only "already open" errors
|
||||||
// If already open, that's fine - continue
|
if (!/already open/i.test(openMsg)) {
|
||||||
if (!openErrorMessage.includes("already open")) {
|
|
||||||
throw openError;
|
throw openError;
|
||||||
}
|
}
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"[CapacitorPlatformService] Database connection already open",
|
`[CapacitorPlatformService] Database already open (${Date.now() - openStart}ms)`,
|
||||||
);
|
);
|
||||||
|
dbOpened = true;
|
||||||
|
}
|
||||||
|
phaseTimings.open = Date.now() - openStart;
|
||||||
|
|
||||||
|
// Phase 3: Apply PRAGMAs
|
||||||
|
const pragmasStart = Date.now();
|
||||||
|
journalMode = await this._applyPragmas(db);
|
||||||
|
phaseTimings.pragmas = Date.now() - pragmasStart;
|
||||||
|
|
||||||
|
// Phase 4: Run migrations in transaction
|
||||||
|
const migrateStart = Date.now();
|
||||||
|
try {
|
||||||
|
await db.execute("BEGIN IMMEDIATE;");
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Migration transaction started (${Date.now() - migrateStart}ms)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.runCapacitorMigrations();
|
||||||
|
|
||||||
|
await db.execute("COMMIT;");
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Migration transaction committed (${Date.now() - migrateStart}ms)`,
|
||||||
|
);
|
||||||
|
} catch (migrateError) {
|
||||||
|
// Rollback on any migration error
|
||||||
|
try {
|
||||||
|
await db.execute("ROLLBACK;");
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Migration transaction rolled back (${Date.now() - migrateStart}ms)`,
|
||||||
|
);
|
||||||
|
} catch (rollbackError) {
|
||||||
|
logger.error(
|
||||||
|
`[CapacitorPlatformService] Rollback failed:`,
|
||||||
|
this._asMessage(rollbackError),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw migrateError;
|
||||||
|
}
|
||||||
|
phaseTimings.migrate = Date.now() - migrateStart;
|
||||||
|
|
||||||
|
// Phase 5: Post-migration integrity verification
|
||||||
|
const verifyStart = Date.now();
|
||||||
|
await this.verifyDatabaseIntegrity();
|
||||||
|
phaseTimings.verify = Date.now() - verifyStart;
|
||||||
|
|
||||||
|
// Mark as initialized
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
// Phase 6: Start queue processor (only once)
|
||||||
|
if (!this.isProcessingQueue) {
|
||||||
|
this.processQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set journal mode to WAL for better performance
|
// Log success with all timing and config info
|
||||||
// await this.db.execute("PRAGMA journal_mode=WAL;");
|
const totalTime = Date.now() - initStartTime;
|
||||||
|
const platform = Capacitor.getPlatform();
|
||||||
// Run migrations
|
|
||||||
await this.runCapacitorMigrations();
|
|
||||||
|
|
||||||
this.initialized = true;
|
|
||||||
logger.log(
|
logger.log(
|
||||||
"[CapacitorPlatformService] SQLite database initialized successfully",
|
`[CapacitorPlatformService] Database initialized successfully ` +
|
||||||
|
`(total: ${totalTime}ms, connect: ${phaseTimings.connect}ms, ` +
|
||||||
|
`open: ${phaseTimings.open}ms, pragmas: ${phaseTimings.pragmas}ms, ` +
|
||||||
|
`migrate: ${phaseTimings.migrate}ms, verify: ${phaseTimings.verify}ms) ` +
|
||||||
|
`[dbName: ${this.dbName}, dbVersion: ${this.dbVersion}, ` +
|
||||||
|
`encryption: ${this.encryption}, platform: ${platform}, ` +
|
||||||
|
`journal_mode: ${journalMode}]`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
const phase =
|
||||||
|
Object.keys(phaseTimings).length === 0
|
||||||
|
? "connection"
|
||||||
|
: Object.keys(phaseTimings).pop() || "unknown";
|
||||||
|
const totalTime = Date.now() - initStartTime;
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
`[CapacitorPlatformService] Initialization failed at phase '${phase}' ` +
|
||||||
|
`(total: ${totalTime}ms):`,
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start processing the queue after initialization
|
// Cleanup: best-effort close and reset state
|
||||||
this.processQueue();
|
if (connectionEstablished && this.db) {
|
||||||
} catch (error) {
|
try {
|
||||||
logger.error(
|
if (dbOpened) {
|
||||||
"[CapacitorPlatformService] Error initializing SQLite database:",
|
await this.db.close();
|
||||||
error,
|
}
|
||||||
);
|
} catch (closeError) {
|
||||||
throw new Error(
|
logger.debug(
|
||||||
"[CapacitorPlatformService] Failed to initialize database",
|
`[CapacitorPlatformService] Cleanup close error:`,
|
||||||
|
this._asMessage(closeError),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.sqlite.closeConnection(this.dbName, false);
|
||||||
|
} catch (closeConnError) {
|
||||||
|
logger.debug(
|
||||||
|
`[CapacitorPlatformService] Cleanup closeConnection error:`,
|
||||||
|
this._asMessage(closeConnError),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.db = null;
|
||||||
|
this.initialized = false;
|
||||||
|
|
||||||
|
// Re-throw with cause for upstream logging
|
||||||
|
const initError = new Error(
|
||||||
|
`[CapacitorPlatformService] Failed to initialize database at phase '${phase}'`,
|
||||||
);
|
);
|
||||||
|
if (error instanceof Error) {
|
||||||
|
// Use standard Error properties for compatibility
|
||||||
|
initError.message = `${initError.message}: ${error.message}`;
|
||||||
|
initError.stack = error.stack;
|
||||||
|
}
|
||||||
|
throw initError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -673,22 +951,13 @@ export class CapacitorPlatformService
|
|||||||
return new Set(names);
|
return new Set(names);
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
// Execute the migration process
|
||||||
// Execute the migration process
|
// Note: Integrity verification is handled by _initialize() after transaction commit
|
||||||
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
|
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
|
||||||
|
|
||||||
// After migrations, run integrity check to verify database state
|
|
||||||
await this.verifyDatabaseIntegrity();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`❌ [CapacitorMigration] Migration failed:`, error);
|
|
||||||
// Still try to verify what we have for debugging purposes
|
|
||||||
await this.verifyDatabaseIntegrity();
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify database integrity and migration status
|
* Verify database integrity and migration status.
|
||||||
*
|
*
|
||||||
* This method performs comprehensive validation of the database structure
|
* This method performs comprehensive validation of the database structure
|
||||||
* and migration state. It's designed to help identify issues with the
|
* and migration state. It's designed to help identify issues with the
|
||||||
@@ -719,9 +988,9 @@ export class CapacitorPlatformService
|
|||||||
*
|
*
|
||||||
* ## Error Handling:
|
* ## Error Handling:
|
||||||
*
|
*
|
||||||
* This method is designed to never throw errors - it captures and logs
|
* This method throws errors for critical failures (missing core tables)
|
||||||
* all validation issues for debugging purposes. This ensures that even
|
* to ensure initialization fails when database integrity is compromised.
|
||||||
* if integrity checks fail, they don't prevent the application from starting.
|
* Non-critical validation issues are logged but do not cause failures.
|
||||||
*
|
*
|
||||||
* ## Logging Output:
|
* ## Logging Output:
|
||||||
*
|
*
|
||||||
@@ -731,6 +1000,7 @@ export class CapacitorPlatformService
|
|||||||
* - `📊` for data summaries
|
* - `📊` for data summaries
|
||||||
* - `🔍` for investigation steps
|
* - `🔍` for investigation steps
|
||||||
*
|
*
|
||||||
|
* @throws Error if critical integrity checks fail (e.g., missing core tables)
|
||||||
* @private Internal method called after migrations
|
* @private Internal method called after migrations
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -741,12 +1011,15 @@ export class CapacitorPlatformService
|
|||||||
*/
|
*/
|
||||||
private async verifyDatabaseIntegrity(): Promise<void> {
|
private async verifyDatabaseIntegrity(): Promise<void> {
|
||||||
if (!this.db) {
|
if (!this.db) {
|
||||||
logger.error(`❌ [DB-Integrity] Database not initialized`);
|
const error = new Error("[DB-Integrity] Database not initialized");
|
||||||
return;
|
logger.error(`❌ ${error.message}`);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`🔍 [DB-Integrity] Starting database integrity check...`);
|
logger.debug(`🔍 [DB-Integrity] Starting database integrity check...`);
|
||||||
|
|
||||||
|
const missingTables: string[] = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 1: Check migrations table and applied migrations
|
// Step 1: Check migrations table and applied migrations
|
||||||
const migrationsResult = await this.db.query(
|
const migrationsResult = await this.db.query(
|
||||||
@@ -763,7 +1036,6 @@ export class CapacitorPlatformService
|
|||||||
"logs",
|
"logs",
|
||||||
"temp",
|
"temp",
|
||||||
];
|
];
|
||||||
const existingTables: string[] = [];
|
|
||||||
|
|
||||||
for (const tableName of coreTableNames) {
|
for (const tableName of coreTableNames) {
|
||||||
try {
|
try {
|
||||||
@@ -771,58 +1043,66 @@ export class CapacitorPlatformService
|
|||||||
`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,
|
`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`,
|
||||||
);
|
);
|
||||||
if (tableCheck.values && tableCheck.values.length > 0) {
|
if (tableCheck.values && tableCheck.values.length > 0) {
|
||||||
existingTables.push(tableName);
|
|
||||||
logger.debug(`✅ [DB-Integrity] Table ${tableName} exists`);
|
logger.debug(`✅ [DB-Integrity] Table ${tableName} exists`);
|
||||||
} else {
|
} else {
|
||||||
logger.error(`❌ [DB-Integrity] Table ${tableName} missing`);
|
logger.error(`❌ [DB-Integrity] Table ${tableName} missing`);
|
||||||
|
missingTables.push(tableName);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`❌ [DB-Integrity] Error checking table ${tableName}:`,
|
`❌ [DB-Integrity] Error checking table ${tableName}:`,
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
|
missingTables.push(tableName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fail init if critical tables are missing
|
||||||
|
if (missingTables.length > 0) {
|
||||||
|
const error = new Error(
|
||||||
|
`[DB-Integrity] Critical tables missing: ${missingTables.join(", ")}`,
|
||||||
|
);
|
||||||
|
logger.error(`❌ ${error.message}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
// Step 3: Check contacts table schema (including iViewContent column)
|
// Step 3: Check contacts table schema (including iViewContent column)
|
||||||
if (existingTables.includes("contacts")) {
|
try {
|
||||||
try {
|
const contactsSchema = await this.db.query(
|
||||||
const contactsSchema = await this.db.query(
|
"PRAGMA table_info(contacts)",
|
||||||
"PRAGMA table_info(contacts)",
|
);
|
||||||
);
|
logger.debug(
|
||||||
|
`📊 [DB-Integrity] Contacts table schema:`,
|
||||||
|
contactsSchema,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for iViewContent column specifically
|
||||||
|
const hasIViewContent = contactsSchema.values?.some(
|
||||||
|
(col: unknown) =>
|
||||||
|
(typeof col === "object" &&
|
||||||
|
col !== null &&
|
||||||
|
"name" in col &&
|
||||||
|
(col as { name: string }).name === "iViewContent") ||
|
||||||
|
(Array.isArray(col) && col[1] === "iViewContent"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasIViewContent) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`📊 [DB-Integrity] Contacts table schema:`,
|
`✅ [DB-Integrity] iViewContent column exists in contacts table`,
|
||||||
contactsSchema,
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
// Check for iViewContent column specifically
|
logger.warn(
|
||||||
const hasIViewContent = contactsSchema.values?.some(
|
`⚠️ [DB-Integrity] iViewContent column missing from contacts table (non-critical)`,
|
||||||
(col: unknown) =>
|
|
||||||
(typeof col === "object" &&
|
|
||||||
col !== null &&
|
|
||||||
"name" in col &&
|
|
||||||
(col as { name: string }).name === "iViewContent") ||
|
|
||||||
(Array.isArray(col) && col[1] === "iViewContent"),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasIViewContent) {
|
|
||||||
logger.debug(
|
|
||||||
`✅ [DB-Integrity] iViewContent column exists in contacts table`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.error(
|
|
||||||
`❌ [DB-Integrity] iViewContent column missing from contacts table`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`❌ [DB-Integrity] Error checking contacts schema:`,
|
|
||||||
error,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`⚠️ [DB-Integrity] Error checking contacts schema (non-critical):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Check for basic data integrity
|
// Step 4: Check for basic data integrity (best-effort, non-critical)
|
||||||
try {
|
try {
|
||||||
const accountCount = await this.db.query(
|
const accountCount = await this.db.query(
|
||||||
"SELECT COUNT(*) as count FROM accounts",
|
"SELECT COUNT(*) as count FROM accounts",
|
||||||
@@ -838,11 +1118,19 @@ export class CapacitorPlatformService
|
|||||||
`📊 [DB-Integrity] Data counts - Accounts: ${JSON.stringify(accountCount)}, Settings: ${JSON.stringify(settingsCount)}, Contacts: ${JSON.stringify(contactsCount)}`,
|
`📊 [DB-Integrity] Data counts - Accounts: ${JSON.stringify(accountCount)}, Settings: ${JSON.stringify(settingsCount)}, Contacts: ${JSON.stringify(contactsCount)}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ [DB-Integrity] Error checking data counts:`, error);
|
logger.warn(
|
||||||
|
`⚠️ [DB-Integrity] Error checking data counts (non-critical):`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(`✅ [DB-Integrity] Database integrity check completed`);
|
logger.log(`✅ [DB-Integrity] Database integrity check completed`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Re-throw if it's our own error (missing tables)
|
||||||
|
if (error instanceof Error && error.message.includes("[DB-Integrity]")) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
// Log other errors but don't fail init
|
||||||
logger.error(`❌ [DB-Integrity] Database integrity check failed:`, error);
|
logger.error(`❌ [DB-Integrity] Database integrity check failed:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user