From ee25dfaee21780f22b5e4597c6dd69611c3e72c2 Mon Sep 17 00:00:00 2001 From: Paul Morris <10599524+1Cor125@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:34:08 -0500 Subject: [PATCH 1/2] perf: optimize on conn close - SQLite recommends running PRAGMA optimize on close to boost query perf so we will do what SQLite recommends :) --- crates/sqlx-sqlite-conn-mgr/src/database.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/crates/sqlx-sqlite-conn-mgr/src/database.rs b/crates/sqlx-sqlite-conn-mgr/src/database.rs index d7b0594..1363303 100644 --- a/crates/sqlx-sqlite-conn-mgr/src/database.rs +++ b/crates/sqlx-sqlite-conn-mgr/src/database.rs @@ -144,8 +144,16 @@ impl SqliteDatabase { drop(conn); // Close immediately after creating the file } + // Enable PRAGMA optimize on close as recommended by SQLite for long-lived databases. + // SQLite recommends analysis_limit values between 100-1000 for older versions; + // SQLite 3.46.0+ handles limits automatically. + // https://www.sqlite.org/lang_analyze.html#recommended_usage_pattern + // // Create read pool with read-only connections - let read_options = SqliteConnectOptions::new().filename(&path).read_only(true); + let read_options = SqliteConnectOptions::new() + .filename(&path) + .read_only(true) + .optimize_on_close(true, 400); let read_pool = SqlitePoolOptions::new() .max_connections(config.max_read_connections) @@ -157,7 +165,10 @@ impl SqliteDatabase { .await?; // Create write pool with a single read-write connection - let write_options = SqliteConnectOptions::new().filename(&path).read_only(false); + let write_options = SqliteConnectOptions::new() + .filename(&path) + .read_only(false) + .optimize_on_close(true, 400); let write_conn = SqlitePoolOptions::new() .max_connections(1) From b75ae26dd614d1d67a2a099fd28cb6ad974252e5 Mon Sep 17 00:00:00 2001 From: Paul Morris <10599524+1Cor125@users.noreply.github.com> Date: Wed, 3 Dec 2025 15:36:55 -0500 Subject: [PATCH 2/2] feat: close dbs when about to exit - This will close connections, truncate WAL files and drain the db cache --- Cargo.lock | 2 +- Cargo.toml | 4 +-- README.md | 2 +- src/lib.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 99 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c461e30..bbf01fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3931,7 +3931,6 @@ name = "tauri-plugin-sqlite" version = "0.1.0" dependencies = [ "base64 0.22.1", - "futures-core", "indexmap 2.12.1", "log", "serde", @@ -3944,6 +3943,7 @@ dependencies = [ "thiserror 2.0.17", "time", "tokio", + "tracing", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fa9bcbb..ef8e366 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,11 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" thiserror = "2.0.17" log = "0.4.28" -futures-core = "0.3.31" time = "0.3.44" -tokio = { version = "1.48.0", features = ["sync"] } +tokio = { version = "1.48.0", features = ["rt", "sync", "time"] } indexmap = { version = "2.12.1", features = ["serde"] } base64 = "0.22.1" +tracing = { version = "0.1.41", default-features = false, features = ["std", "release_max_level_off"] } # SQLx for types and queries (time feature enables datetime type decoding) sqlx = { version = "0.8.6", features = ["sqlite", "json", "time", "runtime-tokio"] } diff --git a/README.md b/README.md index 3295125..43a14ca 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ SQLite database interface for Tauri applications using even while writing (configurable pool size and idle timeouts) * **Write Serialization**: Exclusive write connection - > Wait! Why? From [SQLite docs](https://sqlite.org/whentouse.html): + > From [SQLite docs](https://sqlite.org/whentouse.html): > "_SQLite ... will only allow one writer at any instant in time._" * **WAL Mode**: Enabled automatically on first write operation * **Type Safety**: Full TypeScript bindings diff --git a/src/lib.rs b/src/lib.rs index f22186f..6d38e1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; +use std::sync::Arc; -use tauri::{Manager, Runtime, plugin::Builder as PluginBuilder}; +use tauri::{Manager, RunEvent, Runtime, plugin::Builder as PluginBuilder}; use tokio::sync::RwLock; +use tracing::{debug, error, info, warn}; mod commands; mod decode; @@ -15,8 +17,8 @@ pub use wrapper::{DatabaseWrapper, WriteQueryResult}; /// /// This struct maintains a thread-safe map of database paths to their corresponding /// connection wrappers. -#[derive(Default)] -pub struct DbInstances(pub RwLock>); +#[derive(Clone, Default)] +pub struct DbInstances(pub Arc>>); /// Builder for the SQLite plugin. /// @@ -57,10 +59,99 @@ impl Builder { ]) .setup(|app, _api| { app.manage(DbInstances::default()); + debug!("SQLite plugin initialized"); // Future PR: Possibly handle migrations here - // Future PR: Cleanup on app exit Ok(()) }) + .on_event(|app, event| { + match event { + RunEvent::ExitRequested { api, code, .. } => { + info!("App exit requested (code: {:?}) - closing databases before exit", code); + + // Prevent immediate exit so we can close connections and checkpoint WAL + api.prevent_exit(); + + let app_handle = app.clone(); + + let handle = match tokio::runtime::Handle::try_current() { + Ok(h) => h, + Err(_) => { + warn!("No tokio runtime available for database cleanup"); + app_handle.exit(code.unwrap_or(0)); + return; + } + }; + + let instances = app.state::().inner().clone(); + + // Spawn a blocking thread to close databases + // (block_in_place panics on current_thread runtime) + let cleanup_result = std::thread::spawn(move || { + handle.block_on(async { + let mut guard = instances.0.write().await; + let wrappers: Vec = + guard.drain().map(|(_, v)| v).collect(); + + // Close databases in parallel with timeout + let mut set = tokio::task::JoinSet::new(); + for wrapper in wrappers { + set.spawn(async move { wrapper.close().await }); + } + + let timeout_result = tokio::time::timeout( + std::time::Duration::from_secs(5), + async { + while let Some(result) = set.join_next().await { + match result { + Ok(Err(e)) => warn!("Error closing database: {:?}", e), + Err(e) => warn!("Database close task panicked: {:?}", e), + Ok(Ok(())) => {} + } + } + }, + ) + .await; + + if timeout_result.is_err() { + warn!("Database cleanup timed out after 5 seconds"); + } else { + debug!("Database cleanup complete"); + } + }) + }) + .join(); + + if let Err(e) = cleanup_result { + error!("Database cleanup thread panicked: {:?}", e); + } + + app_handle.exit(code.unwrap_or(0)); + } + RunEvent::Exit => { + // ExitRequested should have already closed all databases + // This is just a safety check + let instances = app.state::(); + match instances.0.try_read() { + Ok(guard) => { + if !guard.is_empty() { + warn!( + "Exit event fired with {} database(s) still open - cleanup may have been skipped", + guard.len() + ); + } else { + debug!("Exit event: all databases already closed"); + } + } + Err(_) => { + warn!("Exit event: could not check database state (lock held - cleanup may still be in progress)"); + } + } + } + _ => { + // Other events don't require action + } + } + }) .build() } }