Let yaak-models compile for wasm32-unknown-unknown (#556)

This commit is contained in:
Gregory Schier
2026-08-15 14:55:54 -07:00
committed by GitHub
parent cdbbef34f8
commit 32e92d484b
23 changed files with 296 additions and 153 deletions
Generated
+56 -48
View File
@@ -40,18 +40,6 @@ dependencies = [
"version_check", "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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.6.10" version = "0.6.10"
@@ -1066,10 +1054,11 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.26" version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
dependencies = [ dependencies = [
"find-msvc-tools",
"jobserver", "jobserver",
"libc", "libc",
"shlex", "shlex",
@@ -2417,6 +2406,12 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "fixedbitset" name = "fixedbitset"
version = "0.4.2" version = "0.4.2"
@@ -2849,8 +2844,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -3091,7 +3088,7 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
dependencies = [ dependencies = [
"ahash 0.7.8", "ahash",
] ]
[[package]] [[package]]
@@ -3099,9 +3096,6 @@ name = "hashbrown"
version = "0.14.5" version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash 0.8.12",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
@@ -3131,11 +3125,11 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]] [[package]]
name = "hashlink" name = "hashlink"
version = "0.9.1" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f"
dependencies = [ dependencies = [
"hashbrown 0.14.5", "hashbrown 0.16.1",
] ]
[[package]] [[package]]
@@ -3561,17 +3555,6 @@ dependencies = [
"cfb", "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]] [[package]]
name = "inotify" name = "inotify"
version = "0.11.0" version = "0.11.0"
@@ -4007,9 +3990,9 @@ dependencies = [
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
version = "0.30.1" version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a"
dependencies = [ dependencies = [
"cc", "cc",
"pkg-config", "pkg-config",
@@ -6242,9 +6225,9 @@ dependencies = [
[[package]] [[package]]
name = "r2d2_sqlite" name = "r2d2_sqlite"
version = "0.25.0" version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb14dba8247a6a15b7fdbc7d389e2e6f03ee9f184f87117706d509c092dfe846" checksum = "a2ebd03c29250cdf191da93a35118b4567c2ef0eacab54f65e058d6f4c9965f6"
dependencies = [ dependencies = [
"r2d2", "r2d2",
"rusqlite", "rusqlite",
@@ -7130,10 +7113,20 @@ dependencies = [
] ]
[[package]] [[package]]
name = "rusqlite" name = "rsqlite-vfs"
version = "0.32.1" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"chrono", "chrono",
@@ -7142,6 +7135,7 @@ dependencies = [
"hashlink", "hashlink",
"libsqlite3-sys", "libsqlite3-sys",
"smallvec", "smallvec",
"sqlite-wasm-rs",
] ]
[[package]] [[package]]
@@ -7423,20 +7417,20 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "sea-query" name = "sea-query"
version = "0.32.6" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64c91783d1514b99754fc6a4079081dcc2c587dadbff65c48c7f62297443536a" checksum = "546040c653a705e60ec65ecd3191a809603734bebbc225775916dea9ae409b31"
dependencies = [ dependencies = [
"chrono", "chrono",
"inherent", "itoa",
"sea-query-derive", "sea-query-derive",
] ]
[[package]] [[package]]
name = "sea-query-derive" name = "sea-query-derive"
version = "0.4.3" version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bae0cbad6ab996955664982739354128c58d16e126114fe88c2a493642502aab" checksum = "a0b0f466921cdd3cf4b89d5c3ac2173dba89a873ab395b123a645de181ec7537"
dependencies = [ dependencies = [
"darling 0.20.11", "darling 0.20.11",
"heck 0.4.1", "heck 0.4.1",
@@ -7448,9 +7442,9 @@ dependencies = [
[[package]] [[package]]
name = "sea-query-rusqlite" name = "sea-query-rusqlite"
version = "0.7.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3743bbdfb24b1a84cc1a6fbf4b1188e6851f6e00ea20944b44c56bf03a585bb4" checksum = "1ec6038023c8517c623e5bf9606b3c54d40bc8296bb6b2986040428dd84deddd"
dependencies = [ dependencies = [
"rusqlite", "rusqlite",
"sea-query", "sea-query",
@@ -7861,9 +7855,9 @@ dependencies = [
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
@@ -8031,6 +8025,18 @@ dependencies = [
"system-deps", "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]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.0" version = "1.2.0"
@@ -11136,6 +11142,7 @@ name = "yaak-database"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"getrandom 0.2.16",
"include_dir", "include_dir",
"log 0.4.29", "log 0.4.29",
"nanoid", "nanoid",
@@ -11148,6 +11155,7 @@ dependencies = [
"serde_json", "serde_json",
"thiserror 2.0.17", "thiserror 2.0.17",
"ts-rs", "ts-rs",
"uuid",
] ]
[[package]] [[package]]
+3 -3
View File
@@ -10,9 +10,9 @@ chrono = { workspace = true, features = ["serde"] }
log = { workspace = true } log = { workspace = true }
include_dir = "0.7" include_dir = "0.7"
r2d2 = "0.8.10" r2d2 = "0.8.10"
r2d2_sqlite = "0.25.0" r2d2_sqlite = "0.32"
rusqlite = { version = "0.32.1", features = ["bundled", "chrono"] } rusqlite = { version = "0.38", features = ["bundled", "chrono"] }
sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] } sea-query = { version = "1.0", features = ["with-chrono", "attr"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
ts-rs = { workspace = true, features = ["chrono-impl"] } ts-rs = { workspace = true, features = ["chrono-impl"] }
+1 -1
View File
@@ -39,7 +39,7 @@ md5 = "0.8.0"
notify = "8.0.0" notify = "8.0.0"
pretty_graphql = "0.2" pretty_graphql = "0.2"
r2d2 = "0.8.10" r2d2 = "0.8.10"
r2d2_sqlite = "0.25.0" r2d2_sqlite = "0.32"
mime_guess = "2.0.5" mime_guess = "2.0.5"
rand = "0.9.0" rand = "0.9.0"
reqwest = { workspace = true, features = [ reqwest = { workspace = true, features = [
+13 -5
View File
@@ -9,12 +9,20 @@ chrono = { version = "0.4.38", features = ["serde"] }
include_dir = "0.7" include_dir = "0.7"
log = { workspace = true } log = { workspace = true }
nanoid = "0.4.0" nanoid = "0.4.0"
r2d2 = "0.8.10" rusqlite = { version = "0.38", features = ["bundled", "chrono"] }
r2d2_sqlite = { version = "0.25.0" } sea-query-rusqlite = { version = "0.8.0", features = ["with-chrono"] }
rusqlite = { version = "0.32.1", features = ["bundled", "chrono"] } sea-query = { version = "1.0", features = ["with-chrono", "attr"] }
sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] }
sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
ts-rs = { 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"] }
@@ -1,9 +1,8 @@
use r2d2::PooledConnection; use crate::pool::SqliteConn;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{Connection, Statement, ToSql, Transaction}; use rusqlite::{Connection, Statement, ToSql, Transaction};
pub enum ConnectionOrTx<'a> { pub enum ConnectionOrTx<'a> {
Connection(PooledConnection<SqliteConnectionManager>), Connection(SqliteConn),
Transaction(&'a Transaction<'a>), Transaction(&'a Transaction<'a>),
} }
@@ -3,6 +3,7 @@ use crate::error::Error::ModelNotFound;
use crate::error::Result; use crate::error::Result;
use crate::traits::UpsertModelInfo; use crate::traits::UpsertModelInfo;
use crate::update_source::UpdateSource; use crate::update_source::UpdateSource;
use sea_query::ExprTrait;
use sea_query::{ use sea_query::{
Asterisk, Expr, Func, IntoColumnRef, IntoIden, OnConflict, Query, SimpleExpr, Asterisk, Expr, Func, IntoColumnRef, IntoIden, OnConflict, Query, SimpleExpr,
SqliteQueryBuilder, SqliteQueryBuilder,
+1 -1
View File
@@ -7,7 +7,7 @@ pub enum Error {
SqlError(#[from] rusqlite::Error), SqlError(#[from] rusqlite::Error),
#[error("SQL Pool error: {0}")] #[error("SQL Pool error: {0}")]
SqlPoolError(#[from] r2d2::Error), SqlPoolError(#[from] crate::pool::PoolError),
#[error("Database error: {0}")] #[error("Database error: {0}")]
Database(String), Database(String),
+5 -2
View File
@@ -2,6 +2,7 @@ pub mod connection_or_tx;
pub mod db_context; pub mod db_context;
pub mod error; pub mod error;
pub mod migrate; pub mod migrate;
pub mod pool;
pub mod traits; pub mod traits;
pub mod update_source; pub mod update_source;
pub mod util; pub mod util;
@@ -11,13 +12,15 @@ pub use connection_or_tx::ConnectionOrTx;
pub use db_context::DbContext; pub use db_context::DbContext;
pub use error::{Error, Result}; pub use error::{Error, Result};
pub use migrate::run_migrations; pub use migrate::run_migrations;
pub use pool::{PoolError, SqliteConn, SqlitePool};
pub use traits::{UpsertModelInfo, upsert_date}; pub use traits::{UpsertModelInfo, upsert_date};
pub use update_source::{ModelChangeEvent, UpdateSource}; pub use update_source::{ModelChangeEvent, UpdateSource};
pub use util::{generate_id, generate_id_of_length, generate_prefixed_id}; 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; pub use r2d2;
#[cfg(not(target_arch = "wasm32"))]
pub use r2d2_sqlite; pub use r2d2_sqlite;
pub use rusqlite; pub use rusqlite;
pub use sea_query; pub use sea_query;
pub use sea_query_rusqlite;
+2 -3
View File
@@ -1,8 +1,7 @@
use crate::error::Result; use crate::error::Result;
use crate::pool::SqlitePool;
use include_dir::Dir; use include_dir::Dir;
use log::{debug, info}; use log::{debug, info};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{OptionalExtension, params}; use rusqlite::{OptionalExtension, params};
const TRACKING_TABLE: &str = "_sqlx_migrations"; 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`). /// Migrations are sorted by filename (use timestamp prefixes like `00000001_init.sql`).
/// Applied migrations are tracked in `_sqlx_migrations`. /// Applied migrations are tracked in `_sqlx_migrations`.
pub fn run_migrations(pool: &Pool<SqliteConnectionManager>, dir: &Dir<'_>) -> Result<()> { pub fn run_migrations(pool: &SqlitePool, dir: &Dir<'_>) -> Result<()> {
info!("Running migrations"); info!("Running migrations");
// Create tracking table // Create tracking table
+80
View File
@@ -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<SqliteConnectionManager>;
pub type SqliteConn = r2d2::PooledConnection<SqliteConnectionManager>;
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<Connection>,
}
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<SqliteConn, PoolError> {
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<Connection>);
impl Deref for SqliteConn {
type Target = Connection;
fn deref(&self) -> &Connection {
&self.0
}
}
}
pub use imp::*;
+7 -5
View File
@@ -11,11 +11,9 @@ hex = { workspace = true }
include_dir = "0.7" include_dir = "0.7"
log = { workspace = true } log = { workspace = true }
nanoid = "0.4.0" nanoid = "0.4.0"
r2d2 = "0.8.10" rusqlite = { version = "0.38", features = ["bundled", "chrono"] }
r2d2_sqlite = { version = "0.25.0" } sea-query = { version = "1.0", features = ["with-chrono", "attr"] }
rusqlite = { version = "0.32.1", features = ["bundled", "chrono"] } sea-query-rusqlite = { version = "0.8.0", features = ["with-chrono"] }
sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] }
sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
schemars = { workspace = true } schemars = { workspace = true }
@@ -23,3 +21,7 @@ sha2 = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] } ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
yaak-core = { workspace = true } yaak-core = { workspace = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
r2d2 = "0.8.10"
r2d2_sqlite = { version = "0.32" }
+8 -9
View File
@@ -2,9 +2,8 @@ use crate::error::Result;
use crate::util::generate_prefixed_id; use crate::util::generate_prefixed_id;
use include_dir::{Dir, include_dir}; use include_dir::{Dir, include_dir};
use log::{debug, info}; use log::{debug, info};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{OptionalExtension, params}; use rusqlite::{OptionalExtension, params};
use yaak_database::{SqliteConn, SqlitePool};
static BLOB_MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/blob_migrations"); 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. // whole app whenever the pool is exhausted.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BlobManager { pub struct BlobManager {
pool: Pool<SqliteConnectionManager>, pool: SqlitePool,
} }
impl BlobManager { impl BlobManager {
pub fn new(pool: Pool<SqliteConnectionManager>) -> Self { pub fn new(pool: SqlitePool) -> Self {
Self { pool } Self { pool }
} }
@@ -45,7 +44,7 @@ impl BlobManager {
/// Context for blob database operations. /// Context for blob database operations.
pub struct BlobContext { pub struct BlobContext {
conn: r2d2::PooledConnection<SqliteConnectionManager>, conn: SqliteConn,
} }
impl BlobContext { impl BlobContext {
@@ -131,7 +130,7 @@ impl BlobContext {
} }
/// Run migrations for the blob database. /// Run migrations for the blob database.
pub fn migrate_blob_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> { pub fn migrate_blob_db(pool: &SqlitePool) -> Result<()> {
info!("Running blob database migrations"); info!("Running blob database migrations");
// Create migrations tracking table // Create migrations tracking table
@@ -198,9 +197,9 @@ pub fn migrate_blob_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> {
mod tests { mod tests {
use super::*; use super::*;
fn create_test_pool() -> Pool<SqliteConnectionManager> { fn create_test_pool() -> SqlitePool {
let manager = SqliteConnectionManager::memory(); let manager = r2d2_sqlite::SqliteConnectionManager::memory();
let pool = Pool::builder().max_size(1).build(manager).unwrap(); let pool = r2d2::Pool::builder().max_size(1).build(manager).unwrap();
migrate_blob_db(&pool).unwrap(); migrate_blob_db(&pool).unwrap();
pool pool
} }
+1 -1
View File
@@ -7,7 +7,7 @@ pub enum Error {
SqlError(#[from] rusqlite::Error), SqlError(#[from] rusqlite::Error),
#[error("SQL Pool error: {0}")] #[error("SQL Pool error: {0}")]
SqlPoolError(#[from] r2d2::Error), SqlPoolError(#[from] yaak_database::PoolError),
#[error("Database error: {0}")] #[error("Database error: {0}")]
Database(String), Database(String),
+77 -55
View File
@@ -1,15 +1,12 @@
use crate::blob_manager::{BlobManager, migrate_blob_db}; use crate::blob_manager::{BlobManager, migrate_blob_db};
use crate::error::{Error, Result}; use crate::error::Result;
use crate::migrate::migrate_db; use crate::migrate::migrate_db;
use crate::query_manager::QueryManager; use crate::query_manager::QueryManager;
use crate::util::ModelPayload; use crate::util::ModelPayload;
use log::info; use log::info;
use r2d2::Pool; use std::path::Path;
use r2d2_sqlite::SqliteConnectionManager;
use std::fs::create_dir_all;
use std::path::{Path, PathBuf};
use std::sync::mpsc; use std::sync::mpsc;
use std::time::Duration; use yaak_database::SqlitePool;
pub mod blob_manager; pub mod blob_manager;
pub mod client_db; pub mod client_db;
@@ -22,17 +19,78 @@ pub mod query_manager;
pub mod render; pub mod render;
pub mod util; pub mod util;
fn sqlite_file_manager(path: impl Into<PathBuf>) -> SqliteConnectionManager { /// Per-connection setup, applied by every pool on every connection it opens.
SqliteConnectionManager::file(path.into()).with_init(|conn| { fn init_connection(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
conn.pragma_update(None, "journal_mode", "WAL")?; conn.busy_timeout(std::time::Duration::from_millis(5000))
conn.pragma_update(None, "synchronous", "NORMAL")?;
conn.busy_timeout(Duration::from_millis(5000))
})
} }
fn sqlite_memory_manager() -> SqliteConnectionManager { fn init_file_connection(conn: &rusqlite::Connection) -> rusqlite::Result<()> {
SqliteConnectionManager::memory() conn.pragma_update(None, "journal_mode", "WAL")?;
.with_init(|conn| conn.busy_timeout(Duration::from_millis(5000))) 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<PathBuf>, max_size: u32, min_idle: u32) -> Result<SqlitePool> {
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<SqlitePool> {
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<PathBuf>,
_max_size: u32,
_min_idle: u32,
) -> Result<SqlitePool> {
// 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<SqlitePool> {
let conn = Connection::open_in_memory()?;
init_connection(&conn)?;
Ok(SqlitePool::single(conn))
}
} }
/// Initialize the database managers for standalone (non-Tauri) usage. /// 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 db_path = db_path.as_ref();
let blob_path = blob_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 // 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 // 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 // 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. // hold ~3 file descriptors each, and macOS GUI apps get a 256 fd soft limit.
info!("Initializing app database {db_path:?}"); info!("Initializing app database {db_path:?}");
let manager = sqlite_file_manager(db_path); let pool = open::file_pool(db_path, 20, 2)?;
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()))?;
migrate_db(&pool)?; migrate_db(&pool)?;
info!("Initializing blobs database {blob_path:?}"); info!("Initializing blobs database {blob_path:?}");
let blob_pool = open::file_pool(blob_path, 10, 1)?;
// 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()))?;
migrate_blob_db(&blob_pool)?; migrate_blob_db(&blob_pool)?;
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
@@ -92,22 +126,10 @@ pub fn init_standalone(
/// Initialize the database managers with in-memory SQLite databases. /// Initialize the database managers with in-memory SQLite databases.
/// Useful for testing and CI environments. /// Useful for testing and CI environments.
pub fn init_in_memory() -> Result<(QueryManager, BlobManager, mpsc::Receiver<ModelPayload>)> { pub fn init_in_memory() -> Result<(QueryManager, BlobManager, mpsc::Receiver<ModelPayload>)> {
// Main database pool let pool = open::memory_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()))?;
migrate_db(&pool)?; migrate_db(&pool)?;
// Blob database pool let blob_pool = open::memory_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()))?;
migrate_blob_db(&blob_pool)?; migrate_blob_db(&blob_pool)?;
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
+24 -8
View File
@@ -2,14 +2,13 @@ use crate::error::Error::MigrationError;
use crate::error::Result; use crate::error::Result;
use include_dir::{Dir, DirEntry, include_dir}; use include_dir::{Dir, DirEntry, include_dir};
use log::{debug, info}; use log::{debug, info};
use r2d2::Pool; use rusqlite::{OptionalExtension, Transaction, TransactionBehavior, params};
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::{OptionalExtension, TransactionBehavior, params};
use sha2::{Digest, Sha384}; use sha2::{Digest, Sha384};
use yaak_database::SqlitePool;
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations"); static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/migrations");
pub fn migrate_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> { pub fn migrate_db(pool: &SqlitePool) -> Result<()> {
info!("Running database migrations"); info!("Running database migrations");
// Ensure the table exists // Ensure the table exists
@@ -43,8 +42,10 @@ pub fn migrate_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> {
let mut ran_migrations = 0; let mut ran_migrations = 0;
for entry in entries { for entry in entries {
num_migrations += 1; num_migrations += 1;
let mut conn = pool.get()?; let conn = pool.get()?;
let mut tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?; // `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) { match run_migration(entry, &mut tx) {
Ok(ran) => { Ok(ran) => {
if ran { if ran {
@@ -74,7 +75,7 @@ pub fn migrate_db(pool: &Pool<SqliteConnectionManager>) -> Result<()> {
} }
fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> Result<bool> { fn run_migration(migration_path: &DirEntry, tx: &mut rusqlite::Transaction) -> Result<bool> {
let start = std::time::Instant::now(); let start = elapsed_timer();
let (version, description) = split_migration_filename(migration_path.path().to_str().unwrap()) let (version, description) = split_migration_filename(migration_path.path().to_str().unwrap())
.expect("Failed to parse migration filename"); .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 // Split on `;`? → optional depending on how your SQL is structured
tx.execute_batch(&sql)?; 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()); let checksum = sha384_hex_prefixed(sql.as_bytes());
// NOTE: The success column is never used. It's just there for sqlx compatibility. // 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) 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)> { fn split_migration_filename(filename: &str) -> Option<(String, String)> {
// Remove the .sql extension // Remove the .sql extension
let trimmed = filename.strip_suffix(".sql")?; let trimmed = filename.strip_suffix(".sql")?;
@@ -3,6 +3,7 @@ use crate::error::Result;
use crate::models::{GraphQlIntrospection, GraphQlIntrospectionIden}; use crate::models::{GraphQlIntrospection, GraphQlIntrospectionIden};
use crate::util::UpdateSource; use crate::util::UpdateSource;
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use sea_query::ExprTrait;
use sea_query::{Expr, Query, SqliteQueryBuilder}; use sea_query::{Expr, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder; use sea_query_rusqlite::RusqliteBinder;
@@ -4,6 +4,7 @@ use crate::models::{GrpcConnection, GrpcConnectionIden, GrpcConnectionState};
use crate::queries::MAX_HISTORY_ITEMS; use crate::queries::MAX_HISTORY_ITEMS;
use crate::util::UpdateSource; use crate::util::UpdateSource;
use log::debug; use log::debug;
use sea_query::ExprTrait;
use sea_query::{Expr, Query, SqliteQueryBuilder}; use sea_query::{Expr, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder; use sea_query_rusqlite::RusqliteBinder;
@@ -5,6 +5,7 @@ use crate::models::{HttpResponse, HttpResponseIden, HttpResponseState};
use crate::queries::MAX_HISTORY_ITEMS; use crate::queries::MAX_HISTORY_ITEMS;
use crate::util::UpdateSource; use crate::util::UpdateSource;
use log::{debug, error}; use log::{debug, error};
use sea_query::ExprTrait;
use sea_query::{Expr, Query, SqliteQueryBuilder}; use sea_query::{Expr, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder; use sea_query_rusqlite::RusqliteBinder;
use std::fs; use std::fs;
@@ -4,6 +4,7 @@ use crate::models::{KeyValue, KeyValueIden, UpsertModelInfo};
use crate::util::UpdateSource; use crate::util::UpdateSource;
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use log::error; use log::error;
use sea_query::ExprTrait;
use sea_query::{Asterisk, Cond, Expr, Query, SqliteQueryBuilder}; use sea_query::{Asterisk, Cond, Expr, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder; use sea_query_rusqlite::RusqliteBinder;
@@ -1,6 +1,7 @@
use crate::client_db::ClientDb; use crate::client_db::ClientDb;
use crate::error::Result; use crate::error::Result;
use crate::models::{PluginKeyValue, PluginKeyValueIden}; use crate::models::{PluginKeyValue, PluginKeyValueIden};
use sea_query::ExprTrait;
use sea_query::Keyword::CurrentTimestamp; use sea_query::Keyword::CurrentTimestamp;
use sea_query::{Asterisk, Cond, Expr, OnConflict, Query, SqliteQueryBuilder}; use sea_query::{Asterisk, Cond, Expr, OnConflict, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder; use sea_query_rusqlite::RusqliteBinder;
@@ -2,6 +2,7 @@ use crate::client_db::ClientDb;
use crate::error::Result; use crate::error::Result;
use crate::models::{SyncState, SyncStateIden, UpsertModelInfo}; use crate::models::{SyncState, SyncStateIden, UpsertModelInfo};
use crate::util::UpdateSource; use crate::util::UpdateSource;
use sea_query::ExprTrait;
use sea_query::{Asterisk, Cond, Expr, Query, SqliteQueryBuilder}; use sea_query::{Asterisk, Cond, Expr, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder; use sea_query_rusqlite::RusqliteBinder;
use std::path::Path; use std::path::Path;
@@ -4,6 +4,7 @@ use crate::models::{WebsocketConnection, WebsocketConnectionIden, WebsocketConne
use crate::queries::MAX_HISTORY_ITEMS; use crate::queries::MAX_HISTORY_ITEMS;
use crate::util::UpdateSource; use crate::util::UpdateSource;
use log::debug; use log::debug;
use sea_query::ExprTrait;
use sea_query::{Expr, Query, SqliteQueryBuilder}; use sea_query::{Expr, Query, SqliteQueryBuilder};
use sea_query_rusqlite::RusqliteBinder; use sea_query_rusqlite::RusqliteBinder;
+8 -9
View File
@@ -1,23 +1,21 @@
use crate::client_db::ClientDb; use crate::client_db::ClientDb;
use crate::error::Error::GenericError; use crate::error::Error::GenericError;
use crate::util::ModelPayload; use crate::util::ModelPayload;
use r2d2::Pool; use rusqlite::{Transaction, TransactionBehavior};
use r2d2_sqlite::SqliteConnectionManager;
use rusqlite::TransactionBehavior;
use std::sync::mpsc; 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 // 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 // blocking `get()` serializes every DB access behind the slowest waiter, freezing the
// whole app whenever the pool is exhausted. // whole app whenever the pool is exhausted.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct QueryManager { pub struct QueryManager {
pool: Pool<SqliteConnectionManager>, pool: SqlitePool,
events_tx: mpsc::Sender<ModelPayload>, events_tx: mpsc::Sender<ModelPayload>,
} }
impl QueryManager { impl QueryManager {
pub fn new(pool: Pool<SqliteConnectionManager>, events_tx: mpsc::Sender<ModelPayload>) -> Self { pub fn new(pool: SqlitePool, events_tx: mpsc::Sender<ModelPayload>) -> Self {
QueryManager { pool, events_tx } QueryManager { pool, events_tx }
} }
@@ -46,9 +44,10 @@ impl QueryManager {
where where
E: From<crate::error::Error>, E: From<crate::error::Error>,
{ {
let mut conn = self.pool.get().expect("Failed to get new DB connection from the pool"); let conn = self.pool.get().expect("Failed to get new DB connection from the pool");
let tx = conn // `new_unchecked` takes `&Connection`; see yaak_database::pool for why
.transaction_with_behavior(TransactionBehavior::Immediate) // the pool never hands out `&mut`.
let tx = Transaction::new_unchecked(&conn, TransactionBehavior::Immediate)
.expect("Failed to start DB transaction"); .expect("Failed to start DB transaction");
let ctx = DbContext::new(ConnectionOrTx::Transaction(&tx)); let ctx = DbContext::new(ConnectionOrTx::Transaction(&tx));