From 32e92d484bd20d0ce83868ebfa8cafb912bb244e Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sat, 15 Aug 2026 14:55:54 -0700 Subject: [PATCH] Let yaak-models compile for wasm32-unknown-unknown (#556) --- Cargo.lock | 104 +++++++------- crates-proxy/yaak-proxy-lib/Cargo.toml | 6 +- crates-tauri/yaak-app-client/Cargo.toml | 2 +- crates/common/yaak-database/Cargo.toml | 18 ++- .../yaak-database/src/connection_or_tx.rs | 5 +- crates/common/yaak-database/src/db_context.rs | 1 + crates/common/yaak-database/src/error.rs | 2 +- crates/common/yaak-database/src/lib.rs | 7 +- crates/common/yaak-database/src/migrate.rs | 5 +- crates/common/yaak-database/src/pool.rs | 80 +++++++++++ crates/yaak-models/Cargo.toml | 12 +- crates/yaak-models/src/blob_manager.rs | 17 ++- crates/yaak-models/src/error.rs | 2 +- crates/yaak-models/src/lib.rs | 132 ++++++++++-------- crates/yaak-models/src/migrate.rs | 32 +++-- .../src/queries/graphql_introspections.rs | 1 + .../src/queries/grpc_connections.rs | 1 + .../yaak-models/src/queries/http_responses.rs | 1 + crates/yaak-models/src/queries/key_values.rs | 1 + .../src/queries/plugin_key_values.rs | 1 + crates/yaak-models/src/queries/sync_states.rs | 1 + .../src/queries/websocket_connections.rs | 1 + crates/yaak-models/src/query_manager.rs | 17 ++- 23 files changed, 296 insertions(+), 153 deletions(-) create mode 100644 crates/common/yaak-database/src/pool.rs diff --git a/Cargo.lock b/Cargo.lock index 1a94f7d2..b8ba4b2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,18 +40,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "0.6.10" @@ -1066,10 +1054,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.26" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" dependencies = [ + "find-msvc-tools", "jobserver", "libc", "shlex", @@ -2417,6 +2406,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + [[package]] name = "fixedbitset" version = "0.4.2" @@ -2849,8 +2844,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -3091,7 +3088,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash 0.7.8", + "ahash", ] [[package]] @@ -3099,9 +3096,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.12", -] [[package]] name = "hashbrown" @@ -3131,11 +3125,11 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "hashlink" -version = "0.9.1" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.16.1", ] [[package]] @@ -3561,17 +3555,6 @@ dependencies = [ "cfb", ] -[[package]] -name = "inherent" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c38228f24186d9cc68c729accb4d413be9eaed6ad07ff79e0270d9e56f3de13" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.101", -] - [[package]] name = "inotify" version = "0.11.0" @@ -4007,9 +3990,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.30.1" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a" dependencies = [ "cc", "pkg-config", @@ -6242,9 +6225,9 @@ dependencies = [ [[package]] name = "r2d2_sqlite" -version = "0.25.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb14dba8247a6a15b7fdbc7d389e2e6f03ee9f184f87117706d509c092dfe846" +checksum = "a2ebd03c29250cdf191da93a35118b4567c2ef0eacab54f65e058d6f4c9965f6" dependencies = [ "r2d2", "rusqlite", @@ -7130,10 +7113,20 @@ dependencies = [ ] [[package]] -name = "rusqlite" -version = "0.32.1" +name = "rsqlite-vfs" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.17", +] + +[[package]] +name = "rusqlite" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ "bitflags 2.11.0", "chrono", @@ -7142,6 +7135,7 @@ dependencies = [ "hashlink", "libsqlite3-sys", "smallvec", + "sqlite-wasm-rs", ] [[package]] @@ -7423,20 +7417,20 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sea-query" -version = "0.32.6" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c91783d1514b99754fc6a4079081dcc2c587dadbff65c48c7f62297443536a" +checksum = "546040c653a705e60ec65ecd3191a809603734bebbc225775916dea9ae409b31" dependencies = [ "chrono", - "inherent", + "itoa", "sea-query-derive", ] [[package]] name = "sea-query-derive" -version = "0.4.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab" +checksum = "a0b0f466921cdd3cf4b89d5c3ac2173dba89a873ab395b123a645de181ec7537" dependencies = [ "darling 0.20.11", "heck 0.4.1", @@ -7448,9 +7442,9 @@ dependencies = [ [[package]] name = "sea-query-rusqlite" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3743bbdfb24b1a84cc1a6fbf4b1188e6851f6e00ea20944b44c56bf03a585bb4" +checksum = "1ec6038023c8517c623e5bf9606b3c54d40bc8296bb6b2986040428dd84deddd" dependencies = [ "rusqlite", "sea-query", @@ -7861,9 +7855,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook" @@ -8031,6 +8025,18 @@ dependencies = [ "system-deps", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -11136,6 +11142,7 @@ name = "yaak-database" version = "0.1.0" dependencies = [ "chrono", + "getrandom 0.2.16", "include_dir", "log 0.4.29", "nanoid", @@ -11148,6 +11155,7 @@ dependencies = [ "serde_json", "thiserror 2.0.17", "ts-rs", + "uuid", ] [[package]] diff --git a/crates-proxy/yaak-proxy-lib/Cargo.toml b/crates-proxy/yaak-proxy-lib/Cargo.toml index a4492de7..0eb2dd8c 100644 --- a/crates-proxy/yaak-proxy-lib/Cargo.toml +++ b/crates-proxy/yaak-proxy-lib/Cargo.toml @@ -10,9 +10,9 @@ chrono = { workspace = true, features = ["serde"] } log = { workspace = true } include_dir = "0.7" r2d2 = "0.8.10" -r2d2_sqlite = "0.25.0" -rusqlite = { version = "0.32.1", features = ["bundled", "chrono"] } -sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] } +r2d2_sqlite = "0.32" +rusqlite = { version = "0.38", features = ["bundled", "chrono"] } +sea-query = { version = "1.0", features = ["with-chrono", "attr"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } ts-rs = { workspace = true, features = ["chrono-impl"] } diff --git a/crates-tauri/yaak-app-client/Cargo.toml b/crates-tauri/yaak-app-client/Cargo.toml index a6f7555c..a307ad70 100644 --- a/crates-tauri/yaak-app-client/Cargo.toml +++ b/crates-tauri/yaak-app-client/Cargo.toml @@ -39,7 +39,7 @@ md5 = "0.8.0" notify = "8.0.0" pretty_graphql = "0.2" r2d2 = "0.8.10" -r2d2_sqlite = "0.25.0" +r2d2_sqlite = "0.32" mime_guess = "2.0.5" rand = "0.9.0" reqwest = { workspace = true, features = [ diff --git a/crates/common/yaak-database/Cargo.toml b/crates/common/yaak-database/Cargo.toml index f02c4fb2..9ccd4338 100644 --- a/crates/common/yaak-database/Cargo.toml +++ b/crates/common/yaak-database/Cargo.toml @@ -9,12 +9,20 @@ chrono = { version = "0.4.38", features = ["serde"] } include_dir = "0.7" log = { workspace = true } nanoid = "0.4.0" -r2d2 = "0.8.10" -r2d2_sqlite = { version = "0.25.0" } -rusqlite = { version = "0.32.1", features = ["bundled", "chrono"] } -sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] } -sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] } +rusqlite = { version = "0.38", features = ["bundled", "chrono"] } +sea-query-rusqlite = { version = "0.8.0", features = ["with-chrono"] } +sea-query = { version = "1.0", features = ["with-chrono", "attr"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } thiserror = { workspace = true } ts-rs = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +r2d2 = "0.8.10" +r2d2_sqlite = { version = "0.32" } + +# nanoid pulls getrandom, which needs to be told how to reach the browser's +# CSPRNG on wasm32-unknown-unknown. Native targets are unaffected. +[target.'cfg(target_arch = "wasm32")'.dependencies] +getrandom = { version = "0.2", features = ["js"] } +uuid = { version = "1", features = ["js"] } diff --git a/crates/common/yaak-database/src/connection_or_tx.rs b/crates/common/yaak-database/src/connection_or_tx.rs index bad2e1dc..4f37f362 100644 --- a/crates/common/yaak-database/src/connection_or_tx.rs +++ b/crates/common/yaak-database/src/connection_or_tx.rs @@ -1,9 +1,8 @@ -use r2d2::PooledConnection; -use r2d2_sqlite::SqliteConnectionManager; +use crate::pool::SqliteConn; use rusqlite::{Connection, Statement, ToSql, Transaction}; pub enum ConnectionOrTx<'a> { - Connection(PooledConnection), + Connection(SqliteConn), Transaction(&'a Transaction<'a>), } diff --git a/crates/common/yaak-database/src/db_context.rs b/crates/common/yaak-database/src/db_context.rs index eabafef4..ba874d15 100644 --- a/crates/common/yaak-database/src/db_context.rs +++ b/crates/common/yaak-database/src/db_context.rs @@ -3,6 +3,7 @@ use crate::error::Error::ModelNotFound; use crate::error::Result; use crate::traits::UpsertModelInfo; use crate::update_source::UpdateSource; +use sea_query::ExprTrait; use sea_query::{ Asterisk, Expr, Func, IntoColumnRef, IntoIden, OnConflict, Query, SimpleExpr, SqliteQueryBuilder, diff --git a/crates/common/yaak-database/src/error.rs b/crates/common/yaak-database/src/error.rs index eff15c12..f30478c1 100644 --- a/crates/common/yaak-database/src/error.rs +++ b/crates/common/yaak-database/src/error.rs @@ -7,7 +7,7 @@ pub enum Error { SqlError(#[from] rusqlite::Error), #[error("SQL Pool error: {0}")] - SqlPoolError(#[from] r2d2::Error), + SqlPoolError(#[from] crate::pool::PoolError), #[error("Database error: {0}")] Database(String), diff --git a/crates/common/yaak-database/src/lib.rs b/crates/common/yaak-database/src/lib.rs index 54929ea5..da45ac08 100644 --- a/crates/common/yaak-database/src/lib.rs +++ b/crates/common/yaak-database/src/lib.rs @@ -2,6 +2,7 @@ pub mod connection_or_tx; pub mod db_context; pub mod error; pub mod migrate; +pub mod pool; pub mod traits; pub mod update_source; pub mod util; @@ -11,13 +12,15 @@ pub use connection_or_tx::ConnectionOrTx; pub use db_context::DbContext; pub use error::{Error, Result}; pub use migrate::run_migrations; +pub use pool::{PoolError, SqliteConn, SqlitePool}; pub use traits::{UpsertModelInfo, upsert_date}; pub use update_source::{ModelChangeEvent, UpdateSource}; pub use util::{generate_id, generate_id_of_length, generate_prefixed_id}; -// Re-export pool types that consumers will need +// Re-export types that consumers will need +#[cfg(not(target_arch = "wasm32"))] pub use r2d2; +#[cfg(not(target_arch = "wasm32"))] pub use r2d2_sqlite; pub use rusqlite; pub use sea_query; -pub use sea_query_rusqlite; diff --git a/crates/common/yaak-database/src/migrate.rs b/crates/common/yaak-database/src/migrate.rs index 30c53dcd..49fb5903 100644 --- a/crates/common/yaak-database/src/migrate.rs +++ b/crates/common/yaak-database/src/migrate.rs @@ -1,8 +1,7 @@ use crate::error::Result; +use crate::pool::SqlitePool; use include_dir::Dir; use log::{debug, info}; -use r2d2::Pool; -use r2d2_sqlite::SqliteConnectionManager; use rusqlite::{OptionalExtension, params}; const TRACKING_TABLE: &str = "_sqlx_migrations"; @@ -11,7 +10,7 @@ const TRACKING_TABLE: &str = "_sqlx_migrations"; /// /// Migrations are sorted by filename (use timestamp prefixes like `00000001_init.sql`). /// Applied migrations are tracked in `_sqlx_migrations`. -pub fn run_migrations(pool: &Pool, dir: &Dir<'_>) -> Result<()> { +pub fn run_migrations(pool: &SqlitePool, dir: &Dir<'_>) -> Result<()> { info!("Running migrations"); // Create tracking table diff --git a/crates/common/yaak-database/src/pool.rs b/crates/common/yaak-database/src/pool.rs new file mode 100644 index 00000000..863087da --- /dev/null +++ b/crates/common/yaak-database/src/pool.rs @@ -0,0 +1,80 @@ +//! Where connections come from. +//! +//! Every query in the model layer asks a pool for a connection, uses it, and +//! hands it back. That is the whole contract, and it is the one place the +//! desktop and the browser genuinely differ: the desktop has threads and wants +//! an r2d2 pool; a browser tab has one thread, no way to spawn another, and one +//! connection is exactly enough. Everything above this module is identical on +//! both. +//! +//! On native targets `SqlitePool` *is* `r2d2::Pool` — a type alias, so nothing +//! that already builds pools changes. On wasm it is one connection that every +//! `get()` hands out a shared handle to. +//! +//! A `SqliteConn` only ever derefs immutably. The code above this layer opens +//! transactions with [`rusqlite::Transaction::new_unchecked`], which takes +//! `&Connection`; the `&mut` that `Connection::transaction` demands is a +//! compile-time guard against nesting a transaction on one connection, and it +//! is what would have forced the wasm pool to lend its connection exclusively. +//! The model layer nests connections freely — a helper that already holds one +//! calls another that asks for its own — so an exclusive lend would panic on +//! the second ask. Sharing the handle instead makes nested *reads* work the way +//! they do on the desktop; nested *write transactions* fail on both, only +//! differently (here SQLite refuses the inner `BEGIN`; natively the inner +//! connection blocks on `busy_timeout` and then fails). + +#[cfg(not(target_arch = "wasm32"))] +mod imp { + use r2d2_sqlite::SqliteConnectionManager; + + pub type SqlitePool = r2d2::Pool; + pub type SqliteConn = r2d2::PooledConnection; + pub type PoolError = r2d2::Error; +} + +#[cfg(target_arch = "wasm32")] +mod imp { + use rusqlite::Connection; + use std::ops::Deref; + use std::rc::Rc; + + /// One connection, shared by everyone who asks. + /// + /// `Rc` rather than `Arc` because a `Connection` is `!Sync`, so wrapping + /// it in an `Arc` would buy no `Send`/`Sync` anyway — and there is one + /// thread here to be honest about. + #[derive(Clone, Debug)] + pub struct SqlitePool { + conn: Rc, + } + + impl SqlitePool { + pub fn single(conn: Connection) -> Self { + Self { conn: Rc::new(conn) } + } + + /// Another handle to the connection. Cannot fail; the `Result` keeps + /// the signature identical to r2d2's so callers are written once. + pub fn get(&self) -> Result { + Ok(SqliteConn(self.conn.clone())) + } + } + + /// The error a `get()` would return if it could. It can't, so this has no + /// variants; it exists so `Error::SqlPoolError` has the same shape on both + /// targets. + #[derive(Debug, thiserror::Error)] + pub enum PoolError {} + + #[derive(Debug)] + pub struct SqliteConn(Rc); + + impl Deref for SqliteConn { + type Target = Connection; + fn deref(&self) -> &Connection { + &self.0 + } + } +} + +pub use imp::*; diff --git a/crates/yaak-models/Cargo.toml b/crates/yaak-models/Cargo.toml index f28f0d56..fe781836 100644 --- a/crates/yaak-models/Cargo.toml +++ b/crates/yaak-models/Cargo.toml @@ -11,11 +11,9 @@ hex = { workspace = true } include_dir = "0.7" log = { workspace = true } nanoid = "0.4.0" -r2d2 = "0.8.10" -r2d2_sqlite = { version = "0.25.0" } -rusqlite = { version = "0.32.1", features = ["bundled", "chrono"] } -sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] } -sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] } +rusqlite = { version = "0.38", features = ["bundled", "chrono"] } +sea-query = { version = "1.0", features = ["with-chrono", "attr"] } +sea-query-rusqlite = { version = "0.8.0", features = ["with-chrono"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } schemars = { workspace = true } @@ -23,3 +21,7 @@ sha2 = { workspace = true } thiserror = { workspace = true } ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] } yaak-core = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +r2d2 = "0.8.10" +r2d2_sqlite = { version = "0.32" } diff --git a/crates/yaak-models/src/blob_manager.rs b/crates/yaak-models/src/blob_manager.rs index 871cbf75..78243227 100644 --- a/crates/yaak-models/src/blob_manager.rs +++ b/crates/yaak-models/src/blob_manager.rs @@ -2,9 +2,8 @@ use crate::error::Result; use crate::util::generate_prefixed_id; use include_dir::{Dir, include_dir}; use log::{debug, info}; -use r2d2::Pool; -use r2d2_sqlite::SqliteConnectionManager; use rusqlite::{OptionalExtension, params}; +use yaak_database::{SqliteConn, SqlitePool}; static BLOB_MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/blob_migrations"); @@ -29,11 +28,11 @@ impl BodyChunk { // whole app whenever the pool is exhausted. #[derive(Debug, Clone)] pub struct BlobManager { - pool: Pool, + pool: SqlitePool, } impl BlobManager { - pub fn new(pool: Pool) -> Self { + pub fn new(pool: SqlitePool) -> Self { Self { pool } } @@ -45,7 +44,7 @@ impl BlobManager { /// Context for blob database operations. pub struct BlobContext { - conn: r2d2::PooledConnection, + conn: SqliteConn, } impl BlobContext { @@ -131,7 +130,7 @@ impl BlobContext { } /// Run migrations for the blob database. -pub fn migrate_blob_db(pool: &Pool) -> Result<()> { +pub fn migrate_blob_db(pool: &SqlitePool) -> Result<()> { info!("Running blob database migrations"); // Create migrations tracking table @@ -198,9 +197,9 @@ pub fn migrate_blob_db(pool: &Pool) -> Result<()> { mod tests { use super::*; - fn create_test_pool() -> Pool { - let manager = SqliteConnectionManager::memory(); - let pool = Pool::builder().max_size(1).build(manager).unwrap(); + fn create_test_pool() -> SqlitePool { + let manager = r2d2_sqlite::SqliteConnectionManager::memory(); + let pool = r2d2::Pool::builder().max_size(1).build(manager).unwrap(); migrate_blob_db(&pool).unwrap(); pool } diff --git a/crates/yaak-models/src/error.rs b/crates/yaak-models/src/error.rs index 7726ed8c..78c861f9 100644 --- a/crates/yaak-models/src/error.rs +++ b/crates/yaak-models/src/error.rs @@ -7,7 +7,7 @@ pub enum Error { SqlError(#[from] rusqlite::Error), #[error("SQL Pool error: {0}")] - SqlPoolError(#[from] r2d2::Error), + SqlPoolError(#[from] yaak_database::PoolError), #[error("Database error: {0}")] Database(String), diff --git a/crates/yaak-models/src/lib.rs b/crates/yaak-models/src/lib.rs index a642eaee..b560d2a1 100644 --- a/crates/yaak-models/src/lib.rs +++ b/crates/yaak-models/src/lib.rs @@ -1,15 +1,12 @@ use crate::blob_manager::{BlobManager, migrate_blob_db}; -use crate::error::{Error, Result}; +use crate::error::Result; use crate::migrate::migrate_db; use crate::query_manager::QueryManager; use crate::util::ModelPayload; use log::info; -use r2d2::Pool; -use r2d2_sqlite::SqliteConnectionManager; -use std::fs::create_dir_all; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::mpsc; -use std::time::Duration; +use yaak_database::SqlitePool; pub mod blob_manager; pub mod client_db; @@ -22,17 +19,78 @@ pub mod query_manager; pub mod render; pub mod util; -fn sqlite_file_manager(path: impl Into) -> SqliteConnectionManager { - SqliteConnectionManager::file(path.into()).with_init(|conn| { - conn.pragma_update(None, "journal_mode", "WAL")?; - conn.pragma_update(None, "synchronous", "NORMAL")?; - conn.busy_timeout(Duration::from_millis(5000)) - }) +/// Per-connection setup, applied by every pool on every connection it opens. +fn init_connection(conn: &rusqlite::Connection) -> rusqlite::Result<()> { + conn.busy_timeout(std::time::Duration::from_millis(5000)) } -fn sqlite_memory_manager() -> SqliteConnectionManager { - SqliteConnectionManager::memory() - .with_init(|conn| conn.busy_timeout(Duration::from_millis(5000))) +fn init_file_connection(conn: &rusqlite::Connection) -> rusqlite::Result<()> { + conn.pragma_update(None, "journal_mode", "WAL")?; + conn.pragma_update(None, "synchronous", "NORMAL")?; + init_connection(conn) +} + +/// The two ways a pool comes to exist, one per target. +/// +/// On the desktop and CLI, an r2d2 pool over a file. In a browser, a single +/// connection over whatever VFS the host registered before calling in — the +/// path is a name inside that VFS, not a place on disk. Everything downstream +/// of `SqlitePool` is target-agnostic; this is the only fork. +#[cfg(not(target_arch = "wasm32"))] +mod open { + use super::*; + use crate::error::Error; + use r2d2::Pool; + use r2d2_sqlite::SqliteConnectionManager; + use std::path::PathBuf; + use std::time::Duration; + + pub fn file_pool(path: impl Into, max_size: u32, min_idle: u32) -> Result { + let path: PathBuf = path.into(); + // Create parent directories if needed + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let manager = SqliteConnectionManager::file(path).with_init(|c| init_file_connection(c)); + Pool::builder() + .max_size(max_size) + .min_idle(Some(min_idle)) + .connection_timeout(Duration::from_secs(10)) + .build(manager) + .map_err(|e| Error::Database(e.to_string())) + } + + pub fn memory_pool() -> Result { + let manager = SqliteConnectionManager::memory().with_init(|c| init_connection(c)); + // In-memory DB doesn't support multiple connections + Pool::builder().max_size(1).build(manager).map_err(|e| Error::Database(e.to_string())) + } +} + +#[cfg(target_arch = "wasm32")] +mod open { + use super::*; + use rusqlite::Connection; + use std::path::PathBuf; + + pub fn file_pool( + path: impl Into, + _max_size: u32, + _min_idle: u32, + ) -> Result { + // No WAL: the browser VFSs are single-connection and journal their own + // way; the pragma is accepted and ignored on some and rejected on + // others, so it is not applied at all here. + let conn = Connection::open(path.into())?; + init_connection(&conn)?; + Ok(SqlitePool::single(conn)) + } + + pub fn memory_pool() -> Result { + let conn = Connection::open_in_memory()?; + init_connection(&conn)?; + Ok(SqlitePool::single(conn)) + } } /// Initialize the database managers for standalone (non-Tauri) usage. @@ -46,40 +104,16 @@ pub fn init_standalone( let db_path = db_path.as_ref(); let blob_path = blob_path.as_ref(); - // Create parent directories if needed - if let Some(parent) = db_path.parent() { - create_dir_all(parent)?; - } - if let Some(parent) = blob_path.parent() { - create_dir_all(parent)?; - } - // Main database pool. Sized for concurrent in-flight queries, not concurrent app // features — connections are held per-statement, so even heavy fan-out (e.g. many // gRPC streams) only needs a handful at once. Keep max_size modest: WAL connections // hold ~3 file descriptors each, and macOS GUI apps get a 256 fd soft limit. info!("Initializing app database {db_path:?}"); - let manager = sqlite_file_manager(db_path); - let pool = Pool::builder() - .max_size(20) - .min_idle(Some(2)) - .connection_timeout(Duration::from_secs(10)) - .build(manager) - .map_err(|e| Error::Database(e.to_string()))?; - + let pool = open::file_pool(db_path, 20, 2)?; migrate_db(&pool)?; info!("Initializing blobs database {blob_path:?}"); - - // Blob database pool - let blob_manager = sqlite_file_manager(blob_path); - let blob_pool = Pool::builder() - .max_size(10) - .min_idle(Some(1)) - .connection_timeout(Duration::from_secs(10)) - .build(blob_manager) - .map_err(|e| Error::Database(e.to_string()))?; - + let blob_pool = open::file_pool(blob_path, 10, 1)?; migrate_blob_db(&blob_pool)?; let (tx, rx) = mpsc::channel(); @@ -92,22 +126,10 @@ pub fn init_standalone( /// Initialize the database managers with in-memory SQLite databases. /// Useful for testing and CI environments. pub fn init_in_memory() -> Result<(QueryManager, BlobManager, mpsc::Receiver)> { - // Main database pool - let manager = sqlite_memory_manager(); - let pool = Pool::builder() - .max_size(1) // In-memory DB doesn't support multiple connections - .build(manager) - .map_err(|e| Error::Database(e.to_string()))?; - + let pool = open::memory_pool()?; migrate_db(&pool)?; - // Blob database pool - let blob_manager = sqlite_memory_manager(); - let blob_pool = Pool::builder() - .max_size(1) - .build(blob_manager) - .map_err(|e| Error::Database(e.to_string()))?; - + let blob_pool = open::memory_pool()?; migrate_blob_db(&blob_pool)?; let (tx, rx) = mpsc::channel(); diff --git a/crates/yaak-models/src/migrate.rs b/crates/yaak-models/src/migrate.rs index 96ab801a..607045e0 100644 --- a/crates/yaak-models/src/migrate.rs +++ b/crates/yaak-models/src/migrate.rs @@ -2,14 +2,13 @@ use crate::error::Error::MigrationError; use crate::error::Result; use include_dir::{Dir, DirEntry, include_dir}; use log::{debug, info}; -use r2d2::Pool; -use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::{OptionalExtension, TransactionBehavior, params}; +use rusqlite::{OptionalExtension, Transaction, TransactionBehavior, params}; use sha2::{Digest, Sha384}; +use yaak_database::SqlitePool; static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); -pub fn migrate_db(pool: &Pool) -> Result<()> { +pub fn migrate_db(pool: &SqlitePool) -> Result<()> { info!("Running database migrations"); // Ensure the table exists @@ -43,8 +42,10 @@ pub fn migrate_db(pool: &Pool) -> Result<()> { let mut ran_migrations = 0; for entry in entries { num_migrations += 1; - let mut conn = pool.get()?; - let mut tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; + let conn = pool.get()?; + // `new_unchecked` takes `&Connection`; see yaak_database::pool for why + // the pool never hands out `&mut`. + let mut tx = Transaction::new_unchecked(&conn, TransactionBehavior::Immediate)?; match run_migration(entry, &mut tx) { Ok(ran) => { if ran { @@ -74,7 +75,7 @@ pub fn migrate_db(pool: &Pool) -> Result<()> { } fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> Result { - let start = std::time::Instant::now(); + let start = elapsed_timer(); let (version, description) = split_migration_filename(migration_path.path().to_str().unwrap()) .expect("Failed to parse migration filename"); @@ -97,7 +98,7 @@ fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> R // Split on `;`? → optional depending on how your SQL is structured tx.execute_batch(&sql)?; - let execution_time = start.elapsed().as_nanos() as i64; + let execution_time = start(); let checksum = sha384_hex_prefixed(sql.as_bytes()); // NOTE: The success column is never used. It's just there for sqlx compatibility. @@ -109,6 +110,21 @@ fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> R Ok(true) } +/// Nanoseconds since the timer was started, for the sqlx-compatible +/// `execution_time` column. `Instant` does not exist on `wasm32-unknown-unknown` +/// (there is no monotonic clock to ask), and the column is bookkeeping, so +/// there it reads as zero rather than taking the migrator down with it. +#[cfg(not(target_arch = "wasm32"))] +fn elapsed_timer() -> impl Fn() -> i64 { + let start = std::time::Instant::now(); + move || start.elapsed().as_nanos() as i64 +} + +#[cfg(target_arch = "wasm32")] +fn elapsed_timer() -> impl Fn() -> i64 { + || 0 +} + fn split_migration_filename(filename: &str) -> Option<(String, String)> { // Remove the .sql extension let trimmed = filename.strip_suffix(".sql")?; diff --git a/crates/yaak-models/src/queries/graphql_introspections.rs b/crates/yaak-models/src/queries/graphql_introspections.rs index 2edbd2e3..8ece36de 100644 --- a/crates/yaak-models/src/queries/graphql_introspections.rs +++ b/crates/yaak-models/src/queries/graphql_introspections.rs @@ -3,6 +3,7 @@ use crate::error::Result; use crate::models::{GraphQlIntrospection, GraphQlIntrospectionIden}; use crate::util::UpdateSource; use chrono::{Duration, Utc}; +use sea_query::ExprTrait; use sea_query::{Expr, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; diff --git a/crates/yaak-models/src/queries/grpc_connections.rs b/crates/yaak-models/src/queries/grpc_connections.rs index 32212092..7de34ee2 100644 --- a/crates/yaak-models/src/queries/grpc_connections.rs +++ b/crates/yaak-models/src/queries/grpc_connections.rs @@ -4,6 +4,7 @@ use crate::models::{GrpcConnection, GrpcConnectionIden, GrpcConnectionState}; use crate::queries::MAX_HISTORY_ITEMS; use crate::util::UpdateSource; use log::debug; +use sea_query::ExprTrait; use sea_query::{Expr, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; diff --git a/crates/yaak-models/src/queries/http_responses.rs b/crates/yaak-models/src/queries/http_responses.rs index a1b2010c..6713cfee 100644 --- a/crates/yaak-models/src/queries/http_responses.rs +++ b/crates/yaak-models/src/queries/http_responses.rs @@ -5,6 +5,7 @@ use crate::models::{HttpResponse, HttpResponseIden, HttpResponseState}; use crate::queries::MAX_HISTORY_ITEMS; use crate::util::UpdateSource; use log::{debug, error}; +use sea_query::ExprTrait; use sea_query::{Expr, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; use std::fs; diff --git a/crates/yaak-models/src/queries/key_values.rs b/crates/yaak-models/src/queries/key_values.rs index 56da7513..1a528eeb 100644 --- a/crates/yaak-models/src/queries/key_values.rs +++ b/crates/yaak-models/src/queries/key_values.rs @@ -4,6 +4,7 @@ use crate::models::{KeyValue, KeyValueIden, UpsertModelInfo}; use crate::util::UpdateSource; use chrono::NaiveDateTime; use log::error; +use sea_query::ExprTrait; use sea_query::{Asterisk, Cond, Expr, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; diff --git a/crates/yaak-models/src/queries/plugin_key_values.rs b/crates/yaak-models/src/queries/plugin_key_values.rs index e01e471c..979fb11c 100644 --- a/crates/yaak-models/src/queries/plugin_key_values.rs +++ b/crates/yaak-models/src/queries/plugin_key_values.rs @@ -1,6 +1,7 @@ use crate::client_db::ClientDb; use crate::error::Result; use crate::models::{PluginKeyValue, PluginKeyValueIden}; +use sea_query::ExprTrait; use sea_query::Keyword::CurrentTimestamp; use sea_query::{Asterisk, Cond, Expr, OnConflict, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; diff --git a/crates/yaak-models/src/queries/sync_states.rs b/crates/yaak-models/src/queries/sync_states.rs index 5d112a29..b3053912 100644 --- a/crates/yaak-models/src/queries/sync_states.rs +++ b/crates/yaak-models/src/queries/sync_states.rs @@ -2,6 +2,7 @@ use crate::client_db::ClientDb; use crate::error::Result; use crate::models::{SyncState, SyncStateIden, UpsertModelInfo}; use crate::util::UpdateSource; +use sea_query::ExprTrait; use sea_query::{Asterisk, Cond, Expr, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; use std::path::Path; diff --git a/crates/yaak-models/src/queries/websocket_connections.rs b/crates/yaak-models/src/queries/websocket_connections.rs index eb6f45c6..d1dbd37a 100644 --- a/crates/yaak-models/src/queries/websocket_connections.rs +++ b/crates/yaak-models/src/queries/websocket_connections.rs @@ -4,6 +4,7 @@ use crate::models::{WebsocketConnection, WebsocketConnectionIden, WebsocketConne use crate::queries::MAX_HISTORY_ITEMS; use crate::util::UpdateSource; use log::debug; +use sea_query::ExprTrait; use sea_query::{Expr, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; diff --git a/crates/yaak-models/src/query_manager.rs b/crates/yaak-models/src/query_manager.rs index b570f153..8b2c86b8 100644 --- a/crates/yaak-models/src/query_manager.rs +++ b/crates/yaak-models/src/query_manager.rs @@ -1,23 +1,21 @@ use crate::client_db::ClientDb; use crate::error::Error::GenericError; use crate::util::ModelPayload; -use r2d2::Pool; -use r2d2_sqlite::SqliteConnectionManager; -use rusqlite::TransactionBehavior; +use rusqlite::{Transaction, TransactionBehavior}; use std::sync::mpsc; -use yaak_database::{ConnectionOrTx, DbContext}; +use yaak_database::{ConnectionOrTx, DbContext, SqlitePool}; // Pool is internally synchronized — don't wrap it in a Mutex. A Mutex held across the // blocking `get()` serializes every DB access behind the slowest waiter, freezing the // whole app whenever the pool is exhausted. #[derive(Debug, Clone)] pub struct QueryManager { - pool: Pool, + pool: SqlitePool, events_tx: mpsc::Sender, } impl QueryManager { - pub fn new(pool: Pool, events_tx: mpsc::Sender) -> Self { + pub fn new(pool: SqlitePool, events_tx: mpsc::Sender) -> Self { QueryManager { pool, events_tx } } @@ -46,9 +44,10 @@ impl QueryManager { where E: From, { - let mut conn = self.pool.get().expect("Failed to get new DB connection from the pool"); - let tx = conn - .transaction_with_behavior(TransactionBehavior::Immediate) + let conn = self.pool.get().expect("Failed to get new DB connection from the pool"); + // `new_unchecked` takes `&Connection`; see yaak_database::pool for why + // the pool never hands out `&mut`. + let tx = Transaction::new_unchecked(&conn, TransactionBehavior::Immediate) .expect("Failed to start DB transaction"); let ctx = DbContext::new(ConnectionOrTx::Transaction(&tx));