refactor-initialize #221

Open
anomalist wants to merge 13 commits from refactor-initialize into master
Showing only changes of commit 471bdd6b92 - Show all commits

View File

@@ -146,10 +146,15 @@ export class CapacitorPlatformService
}
/**
* Internal initialization method (v3.3)
* Internal initialization method (v3.3.3)
*
* Order: PRAGMAs (temp connection, no transactions) → connect → open → migrations (single txn) → verify
*
* ORDERING GUARD (v3.3.3+):
* - PRAGMAs MUST run before any execute/executeSet operations
* - This ensures database-level settings are applied before migrations
* - If ordering changes, add unit test that fails on execute before PRAGMAs
*
* @private Internal method - use initializeDatabase() instead
*/
private async _initialize(): Promise<void> {
@@ -163,6 +168,7 @@ export class CapacitorPlatformService
try {
// Phase 1: Apply PRAGMAs on temporary connection (MUST be before main connection)
// This ensures PRAGMAs run outside any transaction context
// ORDERING GUARD: No execute/executeSet operations should occur before this phase
logger.debug(
"[CapacitorPlatformService] Starting PRAGMA phase (before main connection)",
);
@@ -281,12 +287,17 @@ export class CapacitorPlatformService
*
* Implements retrieve-first, create-second, close+create-last pattern.
* Short-circuits if fallback PRAGMA method already set this.db.
*
* ORDERING GUARD (v3.3.3+):
* - This method MUST be called AFTER _applyPragmasOnTempConnection()
* - PRAGMAs must run before any execute/executeSet operations
* - If this.db is already set by fallback PRAGMA path, return immediately
*/
private async _ensureConnection(): Promise<void> {
// 1) If fallback already set a live handle, use it
// 1) Short-circuit: If fallback PRAGMA method already set this.db, use it (invariant #3)
if (this.db) {
logger.log(
"[CapacitorPlatformService] [DB] ensure: already have connection",
"[CapacitorPlatformService] [DB] ensure: already have connection (from PRAGMA fallback)",
);
return;
}
@@ -469,7 +480,7 @@ export class CapacitorPlatformService
// Foreign keys (safe via run)
await pragmaConn.run("PRAGMA foreign_keys = ON;", []);
// journal_mode via query (returns a row)
// journal_mode via query (returns a row) - invariant #2
let journalMode = "unknown";
if (platform !== "web") {
try {
@@ -477,16 +488,33 @@ export class CapacitorPlatformService
const row = res.values?.[0] as Record<string, unknown> | undefined;
const key = row ? Object.keys(row)[0] : undefined;
journalMode = (key ? (row![key] as string) : undefined) ?? "unknown";
} catch {
// leave as unknown
// iOS compatibility: WAL may be refused on read-only bundles
// Log but don't throw - device may not support WAL
if (journalMode !== "wal") {
logger.debug(
`[CapacitorPlatformService] WAL mode not effective (got: ${journalMode}), continuing with default`,
);
}
} catch (walError) {
// Non-fatal: WAL failure is acceptable (device may not support it)
logger.debug(
"[CapacitorPlatformService] WAL mode failed, continuing with default:",
this._errMsg(walError),
);
journalMode = "default";
}
// Only set synchronous when WAL is effective (invariant #4)
if (journalMode === "wal") {
// Only set NORMAL when WAL took effect
try {
await pragmaConn.run("PRAGMA synchronous = NORMAL;", []);
} catch {
// Non-fatal if synchronous fails
} catch (syncError) {
// Non-fatal if synchronous fails (may be in transaction)
logger.debug(
"[CapacitorPlatformService] synchronous=NORMAL failed (non-critical):",
this._errMsg(syncError),
);
}
}
} else {
@@ -549,6 +577,22 @@ export class CapacitorPlatformService
*
* v3.3.3: Fixed busy_timeout PRAGMA to use query() instead of run() to avoid
* "error code 100: another row available" on some Capacitor SQLite builds.
*
* INVARIANTS (v3.3.3+ - must not regress):
* 1. foreign_keys: Always set via run() (no return value)
* 2. journal_mode: Always set via query() (returns mode as row)
* - On iOS/web: If WAL refused, log + continue (journalMode = "unknown")
* - Never throw on WAL failure (device may not support it)
* 3. busy_timeout: Always set via query() first, fallback to run() if needed
* - Swallow benign "another row available" errors
* 4. synchronous: SKIPPED in fallback path (often fails in transaction)
* - Only set in temp path when journal_mode === 'wal'
* 5. MUST set this.db before returning (even on error, if conn exists)
*
* iOS Compatibility:
* - WAL may be refused on read-only bundles or protected file modes
* - If journal_mode !== 'wal', synchronous is not set (as documented)
* - All PRAGMA failures are non-fatal (log + continue)
*/
private async _applyPragmasOnExistingConnection(): Promise<{
journalMode: string;
@@ -628,20 +672,36 @@ export class CapacitorPlatformService
// Ignore errors
}
// journal_mode via query (safe even if txn-wrapped)
// journal_mode via query (safe even if txn-wrapped) - invariant #2
if (platform !== "web") {
try {
const res = await conn.query("PRAGMA journal_mode = WAL;");
const row = res.values?.[0] as Record<string, unknown> | undefined;
const key = row ? Object.keys(row)[0] : undefined;
journalMode = (key ? (row![key] as string) : undefined) ?? "unknown";
} catch {
// leave as unknown
// iOS compatibility: WAL may be refused on read-only bundles
// Log but don't throw - device may not support WAL
if (journalMode !== "wal") {
logger.debug(
`[CapacitorPlatformService] [PRAGMA] fallback: WAL mode not effective (got: ${journalMode}), continuing`,
);
}
} catch (walError) {
// Non-fatal: WAL failure is acceptable (device may not support it)
logger.debug(
"[CapacitorPlatformService] [PRAGMA] fallback: WAL mode failed, continuing:",
this._errMsg(walError),
);
journalMode = "default";
}
} else {
journalMode = "default";
}
// NOTE: synchronous is SKIPPED in fallback path (invariant #4)
// It often fails in transaction context, and temp path handles it when WAL is effective
// busy_timeout: use query() and ignore the row to avoid "another row available" (v3.3.3)
// v3.3.3: Fixed "error code 100: another row available" by using query() instead of run()
// Some Capacitor SQLite builds return busy_timeout as a row, which causes run() to fail
@@ -668,12 +728,12 @@ export class CapacitorPlatformService
}
}
// CRUCIAL: Set this.db before returning
// CRUCIAL: Set this.db before returning (invariant #5)
this.db = conn;
logger.log(
`[CapacitorPlatformService] [PRAGMA] fallback: PRAGMAs applied; this.db set (journal_mode=${journalMode})`,
`[CapacitorPlatformService] [PRAGMA] fallback: PRAGMAs applied; this.db set (journal_mode=${journalMode}, v3.3.3)`,
);
logger.log("[CapacitorPlatformService] PRAGMA path: fallback");
logger.log("[CapacitorPlatformService] PRAGMA path: fallback (v3.3.3)");
return { journalMode, path: "fallback" as const };
} catch (e) {