diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 932c1d537d..750d637f32 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -7,6 +7,26 @@ concurrency: cancel-in-progress: true jobs: + rusqlite-compat: + runs-on: self-hosted + timeout-minutes: 30 + strategy: + matrix: + # Keep these endpoints in sync with the supported range in Cargo.toml. + rusqlite: ["0.31.0", "0.39.0"] + env: + CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: fallback + steps: + - uses: actions/checkout@v4 + - name: Install MSRV toolchain + run: rustup toolchain install 1.85.0 --profile minimal + - name: Test downstream rusqlite compatibility on MSRV + run: | + export CARGO_TARGET_DIR=$(mktemp -d /tmp/cargo-target-ldk-node-rusqlite-XXXXXX) + trap 'rm -rf "$CARGO_TARGET_DIR"' EXIT + cargo +1.85.0 add --manifest-path tests/rusqlite-compat/Cargo.toml rusqlite@=${{ matrix.rusqlite }} + cargo +1.85.0 test --manifest-path tests/rusqlite-compat/Cargo.toml + build: timeout-minutes: 120 strategy: diff --git a/Cargo.toml b/Cargo.toml index f880b91162..64516eb55e 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,8 @@ bdk_wallet = { version = "3.1.0", default-features = false, features = ["std", " bitreq = { version = "0.3", default-features = false, features = ["async-https", "json-using-serde"] } rustls = { version = "0.23", default-features = false } -rusqlite = { version = "0.31.0", features = ["bundled"], optional = true } +# rusqlite 0.40.0 and 0.40.1 do not compile on our MSRV. +rusqlite = { version = ">=0.31.0, <0.40.0", features = ["bundled"], optional = true } bitcoin = "0.32.7" bip39 = { version = "3.0.0", features = ["rand"] } bip21 = { version = "0.5", features = ["std"], default-features = false, optional = true } diff --git a/src/io/sqlite_store/migrations.rs b/src/io/sqlite_store/migrations.rs index 1b4de9aa07..7adde7e8b6 100644 --- a/src/io/sqlite_store/migrations.rs +++ b/src/io/sqlite_store/migrations.rs @@ -54,12 +54,10 @@ fn migrate_v1_to_v2(connection: &mut Connection, kv_table_name: &str) -> io::Res })?; // Update user_version - tx.pragma(Some(rusqlite::DatabaseName::Main), "user_version", 2u16, |_| Ok(())).map_err( - |e| { - let msg = format!("Failed to upgrade user_version from 1 to 2: {}", e); - io::Error::new(io::ErrorKind::Other, msg) - }, - )?; + tx.pragma_update(None, "user_version", 2u16).map_err(|e| { + let msg = format!("Failed to upgrade user_version from 1 to 2: {}", e); + io::Error::new(io::ErrorKind::Other, msg) + })?; tx.commit().map_err(|e| { let msg = format!("Failed to migrate table {} from v1 to v2: {}", kv_table_name, e); @@ -150,12 +148,10 @@ fn migrate_v2_to_v3(connection: &mut Connection, kv_table_name: &str) -> io::Res tx.execute(&sql, []).map_err(map_err)?; // Update user_version - tx.pragma(Some(rusqlite::DatabaseName::Main), "user_version", 3u16, |_| Ok(())).map_err( - |e| { - let msg = format!("Failed to upgrade user_version from 2 to 3: {}", e); - io::Error::new(io::ErrorKind::Other, msg) - }, - )?; + tx.pragma_update(None, "user_version", 3u16).map_err(|e| { + let msg = format!("Failed to upgrade user_version from 2 to 3: {}", e); + io::Error::new(io::ErrorKind::Other, msg) + })?; tx.commit().map_err(|e| { let msg = format!("Failed to migrate table {} from v2 to v3: {}", kv_table_name, e); @@ -197,14 +193,7 @@ mod tests { let connection = Connection::open(db_file_path.clone()).unwrap(); - connection - .pragma( - Some(rusqlite::DatabaseName::Main), - "user_version", - old_schema_version, - |_| Ok(()), - ) - .unwrap(); + connection.pragma_update(None, "user_version", old_schema_version).unwrap(); let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( @@ -281,14 +270,7 @@ mod tests { let connection = Connection::open(db_file_path.clone()).unwrap(); - connection - .pragma( - Some(rusqlite::DatabaseName::Main), - "user_version", - old_schema_version, - |_| Ok(()), - ) - .unwrap(); + connection.pragma_update(None, "user_version", old_schema_version).unwrap(); let sql = format!( "CREATE TABLE IF NOT EXISTS {} ( diff --git a/src/io/sqlite_store/mod.rs b/src/io/sqlite_store/mod.rs index 8112140a73..6379ce7c60 100644 --- a/src/io/sqlite_store/mod.rs +++ b/src/io/sqlite_store/mod.rs @@ -301,17 +301,10 @@ impl SqliteStoreInner { if version_res == 0 { // New database, set our SCHEMA_USER_VERSION and continue - connection - .pragma( - Some(rusqlite::DatabaseName::Main), - "user_version", - SCHEMA_USER_VERSION, - |_| Ok(()), - ) - .map_err(|e| { - let msg = format!("Failed to set PRAGMA user_version: {}", e); - io::Error::new(io::ErrorKind::Other, msg) - })?; + connection.pragma_update(None, "user_version", SCHEMA_USER_VERSION).map_err(|e| { + let msg = format!("Failed to set PRAGMA user_version: {}", e); + io::Error::new(io::ErrorKind::Other, msg) + })?; } else if version_res < SCHEMA_USER_VERSION { migrations::migrate_schema( &mut connection, @@ -553,8 +546,13 @@ impl SqliteStoreInner { self.kv_table_name ); let count_sql = format!("SELECT COUNT(*) FROM {}", self.kv_table_name); - let count: usize = - locked_conn.query_row(&count_sql, [], |row| row.get(0)).map_err(|e| { + let count = locked_conn + .query_row(&count_sql, [], |row| { + let count: i64 = row.get(0)?; + usize::try_from(count) + .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(0, count)) + }) + .map_err(|e| { let msg = format!("Failed to count rows: {}", e); io::Error::new(io::ErrorKind::Other, msg) })?; diff --git a/tests/rusqlite-compat/Cargo.toml b/tests/rusqlite-compat/Cargo.toml new file mode 100644 index 0000000000..a2be54dee1 --- /dev/null +++ b/tests/rusqlite-compat/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ldk-node-rusqlite-compat" +version = "0.0.0" +edition = "2021" +rust-version = "1.85" +publish = false + +# Resolve as a downstream application, without LDK Node's legacy test dependencies. +[workspace] + +[dependencies] +ldk-node = { path = "../.." } +rusqlite = "=0.39.0" +tokio = { version = "1.39", features = ["macros", "rt"] } diff --git a/tests/rusqlite-compat/README.md b/tests/rusqlite-compat/README.md new file mode 100644 index 0000000000..c27412e60c --- /dev/null +++ b/tests/rusqlite-compat/README.md @@ -0,0 +1,32 @@ +# Downstream rusqlite compatibility + +This separate Cargo workspace checks that an application can select a stock +rusqlite release while using LDK Node's default features. The test writes through +LDK Node, reopens the store, and reads the database with the application's rusqlite. +Neither rusqlite nor libsqlite3-sys is patched. + +The root crate's development dependencies on LDK Node 0.6.2 and 0.7.0 require +rusqlite 0.31 unconditionally. They constrain root builds even when individual +tests are filtered out, but do not participate in downstream resolution. Keep +this fixture separate so CI actually tests both ends of the supported range. + +From the repository root, run: + +```sh +export CARGO_TARGET_DIR=$(mktemp -d /tmp/cargo-target-ldk-node-rusqlite-XXXXXX) +export CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback +cargo +1.85.0 test --manifest-path tests/rusqlite-compat/Cargo.toml +``` + +The fixture defaults to the maximum tested version. To test another release, +change its direct requirement with +`cargo +1.85.0 add --manifest-path tests/rusqlite-compat/Cargo.toml rusqlite@=0.31.0` +and rerun the test. Restore the maximum requirement afterwards. The resolver +setting selects MSRV-compatible versions of unrelated transitive dependencies. + +When changing the supported range, update both endpoints in +`.github/workflows/rust.yml` and test each newly included release locally. The +current upper bound excludes 0.40.0 and 0.40.1, which use `cfg_select!` and fail to +compile on Rust 1.85. Although the bundled-feature probe for 0.40.2 passes on +1.85, Cargo cannot express a disjoint requirement that admits it while excluding +those earlier patches and retaining the older release lines. diff --git a/tests/rusqlite-compat/src/lib.rs b/tests/rusqlite-compat/src/lib.rs new file mode 100644 index 0000000000..4787de2fd8 --- /dev/null +++ b/tests/rusqlite-compat/src/lib.rs @@ -0,0 +1,40 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +#[cfg(test)] +mod tests { + use ldk_node::io::sqlite_store::{ + SqliteStore, DEFAULT_KV_TABLE_NAME, DEFAULT_SQLITE_DB_FILE_NAME, + }; + use ldk_node::lightning::util::persist::KVStore; + + #[tokio::test] + async fn persists_with_the_applications_rusqlite() { + let data_dir = + std::env::temp_dir().join(format!("ldk-node-rusqlite-compat-{}", std::process::id())); + let store = SqliteStore::new(data_dir.clone(), None, None).unwrap(); + KVStore::write(&store, "test", "", "key", vec![42]).await.unwrap(); + drop(store); + + let store = SqliteStore::new(data_dir.clone(), None, None).unwrap(); + assert_eq!(KVStore::read(&store, "test", "", "key").await.unwrap(), vec![42]); + drop(store); + + let connection = + rusqlite::Connection::open(data_dir.join(DEFAULT_SQLITE_DB_FILE_NAME)).unwrap(); + let value: Vec = connection + .query_row( + &format!("SELECT value FROM {DEFAULT_KV_TABLE_NAME} WHERE key = 'key'"), + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(value, vec![42]); + drop(connection); + std::fs::remove_dir_all(data_dir).unwrap(); + } +}