From 216dde07fd4704f8747d6a8cb229945ccd8896bc Mon Sep 17 00:00:00 2001 From: Paul Morris <10599524+1Cor125@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:02:46 -0500 Subject: [PATCH 1/3] feat: use tracing for logs - We protect against ever compiling log statements into release binaries --- Cargo.lock | 1 + README.md | 47 ++++++++++++++++++++- crates/sqlx-sqlite-conn-mgr/Cargo.toml | 1 + crates/sqlx-sqlite-conn-mgr/README.md | 8 ++++ crates/sqlx-sqlite-conn-mgr/src/database.rs | 7 +-- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 37e1cab..4e1dbef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3576,6 +3576,7 @@ dependencies = [ "sqlx", "thiserror 2.0.17", "tokio", + "tracing", ] [[package]] diff --git a/README.md b/README.md index 0114c12..83dcd70 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Run Rust tests: cargo test ``` -### Linting and standards checks +### Linting and Standards Checks ```bash npm run standards @@ -109,6 +109,51 @@ fn main() { } ``` +### Tracing and Logging + +This plugin and its connection manager crate use the +[`tracing`](https://crates.io/crates/tracing) ecosystem for internal logging. They are +configured with the `release_max_level_off` feature so that **all log statements are +compiled out of release builds**. This guarantees that logging from this plugin will never +reach production binaries unless you explicitly change that configuration. + +To see logs during development, initialize a `tracing-subscriber` in your Tauri +application crate and keep it behind a `debug_assertions` guard, for example: + +```toml +[dependencies] +tracing = { version = "0.1.41", default-features = false, features = ["std", "release_max_level_off"] } +tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter"] } +``` + +```rust +#[cfg(debug_assertions)] +fn init_tracing() { + use tracing_subscriber::{fmt, EnvFilter}; + + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("trace")); + + fmt().with_env_filter(filter).compact().init(); +} + +#[cfg(not(debug_assertions))] +fn init_tracing() {} + +fn main() { + init_tracing(); + + tauri::Builder::default() + .plugin(tauri_plugin_sqlite::init()) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} +``` + +With this setup, `tauri dev` shows all plugin and app logs, while `tauri build` produces +a release binary that contains no logging from this plugin or your app-level `tracing` +calls. + ### JavaScript/TypeScript API Install the JavaScript package in your frontend: diff --git a/crates/sqlx-sqlite-conn-mgr/Cargo.toml b/crates/sqlx-sqlite-conn-mgr/Cargo.toml index d204e27..49be472 100644 --- a/crates/sqlx-sqlite-conn-mgr/Cargo.toml +++ b/crates/sqlx-sqlite-conn-mgr/Cargo.toml @@ -10,3 +10,4 @@ rust-version = "1.89" sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] } thiserror = "2.0.17" tokio = { version = "1.48.0", features = ["full"] } +tracing = { version = "0.1.41", default-features = false, features = ["std", "release_max_level_off"] } diff --git a/crates/sqlx-sqlite-conn-mgr/README.md b/crates/sqlx-sqlite-conn-mgr/README.md index d59f689..01740bb 100644 --- a/crates/sqlx-sqlite-conn-mgr/README.md +++ b/crates/sqlx-sqlite-conn-mgr/README.md @@ -154,6 +154,14 @@ queries. * Idle timeout: 30 seconds by default (configurable via `custom_config`) * Only customize `SqliteDatabaseConfig` when defaults don't meet your needs +## Tracing and Logging + +This crate uses the [`tracing`](https://crates.io/crates/tracing) ecosystem for internal +instrumentation. It is built with the `release_max_level_off` feature so that all +`tracing` log statements are compiled out of release builds. To see its logs during +development, the host application must install a `tracing-subscriber` and enable the +desired log level; no extra configuration is required in this crate. + ## Error Handling ```rust diff --git a/crates/sqlx-sqlite-conn-mgr/src/database.rs b/crates/sqlx-sqlite-conn-mgr/src/database.rs index 7c39582..ea9a2d4 100644 --- a/crates/sqlx-sqlite-conn-mgr/src/database.rs +++ b/crates/sqlx-sqlite-conn-mgr/src/database.rs @@ -10,6 +10,7 @@ use sqlx::{ConnectOptions, Pool, Sqlite}; use std::path::{Path, PathBuf}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use tracing::error; /// SQLite database with connection pooling for concurrent reads and optional exclusive writes. /// @@ -273,11 +274,7 @@ impl SqliteDatabase { // Remove from registry if let Err(e) = uncache_database(&self.path).await { - // TODO: Investigate use of "tracing" crate to log this error - #[cfg(debug_assertions)] - eprintln!("Failed to remove database from cache: {}", e); - #[cfg(not(debug_assertions))] - let _ = e; // Suppress unused variable warning + error!("Failed to remove database from cache: {}", e); } // This will await all readers to be returned From 982f5f16f4fd1b1d69e543f06979779595664bdc Mon Sep 17 00:00:00 2001 From: Paul Morris <10599524+1Cor125@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:15:37 -0500 Subject: [PATCH 2/3] chore: clean up new lines and doc comments --- crates/sqlx-sqlite-conn-mgr/src/database.rs | 16 ++++++++++------ crates/sqlx-sqlite-conn-mgr/src/lib.rs | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/sqlx-sqlite-conn-mgr/src/database.rs b/crates/sqlx-sqlite-conn-mgr/src/database.rs index ea9a2d4..d05b3af 100644 --- a/crates/sqlx-sqlite-conn-mgr/src/database.rs +++ b/crates/sqlx-sqlite-conn-mgr/src/database.rs @@ -14,9 +14,9 @@ use tracing::error; /// SQLite database with connection pooling for concurrent reads and optional exclusive writes. /// -/// The database is opened in read-write mode but can be used for read-only operations -/// by calling `read_pool()`. Write operations are available by calling `acquire_writer()` -/// which lazily initializes WAL mode on first use. +/// Once the database is opened it can be used for read-only operations by calling `read_pool()`. +/// Write operations are available by calling `acquire_writer()` which lazily initializes WAL mode +/// on first use. /// /// # Example /// @@ -67,7 +67,7 @@ impl SqliteDatabase { /// If the database is already connected, returns the existing connection. /// Multiple calls with the same path will return the same database instance. /// - /// The database is created if it doesn't exist. WAL mode is optionally enabled when + /// The database is created if it doesn't exist. WAL mode is enabled when /// `acquire_writer()` is first called. /// /// # Arguments @@ -132,7 +132,8 @@ impl SqliteDatabase { // Why do we need to manually create the database file? We could just let the connection // create it if it doesn't exist, using `create_if_missing(true)`, right? Not if we called // connect and then our very first query was a read-only query, like `PRAGMA user_version;`, - // for example. That would fail because the read pool cannot create the file + // for example. That would fail because the read pool connections are read-only and cannot + // create the file if !db_exists && !is_memory_database(&path) { let create_options = SqliteConnectOptions::new() .filename(&path) @@ -175,7 +176,7 @@ impl SqliteDatabase { .await } - /// Get a reference to the connection pool for executing SELECT queries + /// Get a reference to the connection pool for executing read queries /// /// Use this for concurrent read operations. Multiple readers can access /// the pool simultaneously. @@ -367,6 +368,7 @@ mod tests { .fetch_one(db.read_pool().unwrap()) .await .unwrap(); + assert_eq!(count, 12); })); } @@ -673,6 +675,7 @@ mod tests { .fetch_all(db_clone.read_pool().unwrap()) .await .unwrap(); + assert!(rows.len() > 0); })); } @@ -700,6 +703,7 @@ mod tests { .fetch_one(db.read_pool().unwrap()) .await .unwrap(); + assert_eq!(count.0, 2); db.remove().await.unwrap(); diff --git a/crates/sqlx-sqlite-conn-mgr/src/lib.rs b/crates/sqlx-sqlite-conn-mgr/src/lib.rs index 59bdcaf..428e5af 100644 --- a/crates/sqlx-sqlite-conn-mgr/src/lib.rs +++ b/crates/sqlx-sqlite-conn-mgr/src/lib.rs @@ -56,7 +56,7 @@ //! - Uses sqlx's `SqlitePoolOptions` for all pool configuration //! - Uses sqlx's `SqliteConnectOptions` for connection flags and configuration //! - Minimal custom logic - delegates to sqlx wherever possible -//! - Global registry caches new database instances (with their pools) and returns existing ones +//! - Global registry caches new database instances and returns existing ones //! - WAL mode is enabled lazily only when writes are needed //! mod config; From 5da0d5f6cdbe341ffacbdb24e5e3d5758779426b Mon Sep 17 00:00:00 2001 From: Paul Morris <10599524+1Cor125@users.noreply.github.com> Date: Fri, 21 Nov 2025 11:50:54 -0500 Subject: [PATCH 3/3] config: set synchronous to NORMAL for writes - https://www.sqlite.org/wal.html#performance_considerations --- crates/sqlx-sqlite-conn-mgr/README.md | 26 +++++++++++++++++++++ crates/sqlx-sqlite-conn-mgr/src/database.rs | 16 +++++++++++++ 2 files changed, 42 insertions(+) diff --git a/crates/sqlx-sqlite-conn-mgr/README.md b/crates/sqlx-sqlite-conn-mgr/README.md index 01740bb..6b9130e 100644 --- a/crates/sqlx-sqlite-conn-mgr/README.md +++ b/crates/sqlx-sqlite-conn-mgr/README.md @@ -142,6 +142,32 @@ queries. The operation is idempotent and safe to call across multiple sessions, allowing concurrent reads during writes. + **Synchronous Mode: NORMAL vs FULL** + + When WAL mode is enabled, this library sets `PRAGMA synchronous = NORMAL` + instead of `FULL` for the following reasons: + + * **Performance**: `NORMAL` provides significantly better write performance + (up to 2-3x faster) by reducing the number of fsync operations. With `FULL`, + SQLite syncs after every checkpoint; with `NORMAL`, it syncs only the WAL file. + + * **Safety in WAL mode**: `NORMAL` is safe in WAL mode because: + * WAL transactions are atomic and durable at the WAL file level + * The database file itself can be checkpointed asynchronously + * A crash may corrupt the database file, but the WAL file remains intact + and will be used to recover on next open + * This is different from rollback journal mode where `NORMAL` could cause + corruption + + * **Mobile/Desktop Context**: For typical desktop and mobile applications, + `NORMAL` provides the best balance of performance and safety. `FULL` is + primarily needed for scenarios with unreliable storage hardware or when + power loss can occur mid-fsync operation. + + See [SQLite WAL Performance Considerations][wal-perf] for more details. + + [wal-perf]: https://www.sqlite.org/wal.html#performance_considerations + 4. **Exclusive Writes**: The write pool has `max_connections=1`, ensuring only one writer can exist at a time. Other callers to `acquire_writer()` will block (asynchronously) until the current writer diff --git a/crates/sqlx-sqlite-conn-mgr/src/database.rs b/crates/sqlx-sqlite-conn-mgr/src/database.rs index d05b3af..a992323 100644 --- a/crates/sqlx-sqlite-conn-mgr/src/database.rs +++ b/crates/sqlx-sqlite-conn-mgr/src/database.rs @@ -242,6 +242,11 @@ impl SqliteDatabase { .execute(&mut *conn) .await?; + // https://www.sqlite.org/wal.html#performance_considerations + sqlx::query("PRAGMA synchronous = NORMAL") + .execute(&mut *conn) + .await?; + self.wal_initialized.store(true, Ordering::SeqCst); } @@ -563,6 +568,17 @@ mod tests { "Journal mode should be WAL after first acquire_writer" ); + // Check sync setting + let (sync,): (i32,) = sqlx::query_as("PRAGMA synchronous") + .fetch_one(&mut *writer) + .await + .unwrap(); + + assert_eq!( + sync, 1, + "Sync mode should be NORMAL after first acquire_writer" + ); + drop(writer); db.remove().await.unwrap();