Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions crates/sqlx-sqlite-conn-mgr/src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
99 changes: 95 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<HashMap<String, DatabaseWrapper>>);
#[derive(Clone, Default)]
pub struct DbInstances(pub Arc<RwLock<HashMap<String, DatabaseWrapper>>>);

/// Builder for the SQLite plugin.
///
Expand Down Expand Up @@ -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::<DbInstances>().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<DatabaseWrapper> =
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::<DbInstances>();
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()
}
}
Expand Down