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:
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,