From aa20e07a9fc4186c78086348794be1395a78ca4c Mon Sep 17 00:00:00 2001 From: Aditya-9-6 Date: Sun, 6 Sep 2026 16:31:02 +0530 Subject: [PATCH 1/2] feat(local): set WAL journal mode as default for local databases (fixes #1553) - Sets SQLite journal mode to WAL (PRAGMA journal_mode = WAL) by default when opening local file-based database connections. - Safely skips setting WAL mode for in-memory (:memory:) databases and read-only connections (SQLITE_OPEN_READONLY). - Adds integration tests verifying default WAL mode, in-memory databases, and read-only connections. - Gates pprof under cfg(not(windows)) in dev-dependencies to support building and testing on Windows. --- libsql/Cargo.toml | 4 +- libsql/src/local/connection.rs | 12 ++++- libsql/tests/integration_tests.rs | 83 +++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/libsql/Cargo.toml b/libsql/Cargo.toml index e8a7e0f909..42126230e3 100644 --- a/libsql/Cargo.toml +++ b/libsql/Cargo.toml @@ -49,13 +49,15 @@ chrono = { version = "0.4", optional = true } [dev-dependencies] criterion = { version = "0.5", features = ["html_reports", "async", "async_futures", "async_tokio"] } -pprof = { version = "0.14.0", features = ["criterion", "flamegraph"] } tokio = { version = "1.29.1", features = ["full"] } tokio-test = "0.4" tracing-subscriber = "0.3" tempfile = { version = "3.7.0" } rand = "0.8.5" +[target.'cfg(not(windows))'.dev-dependencies] +pprof = { version = "0.14.0", features = ["criterion", "flamegraph"] } + [features] default = ["core", "replication", "remote", "sync", "tls"] core = [ diff --git a/libsql/src/local/connection.rs b/libsql/src/local/connection.rs index 7012651699..b0b8afeceb 100644 --- a/libsql/src/local/connection.rs +++ b/libsql/src/local/connection.rs @@ -77,12 +77,22 @@ impl Connection { writer: db.writer()?, authorizer: Arc::new(RwLock::new(None)), }; + + let is_read_only = (db.flags.bits() & ffi::SQLITE_OPEN_READONLY) != 0; + let is_memory = db.db_path == ":memory:" || db.db_path.is_empty(); + + if !is_read_only && !is_memory { + conn.query("PRAGMA journal_mode = WAL", Params::None)?; + } + #[cfg(feature = "sync")] if let Some(_) = db.sync_ctx { // We need to make sure database is in WAL mode with checkpointing // disabled so that we can sync our changes back to a remote // server. - conn.query("PRAGMA journal_mode = WAL", Params::None)?; + if is_memory { + conn.query("PRAGMA journal_mode = WAL", Params::None)?; + } conn.wal_disable_checkpoint()?; } Ok(conn) diff --git a/libsql/tests/integration_tests.rs b/libsql/tests/integration_tests.rs index 697ac220ef..c690fe04c6 100644 --- a/libsql/tests/integration_tests.rs +++ b/libsql/tests/integration_tests.rs @@ -925,3 +925,86 @@ fn assert_sqlite_error(res: Result, code: i32) { } } } + +#[tokio::test] +async fn test_default_journal_mode_is_wal() { + let temp_dir = tempfile::tempdir().unwrap(); + let db_path = temp_dir.path().join("wal_test.db"); + + // Test with Builder + let db = libsql::Builder::new_local(&db_path).build().await.unwrap(); + let conn = db.connect().unwrap(); + let mut rows = conn.query("PRAGMA journal_mode;", ()).await.unwrap(); + let row = rows.next().await.unwrap().unwrap(); + let mode: String = row.get(0).unwrap(); + assert_eq!(mode.to_lowercase(), "wal"); + + // Test with Database::open + let db_path2 = temp_dir.path().join("wal_test2.db"); + let db2 = Database::open(db_path2.to_str().unwrap()).unwrap(); + let conn2 = db2.connect().unwrap(); + let mut rows2 = conn2.query("PRAGMA journal_mode;", ()).await.unwrap(); + let row2 = rows2.next().await.unwrap().unwrap(); + let mode2: String = row2.get(0).unwrap(); + assert_eq!(mode2.to_lowercase(), "wal"); +} + +#[tokio::test] +async fn test_memory_journal_mode_is_memory() { + let db = libsql::Builder::new_local(":memory:") + .build() + .await + .unwrap(); + let conn = db.connect().unwrap(); + let mut rows = conn.query("PRAGMA journal_mode;", ()).await.unwrap(); + let row = rows.next().await.unwrap().unwrap(); + let mode: String = row.get(0).unwrap(); + assert_eq!(mode.to_lowercase(), "memory"); + + let db2 = Database::open(":memory:").unwrap(); + let conn2 = db2.connect().unwrap(); + let mut rows2 = conn2.query("PRAGMA journal_mode;", ()).await.unwrap(); + let row2 = rows2.next().await.unwrap().unwrap(); + let mode2: String = row2.get(0).unwrap(); + assert_eq!(mode2.to_lowercase(), "memory"); +} + +#[tokio::test] +async fn test_readonly_connection_succeeds() { + let temp_dir = tempfile::tempdir().unwrap(); + let db_path = temp_dir.path().join("readonly.db"); + + // Initialize database with table and data + { + let db = libsql::Builder::new_local(&db_path).build().await.unwrap(); + let conn = db.connect().unwrap(); + conn.execute("CREATE TABLE test (id INTEGER, val TEXT)", ()) + .await + .unwrap(); + conn.execute("INSERT INTO test VALUES (1, 'hello')", ()) + .await + .unwrap(); + } + + // Reopen as read-only + let db_ro = libsql::Builder::new_local(&db_path) + .flags(libsql::OpenFlags::SQLITE_OPEN_READ_ONLY) + .build() + .await + .unwrap(); + let conn_ro = db_ro.connect().unwrap(); + + // Verify reading works and journal_mode is preserved + let mut rows = conn_ro + .query("SELECT val FROM test WHERE id = 1", ()) + .await + .unwrap(); + let row = rows.next().await.unwrap().unwrap(); + let val: String = row.get(0).unwrap(); + assert_eq!(val, "hello"); + + let mut jm_rows = conn_ro.query("PRAGMA journal_mode;", ()).await.unwrap(); + let jm_row = jm_rows.next().await.unwrap().unwrap(); + let mode: String = jm_row.get(0).unwrap(); + assert_eq!(mode.to_lowercase(), "wal"); +} From 8c899bd4cac5b3b4a716b35c0a3ebeb13bab976c Mon Sep 17 00:00:00 2001 From: Aditya-9-6 Date: Sun, 6 Sep 2026 17:02:49 +0530 Subject: [PATCH 2/2] fix: configure WAL mode after setting encryption key on local file databases - Moves PRAGMA journal_mode = WAL execution to DbType::File after encryption cipher and key are set. - Restores Connection::connect to avoid executing SQL before encryption keys are applied to raw SQLite handles, fixing 'file is not a database' on encrypted databases and embedded replicas. --- libsql/src/database.rs | 5 +++++ libsql/src/local/connection.rs | 12 +----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/libsql/src/database.rs b/libsql/src/database.rs index b4da6171bf..75902f8783 100644 --- a/libsql/src/database.rs +++ b/libsql/src/database.rs @@ -623,6 +623,11 @@ impl Database { } } + let is_read_only = flags.contains(OpenFlags::SQLITE_OPEN_READ_ONLY); + if !is_read_only { + conn.query("PRAGMA journal_mode = WAL", crate::params::Params::None)?; + } + let conn = std::sync::Arc::new(LibsqlConnection { conn }); Ok(Connection { conn }) diff --git a/libsql/src/local/connection.rs b/libsql/src/local/connection.rs index b0b8afeceb..7012651699 100644 --- a/libsql/src/local/connection.rs +++ b/libsql/src/local/connection.rs @@ -77,22 +77,12 @@ impl Connection { writer: db.writer()?, authorizer: Arc::new(RwLock::new(None)), }; - - let is_read_only = (db.flags.bits() & ffi::SQLITE_OPEN_READONLY) != 0; - let is_memory = db.db_path == ":memory:" || db.db_path.is_empty(); - - if !is_read_only && !is_memory { - conn.query("PRAGMA journal_mode = WAL", Params::None)?; - } - #[cfg(feature = "sync")] if let Some(_) = db.sync_ctx { // We need to make sure database is in WAL mode with checkpointing // disabled so that we can sync our changes back to a remote // server. - if is_memory { - conn.query("PRAGMA journal_mode = WAL", Params::None)?; - } + conn.query("PRAGMA journal_mode = WAL", Params::None)?; conn.wal_disable_checkpoint()?; } Ok(conn)