lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit c625578e7d06620ae4d5d63997f7cf66564972f1
parent 4e4c6790310fb1901d48b13df3d7f212a5096d6c
Author: triesap <tyson@radroots.org>
Date:   Fri, 20 Mar 2026 15:12:08 +0000

sql: upgrade rusqlite and restore workspace green

Diffstat:
MCargo.lock | 43++++++++++++++++++++++++++++---------------
MCargo.toml | 2+-
Mcrates/nostr-ndb/src/ndb.rs | 74++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mcrates/replica-sync/src/emit.rs | 45+++++++++++++++++++++++----------------------
Mcrates/replica-sync/tests/ingest_roundtrip.rs | 14+++++---------
Mcrates/sql-core/src/executor_sqlite.rs | 10++++++++++
Mcrates/sql-core/tests/coverage.rs | 29+++++++++++++++++++++++++++++
7 files changed, 150 insertions(+), 67 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1100,15 +1100,6 @@ dependencies = [ ] [[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1579,9 +1570,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.28.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" dependencies = [ "cc", "pkg-config", @@ -2642,17 +2633,27 @@ dependencies = [ ] [[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] name = "rusqlite" -version = "0.31.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" dependencies = [ "bitflags 2.11.0", "fallible-iterator", "fallible-streaming-iterator", - "hashlink 0.9.1", "libsqlite3-sys", "smallvec", + "sqlite-wasm-rs", ] [[package]] @@ -3055,6 +3056,18 @@ dependencies = [ ] [[package]] +name = "sqlite-wasm-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4206ed3a67690b9c29b77d728f6acc3ce78f16bf846d83c94f76400320181b" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] name = "stable_deref_trait" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4432,7 +4445,7 @@ checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" dependencies = [ "arraydeque", "encoding_rs", - "hashlink 0.8.4", + "hashlink", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml @@ -107,4 +107,4 @@ uniffi_build = { version = "=0.29.4" } wasm-bindgen = { version = "0.2" } wasm-bindgen-futures = { version = "0.4" } wasm-bindgen-test = { version = "0.3" } -rusqlite = { version = "0.31", default-features = false } +rusqlite = { version = "0.39", default-features = false } diff --git a/crates/nostr-ndb/src/ndb.rs b/crates/nostr-ndb/src/ndb.rs @@ -359,19 +359,25 @@ mod tests { use std::time::Duration; use tempfile::TempDir; - fn query_notes_lock() -> &'static Mutex<()> { - static QUERY_NOTES_LOCK: OnceLock<Mutex<()>> = OnceLock::new(); - QUERY_NOTES_LOCK.get_or_init(|| Mutex::new(())) + fn test_hooks_lock() -> &'static Mutex<()> { + static TEST_HOOKS_LOCK: OnceLock<Mutex<()>> = OnceLock::new(); + TEST_HOOKS_LOCK.get_or_init(|| Mutex::new(())) } - fn query_notes_guard() -> std::sync::MutexGuard<'static, ()> { - query_notes_lock().lock().expect("query notes lock") + fn test_hooks_guard() -> std::sync::MutexGuard<'static, ()> { + test_hooks_lock().lock().expect("test hooks lock") } - fn reset_query_flags() { + fn reset_test_flags() { + test_hooks::FORCE_EVENT_JSON_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_PROCESS_EVENT_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_SUBSCRIBE_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_UNSUBSCRIBE_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_WAIT_ERROR.store(false, Ordering::SeqCst); test_hooks::FORCE_TRANSACTION_ERROR.store(false, Ordering::SeqCst); test_hooks::FORCE_QUERY_ERROR.store(false, Ordering::SeqCst); test_hooks::FORCE_NOTE_JSON_ERROR.store(false, Ordering::SeqCst); + test_hooks::FORCE_PROFILE_QUERY_ERROR.store(false, Ordering::SeqCst); } #[test] @@ -487,6 +493,8 @@ mod tests { #[test] fn ingest_event_json_rejects_invalid_json() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -501,6 +509,8 @@ mod tests { #[test] fn ingest_event_reports_event_json_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -520,6 +530,8 @@ mod tests { #[test] fn subscribe_poll_and_unsubscribe_round_trip() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -553,6 +565,8 @@ mod tests { #[test] fn subscribe_reports_ndb_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -566,6 +580,8 @@ mod tests { #[test] fn unsubscribe_reports_ndb_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -582,6 +598,8 @@ mod tests { #[tokio::test] async fn wait_for_note_keys_yields_results() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -605,6 +623,8 @@ mod tests { #[tokio::test] async fn wait_for_note_keys_reports_ndb_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -622,8 +642,8 @@ mod tests { #[test] fn query_notes_returns_ingested_results() { - let _guard = query_notes_guard(); - reset_query_flags(); + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -655,8 +675,8 @@ mod tests { #[test] fn query_notes_empty_filters_returns_empty() { - let _guard = query_notes_guard(); - reset_query_flags(); + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -669,8 +689,8 @@ mod tests { #[test] fn query_notes_rejects_invalid_filters() { - let _guard = query_notes_guard(); - reset_query_flags(); + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -686,8 +706,8 @@ mod tests { #[test] fn query_notes_reports_transaction_error() { - let _guard = query_notes_guard(); - reset_query_flags(); + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -703,8 +723,8 @@ mod tests { #[test] fn query_notes_reports_query_error() { - let _guard = query_notes_guard(); - reset_query_flags(); + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -718,8 +738,8 @@ mod tests { #[test] fn query_notes_reports_note_json_error() { - let _guard = query_notes_guard(); - reset_query_flags(); + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -750,6 +770,8 @@ mod tests { #[test] fn profile_lookup_returns_metadata_fields() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -787,6 +809,8 @@ mod tests { #[test] fn profile_lookup_returns_none_when_missing() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -801,6 +825,8 @@ mod tests { #[test] fn profile_lookup_reports_query_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -816,6 +842,8 @@ mod tests { #[test] fn profile_lookup_reports_transaction_error() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -831,6 +859,8 @@ mod tests { #[test] fn profile_lookup_returns_none_without_metadata_record() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -852,6 +882,8 @@ mod tests { #[test] fn profile_lookup_invalid_metadata_content_returns_none() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -888,6 +920,8 @@ mod tests { #[test] fn profile_lookup_rejects_invalid_pubkey_length() { + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); @@ -929,8 +963,8 @@ mod tests { #[test] fn concurrent_ingest_handles_parallel_writers() { - let _guard = query_notes_guard(); - reset_query_flags(); + let _guard = test_hooks_guard(); + reset_test_flags(); let tmp_dir = TempDir::new().expect("tempdir should open"); let db_dir = tmp_dir.path().join("ndb"); let config = RadrootsNostrNdbConfig::new(&db_dir); diff --git a/crates/replica-sync/src/emit.rs b/crates/replica-sync/src/emit.rs @@ -1327,34 +1327,35 @@ mod tests { .is_err() ); - let _ = farm::create( - &exec, - &IFarmFields { - d_tag: farm_row.d_tag.clone(), - pubkey: farm_row.pubkey.clone(), - name: "duplicate".to_string(), - about: None, - website: None, - picture: None, - banner: None, - location_primary: None, - location_city: None, - location_region: None, - location_country: None, - }, - ) - .expect("duplicate farm"); assert!( - resolve_farm( + farm::create( &exec, - &RadrootsReplicaFarmSelector { - id: None, - d_tag: Some(farm_row.d_tag.clone()), - pubkey: Some(farm_row.pubkey.clone()), + &IFarmFields { + d_tag: farm_row.d_tag.clone(), + pubkey: farm_row.pubkey.clone(), + name: "duplicate".to_string(), + about: None, + website: None, + picture: None, + banner: None, + location_primary: None, + location_city: None, + location_region: None, + location_country: None, }, ) .is_err() ); + let by_identity = resolve_farm( + &exec, + &RadrootsReplicaFarmSelector { + id: None, + d_tag: Some(farm_row.d_tag.clone()), + pubkey: Some(farm_row.pubkey.clone()), + }, + ) + .expect("resolve unique farm"); + assert_eq!(by_identity.id, farm_row.id); let tags = collect_farm_tags(&exec, &farm_row.id).expect("farm tags"); assert_eq!(tags, vec!["coffee".to_string()]); diff --git a/crates/replica-sync/tests/ingest_roundtrip.rs b/crates/replica-sync/tests/ingest_roundtrip.rs @@ -1819,7 +1819,7 @@ fn sync_status_reports_pending_when_not_all_events_are_ingested() { } #[test] -fn sync_all_rejects_invalid_selectors_and_non_unique_pair() { +fn sync_all_rejects_invalid_selectors_and_resolves_unique_pair() { let exec = SqliteExecutor::open_memory().expect("db"); migrations::run_all_up(&exec).expect("migrations"); @@ -1871,9 +1871,9 @@ fn sync_all_rejects_invalid_selectors_and_non_unique_pair() { location_country: None, }; let _ = unwrap_sql(farm::create(&exec, &fields), "farm one"); - let _ = unwrap_sql(farm::create(&exec, &fields), "farm two"); + assert!(farm::create(&exec, &fields).is_err()); - let non_unique_err = radroots_replica_sync_all( + let bundle = radroots_replica_sync_all( &exec, &RadrootsReplicaSyncRequest { farm: RadrootsReplicaFarmSelector { @@ -1884,12 +1884,8 @@ fn sync_all_rejects_invalid_selectors_and_non_unique_pair() { options: None, }, ) - .expect_err("non unique selector"); - assert!( - non_unique_err - .to_string() - .contains("did not resolve to a single farm") - ); + .expect("unique pair should resolve"); + assert_eq!(bundle.version, RADROOTS_REPLICA_TRANSFER_VERSION); } #[test] diff --git a/crates/sql-core/src/executor_sqlite.rs b/crates/sql-core/src/executor_sqlite.rs @@ -29,6 +29,16 @@ impl SqlExecutor for SqliteExecutor { fn exec(&self, sql: &str, params_json: &str) -> Result<ExecOutcome, SqlError> { let binds = sqlite_util::parse_params(params_json)?; let conn = self.conn.lock().map_err(|_| SqlError::Internal)?; + if binds.is_empty() { + let total_changes_before = conn.total_changes(); + conn.execute_batch(sql).map_err(SqlError::from)?; + let total_changes_after = conn.total_changes(); + let last_insert_id = conn.last_insert_rowid(); + return Ok(ExecOutcome { + changes: (total_changes_after - total_changes_before) as i64, + last_insert_id, + }); + } let n = conn .execute(sql, params_from_iter(binds.into_iter())) .map_err(SqlError::from)?; diff --git a/crates/sql-core/tests/coverage.rs b/crates/sql-core/tests/coverage.rs @@ -6,6 +6,8 @@ use radroots_sql_core::utils::{ to_partial_object_map, uuidv4, with_transaction, }; use radroots_sql_core::{ExecOutcome, SqlExecutor}; +#[cfg(feature = "native")] +use radroots_sql_core::SqliteExecutor; use serde::ser::{SerializeMap, SerializeSeq}; use serde::{Deserialize, Serialize, Serializer}; use serde_json::{Map, Value, json}; @@ -189,6 +191,33 @@ fn sql_executor_reference_impl_forwards_all_methods() { assert!(snapshot.exec_sql.iter().any(|sql| sql == "select 1")); } +#[cfg(feature = "native")] +#[test] +fn sqlite_executor_exec_runs_multi_statement_batches_without_params() { + let exec = SqliteExecutor::open_memory().expect("open sqlite memory"); + + let outcome = exec + .exec( + "create table demo (id integer primary key, name text not null);\ncreate unique index demo_name_idx on demo(name);", + "[]", + ) + .expect("multi-statement batch should succeed"); + assert_eq!(outcome.changes, 0); + + let insert = exec + .exec("insert into demo(name) values ('alpha')", "[]") + .expect("insert should succeed"); + assert_eq!(insert.changes, 1); + + let index_rows = exec + .query_raw( + "select name from sqlite_master where type = 'index' and name = 'demo_name_idx'", + "[]", + ) + .expect("index metadata query should succeed"); + assert_eq!(index_rows, json!([{ "name": "demo_name_idx" }]).to_string()); +} + #[derive(Debug, Serialize, Deserialize, PartialEq)] struct Payload { id: String,