Fix SQLITE_READONLY(8): move WAL/synchronous PRAGMAs out of intercept…#37
Conversation
…or into one-time startup connection - Remove PRAGMA journal_mode=WAL and synchronous=NORMAL from SqlCipherConnectionInterceptor so they no longer run on EF Core read-only connections (which caused SQLITE_READONLY error 8) - Add one-time writable startup connection in WebServiceExtensions.cs to apply WAL/synchronous before EF Core registers any contexts - Remove diagnostic Console.WriteLines from interceptor and WebServiceExtensions - Explicitly reference bundle_e_sqlcipher in SimpleStart.csproj - Add pending migration count logging in DatabaseService
There was a problem hiding this comment.
Pull request overview
This PR addresses SQLITE_READONLY(8) errors caused by executing PRAGMA journal_mode=WAL on EF Core read-only SQLite connections by moving WAL/synchronous configuration out of the EF connection interceptor and into a one-time startup connection, while also updating SQLCipher wiring and adding migration-count logging.
Changes:
- Move
journal_mode=WALandsynchronous=NORMALconfiguration fromSqlCipherConnectionInterceptorto a one-time writable startup connection inWebServiceExtensions. - Switch SimpleStart to explicitly reference
SQLitePCLRaw.bundle_e_sqlcipherinstead ofbundle_e_sqlite3. - Add logging for pending migration counts in
DatabaseServiceand add additional SQLCipher cipher PRAGMAs in the interceptor.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| 4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs | Adds a one-time writable connection to set WAL/synchronous during startup and logs unlock state. |
| 4-Aquiis.SimpleStart/Aquiis.SimpleStart.csproj | Replaces SQLite native bundle reference with SQLCipher bundle. |
| 2-Aquiis.Application/Services/DatabaseService.cs | Logs pending migration counts for business and identity contexts. |
| 1-Aquiis.Infrastructure/Data/SqlCipherConnectionInterceptor.cs | Removes WAL/synchronous PRAGMAs and expands SQLCipher cipher PRAGMAs applied on connection open. |
Comments suppressed due to low confidence (4)
2-Aquiis.Application/Services/DatabaseService.cs:82
pendingis enumerated twice here (once for logging, once for return). Store the count in a local variable to avoid double enumeration, and prefer structured logging (e.g., message template + {Count}) rather than interpolated strings for better queryability.
var pending = await _businessContext.Database.GetPendingMigrationsAsync();
_logger.LogInformation($"Business context has {pending.Count()} pending migrations.");
return pending.Count();
}
2-Aquiis.Application/Services/DatabaseService.cs:89
- Same issue as the business-context method:
pending.Count()is computed twice and the log uses string interpolation. Cache the count in a local and use structured logging so the pending migration count is consistently recorded as a numeric field.
var pending = await _identityContext.Database.GetPendingMigrationsAsync();
_logger.LogInformation($"Identity context has {pending.Count()} pending migrations.");
return pending.Count();
}
4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs:67
- The PR description says diagnostic Console.WriteLines were removed, but this adds a new Console.WriteLine during service registration. Consider removing this (or gating it behind EnableVerboseLogging and using ILogger at Debug level) to avoid leaking environment details like DatabasePath and to keep startup output clean.
Console.WriteLine($"Database unlock state: NeedsUnlock={unlockState.NeedsUnlock}, DatabasePath={unlockState.DatabasePath}");
4-Aquiis.SimpleStart/Extensions/WebServiceExtensions.cs:105
- This startup setup connection duplicates low-level PRAGMA initialization logic that also exists in SqlCipherConnectionInterceptor, but it only sets PRAGMA key/journal_mode/synchronous. To reduce drift (and to ensure encrypted DBs opened here use the same cipher parameters as normal EF connections), consider factoring this into a shared helper that applies the full required SQLCipher PRAGMAs + WAL settings in one place and reuse it from each host (Web/Electron).
// Set WAL mode and synchronous=NORMAL once on a writable connection.
// These are persistent database settings — no need to repeat on every connection open.
// This MUST happen here (not in the interceptor) because EF Core also opens read-only
// connections (e.g. SqliteDatabaseCreator.Exists()), and PRAGMA journal_mode = WAL
// returns SQLITE_READONLY (8) on a read-only connection.
using var setupConn = new SqliteConnection(connectionString);
setupConn.Open();
using var setupCmd = setupConn.CreateCommand();
setupCmd.CommandText = $"PRAGMA key = \"{encryptionPassword}\";";
setupCmd.ExecuteNonQuery();
setupCmd.CommandText = "PRAGMA journal_mode = WAL;";
setupCmd.ExecuteNonQuery();
setupCmd.CommandText = "PRAGMA synchronous = NORMAL;";
setupCmd.ExecuteNonQuery();
| setupCmd.CommandText = $"PRAGMA key = \"{encryptionPassword}\";"; | ||
| setupCmd.ExecuteNonQuery(); |
There was a problem hiding this comment.
Building the SQLCipher key PRAGMA via string interpolation/quoting is brittle (passwords/raw keys containing quotes will break) and makes the command text depend on unescaped input. Use a parameterized command (or reuse the same PRAGMA-key-setting helper logic as the interceptor, including handling raw-key vs passphrase quoting) so the setup connection reliably opens databases and avoids injection/escaping issues.
| setupCmd.CommandText = $"PRAGMA key = \"{encryptionPassword}\";"; | |
| setupCmd.ExecuteNonQuery(); | |
| setupCmd.CommandText = "PRAGMA key = $key;"; | |
| setupCmd.Parameters.AddWithValue("$key", encryptionPassword); | |
| setupCmd.ExecuteNonQuery(); | |
| setupCmd.Parameters.Clear(); |
…or into one-time startup connection