lib

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

commit 08d25db13554895b62385d5921a80d277be684de
parent cc9aa1ef5d6e570a5d6d834c0d602e301375b1dd
Author: triesap <tyson@radroots.org>
Date:   Sat, 15 Aug 2026 01:35:47 +0000

service-sqlite: remove competing sqlite wrappers

- migrate backup capture and verification queries to SQLx-owned connections
- convert restore, crash, and backup fixtures to the governed SQLx path
- remove the service crate's rusqlite dependency and lockfile edge
- preserve authority, cancellation, integrity, and cross-target contracts

Diffstat:
MCargo.lock | 1-
Mcontracts/releases/sqlite_runtime.toml | 1-
Mcrates/service_sqlite/Cargo.toml | 1-
Mcrates/service_sqlite/src/backup/capture.rs | 349+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mcrates/service_sqlite/src/backup/verify.rs | 491++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mcrates/service_sqlite/src/connection.rs | 42+++++++++++++++++++++---------------------
Mcrates/service_sqlite/src/open.rs | 4----
Mcrates/service_sqlite/src/restore/process_tests.rs | 68+++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mcrates/service_sqlite/src/restore/stage.rs | 56++++++++++++++++++++++++++++++++++++++------------------
Mcrates/service_sqlite/tests/package_boundary.rs | 17++++++++---------
Mtools/xtask/src/contract.rs | 7++-----
11 files changed, 591 insertions(+), 446 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -3529,7 +3529,6 @@ dependencies = [ "libsqlite3-sys", "radroots_runtime_paths", "radroots_storage", - "rusqlite", "rustix 1.1.4", "serde", "serde_json", diff --git a/contracts/releases/sqlite_runtime.toml b/contracts/releases/sqlite_runtime.toml @@ -38,6 +38,5 @@ owner = "rcld-rshr-045" status = "in_progress" temporary_direct_dependencies = [ "radroots_geonames:rusqlite", - "radroots_service_sqlite:rusqlite", "workspace:rusqlite", ] diff --git a/crates/service_sqlite/Cargo.toml b/crates/service_sqlite/Cargo.toml @@ -17,7 +17,6 @@ futures = { workspace = true } libsqlite3-sys = { workspace = true } radroots_runtime_paths = { workspace = true } radroots_storage = { workspace = true } -rusqlite = { workspace = true, features = ["backup", "bundled"] } rustix = { workspace = true } serde = { workspace = true, features = ["derive", "std"] } serde_json = { workspace = true } diff --git a/crates/service_sqlite/src/backup/capture.rs b/crates/service_sqlite/src/backup/capture.rs @@ -18,14 +18,13 @@ use std::{ #[cfg(test)] use core::sync::atomic::AtomicU8; -use rusqlite::{Connection, OpenFlags, OptionalExtension, types::ValueRef}; use rustix::{ fs::{AtFlags, FileType, Mode, OFlags, fchmod, fstat, mkdirat, open, openat, statat, unlinkat}, process::geteuid, }; use sha2::{Digest, Sha256}; use sqlx::{ - ConnectOptions, Connection as _, Sqlite, SqliteConnection, pool::PoolConnection, + ConnectOptions, Connection as _, Row, Sqlite, SqliteConnection, pool::PoolConnection, sqlite::SqliteConnectOptions, }; @@ -359,12 +358,13 @@ impl CaptureWorker { self.validator.validate()?; staging.validate()?; - let source = self.open_inspection_source()?; - verify_database_inventory(&source)?; - verify_database_metadata(&source, &self.metadata)?; - source - .close() - .map_err(|(_, source)| backup_source(BackupFailureKind::Capture, source))?; + let source = self + .admission + .as_mut() + .ok_or_else(|| backup_error(BackupFailureKind::Capture))?; + self.runtime.block_on(verify_database_inventory(source))?; + self.runtime + .block_on(verify_database_metadata(source, &self.metadata))?; self.validator.validate()?; staging.validate()?; @@ -397,18 +397,20 @@ impl CaptureWorker { self.check_cancelled()?; self.validator.validate()?; staging.validate()?; - let destination = self.open_inspection_destination(&staging)?; - verify_database_inventory(&destination)?; - verify_database_metadata(&destination, &self.metadata)?; - verify_integrity(&destination)?; + let mut destination = self.open_inspection_destination(&staging)?; + self.runtime + .block_on(verify_database_inventory(&mut destination))?; + self.runtime + .block_on(verify_database_metadata(&mut destination, &self.metadata))?; + self.runtime.block_on(verify_integrity(&mut destination))?; staging.record_sidecars(); self.check_cancelled()?; self.validator.validate()?; staging.validate()?; - destination - .close() - .map_err(|(_, source)| backup_source(BackupFailureKind::Capture, source))?; + self.runtime + .block_on(destination.close()) + .map_err(|source| backup_source(BackupFailureKind::Capture, source))?; staging.record_sidecars(); self.validator.validate()?; staging.validate()?; @@ -510,23 +512,6 @@ impl CaptureWorker { result } - fn open_inspection_source(&self) -> Result<Connection, ServiceSqliteError> { - self.validator.validate()?; - let result = Connection::open_with_flags( - self.validator.database_path(), - OpenFlags::SQLITE_OPEN_READ_ONLY - | OpenFlags::SQLITE_OPEN_NO_MUTEX - | OpenFlags::SQLITE_OPEN_NOFOLLOW, - ); - self.validator.validate()?; - let connection = - result.map_err(|source| backup_source(BackupFailureKind::Capture, source))?; - connection - .pragma_update(None, "query_only", true) - .map_err(|source| backup_source(BackupFailureKind::Capture, source))?; - Ok(connection) - } - fn open_sqlx_destination( &self, staging: &StagingGuard, @@ -547,14 +532,16 @@ impl CaptureWorker { fn open_inspection_destination( &self, staging: &StagingGuard, - ) -> Result<Connection, ServiceSqliteError> { + ) -> Result<SqliteConnection, ServiceSqliteError> { staging.validate()?; - let result = Connection::open_with_flags( - staging.state_path(), - OpenFlags::SQLITE_OPEN_READ_WRITE - | OpenFlags::SQLITE_OPEN_NO_MUTEX - | OpenFlags::SQLITE_OPEN_NOFOLLOW, - ); + let options = SqliteConnectOptions::new() + .filename(staging.state_path()) + .create_if_missing(false) + .foreign_keys(false) + .disable_statement_logging(); + let result = self + .runtime + .block_on(SqliteConnection::connect_with(&options)); staging.validate()?; result.map_err(|source| backup_source(BackupFailureKind::Capture, source)) } @@ -1175,29 +1162,24 @@ fn identity(status: &rustix::fs::Stat) -> Result<FileIdentity, ServiceSqliteErro }) } -fn verify_database_inventory(connection: &Connection) -> Result<(), ServiceSqliteError> { - let mut statement = connection - .prepare("PRAGMA database_list") - .map_err(|source| backup_source(BackupFailureKind::Capture, source))?; - let mut rows = statement - .query([]) +async fn verify_database_inventory( + connection: &mut SqliteConnection, +) -> Result<(), ServiceSqliteError> { + let rows = sqlx::query("SELECT seq, name FROM pragma_database_list LIMIT 2") + .fetch_all(connection) + .await .map_err(|source| backup_source(BackupFailureKind::Capture, source))?; let first = rows - .next() - .map_err(|source| backup_source(BackupFailureKind::Capture, source))? + .first() .ok_or_else(|| backup_error(BackupFailureKind::Capture))?; - let sequence: i64 = first - .get(0) + let sequence = first + .try_get::<i64, _>(0) .map_err(|source| backup_source(BackupFailureKind::Capture, source))?; - let name: String = first - .get(1) + let name = first + .try_get::<String, _>(1) .map_err(|source| backup_source(BackupFailureKind::Capture, source))?; - let has_extra = rows - .next() - .map_err(|source| backup_source(BackupFailureKind::Capture, source))? - .is_some(); require_backup_condition( - database_inventory_matches(sequence, &name, has_extra), + database_inventory_matches(sequence, &name, rows.len() > 1), BackupFailureKind::Capture, )?; Ok(()) @@ -1207,20 +1189,20 @@ fn database_inventory_matches(sequence: i64, name: &str, has_extra: bool) -> boo crate::all_constraints([sequence == 0, name == "main", !has_extra]) } -fn verify_database_metadata( - connection: &Connection, +async fn verify_database_metadata( + connection: &mut SqliteConnection, expected: &ServiceDatabaseMetadata, ) -> Result<(), ServiceSqliteError> { - let application_id: i64 = connection - .pragma_query_value(None, "application_id", |row| row.get(0)) - .map_err(metadata_source)?; - let row_count: i64 = connection - .query_row( - "SELECT COUNT(*) FROM (SELECT 1 FROM radroots_service_metadata LIMIT 2)", - [], - |row| row.get(0), - ) + let application_id = sqlx::query_scalar::<_, i64>("PRAGMA application_id") + .fetch_one(&mut *connection) + .await .map_err(metadata_source)?; + let row_count = sqlx::query_scalar::<_, i64>( + "SELECT COUNT(*) FROM (SELECT 1 FROM radroots_service_metadata LIMIT 2)", + ) + .fetch_one(&mut *connection) + .await + .map_err(metadata_source)?; crate::require_condition( crate::all_constraints([ row_count == 1, @@ -1228,9 +1210,8 @@ fn verify_database_metadata( ]), ServiceSqliteErrorKind::Metadata, )?; - let row = connection - .query_row( - "SELECT + let row = sqlx::query( + "SELECT CASE WHEN typeof(service_id) = 'text' AND length(CAST(service_id AS BLOB)) BETWEEN 1 AND ?1 THEN service_id END, @@ -1247,21 +1228,27 @@ fn verify_database_metadata( FROM radroots_service_metadata WHERE singleton = 1 LIMIT 1", - [MAX_ID_UTF8_BYTES], - |row| { - Ok(( - row.get::<_, Option<String>>(0)?, - row.get::<_, Option<String>>(1)?, - row.get::<_, Option<Vec<u8>>>(2)?, - row.get::<_, Option<i64>>(3)?, - row.get::<_, Option<i64>>(4)?, - )) - }, - ) - .optional() + ) + .bind(MAX_ID_UTF8_BYTES) + .fetch_optional(&mut *connection) + .await + .map_err(metadata_source)?; + let Some(row) = row else { + return Err(ServiceSqliteError::new(ServiceSqliteErrorKind::Metadata)); + }; + let service = row + .try_get::<Option<String>, _>(0) + .map_err(metadata_source)?; + let instance = row + .try_get::<Option<String>, _>(1) .map_err(metadata_source)?; - let Some((Some(service), Some(instance), Some(generation), Some(schema), Some(created_at))) = - row + let generation = row + .try_get::<Option<Vec<u8>>, _>(2) + .map_err(metadata_source)?; + let schema = row.try_get::<Option<i64>, _>(3).map_err(metadata_source)?; + let created_at = row.try_get::<Option<i64>, _>(4).map_err(metadata_source)?; + let (Some(service), Some(instance), Some(generation), Some(schema), Some(created_at)) = + (service, instance, generation, schema, created_at) else { return Err(ServiceSqliteError::new(ServiceSqliteErrorKind::Metadata)); }; @@ -1278,43 +1265,45 @@ fn verify_database_metadata( Ok(()) } -fn verify_integrity(connection: &Connection) -> Result<(), ServiceSqliteError> { - let mut statement = connection - .prepare("PRAGMA integrity_check(1)") +async fn verify_integrity(connection: &mut SqliteConnection) -> Result<(), ServiceSqliteError> { + let rows = sqlx::query("PRAGMA integrity_check(1)") + .fetch_all(&mut *connection) + .await .map_err(integrity_source)?; - let mut rows = statement.query([]).map_err(integrity_source)?; let row = rows - .next() - .map_err(integrity_source)? + .first() .ok_or_else(|| ServiceSqliteError::new(ServiceSqliteErrorKind::Integrity))?; - let result = row.get_ref(0).map_err(integrity_source)?; + let value = row.try_get::<&str, _>(0).map_err(integrity_source)?; crate::require_condition( - integrity_projection_is_ok(result) && rows.next().map_err(integrity_source)?.is_none(), + integrity_projection_is_ok( + Some("text"), + i64::try_from(value.len()).ok(), + Some(value.as_bytes()), + ) && rows.len() == 1, ServiceSqliteErrorKind::Integrity, )?; - let mut statement = connection - .prepare("PRAGMA foreign_key_check") + let violation = sqlx::query_scalar::<_, i64>("SELECT 1 FROM pragma_foreign_key_check LIMIT 1") + .fetch_optional(connection) + .await .map_err(integrity_source)?; - crate::require_condition( - statement - .query([]) - .map_err(integrity_source)? - .next() - .map_err(integrity_source)? - .is_none(), - ServiceSqliteErrorKind::Integrity, - )?; + crate::require_condition(violation.is_none(), ServiceSqliteErrorKind::Integrity)?; Ok(()) } -fn integrity_projection_is_ok(value: ValueRef<'_>) -> bool { - matches!( - value, - ValueRef::Text(bytes) - if !bytes.is_empty() - && bytes.len() <= MAX_INTEGRITY_RESULT_UTF8_BYTES - && bytes == b"ok" - ) +fn integrity_projection_is_ok( + value_type: Option<&str>, + byte_length: Option<i64>, + value: Option<&[u8]>, +) -> bool { + crate::all_constraints([ + value_type == Some("text"), + byte_length.is_some_and(|length| { + length > 0 + && usize::try_from(length) + .is_ok_and(|length| length <= MAX_INTEGRITY_RESULT_UTF8_BYTES) + }), + value == Some(b"ok"), + ]) } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -1414,11 +1403,11 @@ fn backup_source( ) } -fn integrity_source(source: rusqlite::Error) -> ServiceSqliteError { +fn integrity_source(source: sqlx::Error) -> ServiceSqliteError { ServiceSqliteError::with_source(ServiceSqliteErrorKind::Integrity, source) } -fn metadata_source(source: rusqlite::Error) -> ServiceSqliteError { +fn metadata_source(source: sqlx::Error) -> ServiceSqliteError { ServiceSqliteError::with_source(ServiceSqliteErrorKind::Metadata, source) } @@ -1830,14 +1819,34 @@ mod tests { #[test] fn integrity_projection_bounds_corrupt_text_before_semantic_acceptance() { - assert!(integrity_projection_is_ok(ValueRef::Text(b"ok"))); - assert!(!integrity_projection_is_ok(ValueRef::Text(b""))); - assert!(!integrity_projection_is_ok(ValueRef::Text(b"not-ok"))); + assert!(integrity_projection_is_ok( + Some("text"), + Some(2), + Some(b"ok") + )); + assert!(!integrity_projection_is_ok( + Some("text"), + Some(0), + Some(b"") + )); + assert!(!integrity_projection_is_ok( + Some("text"), + Some(6), + Some(b"not-ok") + )); let maximum = vec![b'x'; MAX_INTEGRITY_RESULT_UTF8_BYTES]; - assert!(!integrity_projection_is_ok(ValueRef::Text(&maximum))); + assert!(!integrity_projection_is_ok( + Some("text"), + i64::try_from(maximum.len()).ok(), + Some(&maximum) + )); let over_maximum = vec![b'x'; MAX_INTEGRITY_RESULT_UTF8_BYTES + 1]; - assert!(!integrity_projection_is_ok(ValueRef::Text(&over_maximum))); - assert!(!integrity_projection_is_ok(ValueRef::Null)); + assert!(!integrity_projection_is_ok( + Some("text"), + i64::try_from(over_maximum.len()).ok(), + Some(&over_maximum) + )); + assert!(!integrity_projection_is_ok(None, None, None)); } #[test] @@ -1848,7 +1857,7 @@ mod tests { assert!(!database_inventory_matches(0, "main", true)); } - fn metadata_fixture() -> (Connection, ServiceDatabaseMetadata) { + async fn metadata_fixture() -> (SqliteConnection, ServiceDatabaseMetadata) { let metadata = ServiceDatabaseMetadata::from_verified_backup( ServiceId::new("myc").expect("service"), InstanceId::new("primary").expect("instance"), @@ -1858,10 +1867,11 @@ mod tests { crate::ServiceSqliteApplicationId::new(0x5244_5254).expect("application ID"), ) .expect("metadata"); - let connection = Connection::open_in_memory().expect("database"); - connection - .execute_batch(&format!( - "PRAGMA application_id = {}; + let mut connection = SqliteConnection::connect("sqlite::memory:") + .await + .expect("database"); + let schema = format!( + "PRAGMA application_id = {}; CREATE TABLE radroots_service_metadata ( singleton INTEGER, service_id TEXT, @@ -1870,39 +1880,46 @@ mod tests { state_schema_version INTEGER, created_at_unix_ms INTEGER );", - metadata.application_id().get() - )) + metadata.application_id().get() + ); + sqlx::raw_sql(sqlx::AssertSqlSafe(schema.as_str())) + .execute(&mut connection) + .await .expect("metadata schema"); - connection - .execute( - "INSERT INTO radroots_service_metadata VALUES (1, ?1, ?2, ?3, ?4, ?5)", - rusqlite::params![ - metadata.service().as_str(), - metadata.instance().as_str(), - metadata.source_generation().as_bytes().as_slice(), - i64::from(metadata.state_schema_version().get()), - i64::try_from(metadata.created_at_unix_ms()).expect("time"), - ], - ) + sqlx::query("INSERT INTO radroots_service_metadata VALUES (1, ?, ?, ?, ?, ?)") + .bind(metadata.service().as_str()) + .bind(metadata.instance().as_str()) + .bind(metadata.source_generation().as_bytes().as_slice()) + .bind(i64::from(metadata.state_schema_version().get())) + .bind(i64::try_from(metadata.created_at_unix_ms()).expect("time")) + .execute(&mut connection) + .await .expect("metadata row"); (connection, metadata) } - #[test] - fn capture_database_inventory_metadata_and_integrity_accept_exact_state() { - let (connection, metadata) = metadata_fixture(); - verify_database_inventory(&connection).expect("main-only inventory"); - verify_database_metadata(&connection, &metadata).expect("exact metadata"); - verify_integrity(&connection).expect("healthy database"); - - connection - .execute_batch("ATTACH DATABASE ':memory:' AS extra") + #[tokio::test(flavor = "current_thread")] + async fn capture_database_inventory_metadata_and_integrity_accept_exact_state() { + let (mut connection, metadata) = metadata_fixture().await; + verify_database_inventory(&mut connection) + .await + .expect("main-only inventory"); + verify_database_metadata(&mut connection, &metadata) + .await + .expect("exact metadata"); + verify_integrity(&mut connection) + .await + .expect("healthy database"); + + sqlx::query("ATTACH DATABASE ':memory:' AS extra") + .execute(&mut connection) + .await .expect("attach extra"); - assert!(verify_database_inventory(&connection).is_err()); + assert!(verify_database_inventory(&mut connection).await.is_err()); } - #[test] - fn capture_metadata_rejects_every_independent_identity_drift() { + #[tokio::test(flavor = "current_thread")] + async fn capture_metadata_rejects_every_independent_identity_drift() { for statement in [ "PRAGMA application_id = 1", "INSERT INTO radroots_service_metadata SELECT 2, service_id, instance_id, source_generation, state_schema_version, created_at_unix_ms FROM radroots_service_metadata", @@ -1913,27 +1930,37 @@ mod tests { "UPDATE radroots_service_metadata SET created_at_unix_ms = 1235", "UPDATE radroots_service_metadata SET service_id = NULL", ] { - let (connection, metadata) = metadata_fixture(); - connection.execute_batch(statement).expect("apply drift"); + let (mut connection, metadata) = metadata_fixture().await; + sqlx::raw_sql(statement) + .execute(&mut connection) + .await + .expect("apply drift"); assert!( - verify_database_metadata(&connection, &metadata).is_err(), + verify_database_metadata(&mut connection, &metadata) + .await + .is_err(), "drift must fail: {statement}" ); } } - #[test] - fn capture_integrity_rejects_foreign_key_violations() { - let connection = Connection::open_in_memory().expect("database"); - connection - .execute_batch( - "PRAGMA foreign_keys = OFF; + #[tokio::test(flavor = "current_thread")] + async fn capture_integrity_rejects_foreign_key_violations() { + let mut connection = SqliteConnection::connect("sqlite::memory:") + .await + .expect("database"); + sqlx::raw_sql( + "PRAGMA foreign_keys = OFF; CREATE TABLE parent(id INTEGER PRIMARY KEY); CREATE TABLE child(parent_id INTEGER REFERENCES parent(id)); INSERT INTO child(parent_id) VALUES (41);", - ) - .expect("foreign-key violation fixture"); - let error = verify_integrity(&connection).expect_err("foreign-key drift must fail"); + ) + .execute(&mut connection) + .await + .expect("foreign-key violation fixture"); + let error = verify_integrity(&mut connection) + .await + .expect_err("foreign-key drift must fail"); assert_eq!(error.kind(), ServiceSqliteErrorKind::Integrity); } diff --git a/crates/service_sqlite/src/backup/verify.rs b/crates/service_sqlite/src/backup/verify.rs @@ -13,12 +13,12 @@ use { core::num::NonZeroU32, radroots_runtime_paths::{InstanceId, ServiceId}, radroots_storage::event::SourceGeneration, - rusqlite::{Connection, OpenFlags, OptionalExtension, types::ValueRef}, rustix::{ fs::{Dir, FileType, Mode, OFlags, fstat, open, openat}, process::geteuid, }, sha2::{Digest, Sha256}, + sqlx::{ConnectOptions, Connection as _, Row, SqliteConnection, sqlite::SqliteConnectOptions}, std::{ error::Error, fs::File, @@ -181,15 +181,20 @@ fn verify_backup_bundle_native( )?; binding.validate()?; - let connection = open_sqlite_from_retained_state(&binding)?; - apply_connection_policy(&connection)?; - binding.validate()?; - verify_database_inventory(&connection)?; - let database_metadata = verify_database_metadata(&connection, &manifest, expected_identity)?; - verify_integrity(&connection)?; - connection - .close() - .map_err(|(_, source)| integrity_source(source))?; + let database_metadata = futures::executor::block_on(async { + let mut connection = open_sqlite_from_retained_state(&binding).await?; + apply_connection_policy(&mut connection).await?; + binding.validate()?; + verify_database_inventory(&mut connection).await?; + binding.validate()?; + let database_metadata = + verify_database_metadata(&mut connection, &manifest, expected_identity).await?; + binding.validate()?; + verify_integrity(&mut connection).await?; + binding.validate()?; + connection.close().await.map_err(integrity_source)?; + Ok::<_, ServiceSqliteError>(database_metadata) + })?; binding.validate_inventory()?; let final_digest = binding.hash_state(maximum_state_bytes)?; @@ -474,42 +479,52 @@ fn file_identity(status: &rustix::fs::Stat) -> Result<FileIdentity, ServiceSqlit } #[cfg(any(target_os = "linux", target_os = "macos"))] -fn open_sqlite_from_retained_state( +async fn open_sqlite_from_retained_state( binding: &VerifiedBundleBinding, -) -> Result<Connection, ServiceSqliteError> { +) -> Result<SqliteConnection, ServiceSqliteError> { let descriptor = binding.state.as_raw_fd(); #[cfg(target_os = "linux")] let descriptor_path = format!("/proc/self/fd/{descriptor}"); #[cfg(target_os = "macos")] let descriptor_path = format!("/dev/fd/{descriptor}"); - let uri = format!("file:{descriptor_path}?mode=ro&immutable=1"); - Connection::open_with_flags( - uri, - OpenFlags::SQLITE_OPEN_READ_ONLY - | OpenFlags::SQLITE_OPEN_URI - | OpenFlags::SQLITE_OPEN_NO_MUTEX, - ) - .map_err(|source| verification_source(VerificationFailureKind::Inventory, source)) + let options = SqliteConnectOptions::new() + .filename(descriptor_path) + .read_only(true) + .immutable(true) + .create_if_missing(false) + .foreign_keys(false) + .disable_statement_logging(); + SqliteConnection::connect_with(&options) + .await + .map_err(|source| verification_source(VerificationFailureKind::Inventory, source)) } #[cfg(any(target_os = "linux", target_os = "macos"))] -fn apply_connection_policy(connection: &Connection) -> Result<(), ServiceSqliteError> { - connection - .pragma_update(None, "query_only", true) +async fn apply_connection_policy( + connection: &mut SqliteConnection, +) -> Result<(), ServiceSqliteError> { + sqlx::query("PRAGMA query_only = ON") + .execute(&mut *connection) + .await .map_err(integrity_source)?; - connection - .pragma_update(None, "trusted_schema", false) + sqlx::query("PRAGMA trusted_schema = OFF") + .execute(&mut *connection) + .await .map_err(integrity_source)?; - verify_connection_policy(connection) + verify_connection_policy(connection).await } #[cfg(any(target_os = "linux", target_os = "macos"))] -fn verify_connection_policy(connection: &Connection) -> Result<(), ServiceSqliteError> { - let query_only: i64 = connection - .pragma_query_value(None, "query_only", |row| row.get(0)) +async fn verify_connection_policy( + connection: &mut SqliteConnection, +) -> Result<(), ServiceSqliteError> { + let query_only = sqlx::query_scalar::<_, i64>("PRAGMA query_only") + .fetch_one(&mut *connection) + .await .map_err(integrity_source)?; - let trusted_schema: i64 = connection - .pragma_query_value(None, "trusted_schema", |row| row.get(0)) + let trusted_schema = sqlx::query_scalar::<_, i64>("PRAGMA trusted_schema") + .fetch_one(connection) + .await .map_err(integrity_source)?; require_verification_connection_policy(query_only, trusted_schema) } @@ -525,25 +540,19 @@ fn require_verification_connection_policy( } #[cfg(any(target_os = "linux", target_os = "macos"))] -fn verify_database_inventory(connection: &Connection) -> Result<(), ServiceSqliteError> { - let mut statement = connection - .prepare("PRAGMA database_list") +async fn verify_database_inventory( + connection: &mut SqliteConnection, +) -> Result<(), ServiceSqliteError> { + let rows = sqlx::query("SELECT seq, name FROM pragma_database_list LIMIT 2") + .fetch_all(connection) + .await .map_err(integrity_source)?; - let mut rows = statement.query([]).map_err(integrity_source)?; - let first = rows - .next() - .map_err(integrity_source)? - .ok_or_else(|| integrity_error(IntegrityFailureKind::DatabaseInventory))?; - let sequence_matches = matches!( - first.get_ref(0).map_err(integrity_source)?, - ValueRef::Integer(0) - ); - let name_matches = matches!( - first.get_ref(1).map_err(integrity_source)?, - ValueRef::Text(b"main") - ); - let has_extra = rows.next().map_err(integrity_source)?.is_some(); - require_verification_database_inventory(sequence_matches, name_matches, has_extra) + let Some(first) = rows.first() else { + return Err(integrity_error(IntegrityFailureKind::DatabaseInventory)); + }; + let sequence_matches = first.try_get::<i64, _>(0).ok() == Some(0); + let name_matches = first.try_get::<&str, _>(1).ok() == Some("main"); + require_verification_database_inventory(sequence_matches, name_matches, rows.len() > 1) } #[cfg(any(target_os = "linux", target_os = "macos"))] @@ -558,43 +567,37 @@ fn require_verification_database_inventory( } #[cfg(any(target_os = "linux", target_os = "macos"))] -fn verify_database_metadata( - connection: &Connection, +async fn verify_database_metadata( + connection: &mut SqliteConnection, manifest: &ServiceBackupManifest, expected: &ServiceDatabaseIdentity, ) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> { - let mut object_statement = connection - .prepare( - "SELECT type + let object_rows = sqlx::query( + "SELECT type FROM main.sqlite_schema WHERE name = 'radroots_service_metadata' LIMIT 2", - ) - .map_err(metadata_source)?; - let mut object_rows = object_statement.query([]).map_err(metadata_source)?; - let object = object_rows - .next() - .map_err(metadata_source)? - .ok_or_else(metadata_error)?; + ) + .fetch_all(&mut *connection) + .await + .map_err(metadata_source)?; + let object = object_rows.first().ok_or_else(metadata_error)?; crate::require_condition( - matches!( - object.get_ref(0).map_err(metadata_source)?, - ValueRef::Text(b"table") - ) && object_rows.next().map_err(metadata_source)?.is_none(), + object.try_get::<&str, _>(0).ok() == Some("table") && object_rows.len() == 1, ServiceSqliteErrorKind::Metadata, )?; - let application_id: i64 = connection - .pragma_query_value(None, "application_id", |row| row.get(0)) + let application_id = sqlx::query_scalar::<_, i64>("PRAGMA application_id") + .fetch_one(&mut *connection) + .await .map_err(metadata_source)?; let application_id = u32::try_from(application_id) .ok() .and_then(|value| crate::ServiceSqliteApplicationId::new(value).ok()) .ok_or_else(metadata_error)?; - let mut statement = connection - .prepare( - "SELECT + let rows = sqlx::query( + "SELECT CASE WHEN typeof(singleton) = 'integer' THEN singleton END, CASE WHEN typeof(service_id) = 'text' AND length(CAST(service_id AS BLOB)) BETWEEN 1 AND ?1 @@ -611,25 +614,25 @@ fn verify_database_metadata( THEN created_at_unix_ms END FROM radroots_service_metadata LIMIT 2", - ) + ) + .bind(MAX_ID_UTF8_BYTES) + .fetch_all(connection) + .await + .map_err(metadata_source)?; + let row = rows.first().ok_or_else(metadata_error)?; + let singleton = row.try_get::<Option<i64>, _>(0).map_err(metadata_source)?; + let service = row + .try_get::<Option<String>, _>(1) .map_err(metadata_source)?; - let mut rows = statement - .query([MAX_ID_UTF8_BYTES]) + let instance = row + .try_get::<Option<String>, _>(2) .map_err(metadata_source)?; - let row = rows - .next() - .map_err(metadata_source)? - .ok_or_else(metadata_error)?; - let singleton: Option<i64> = row.get(0).map_err(metadata_source)?; - let service: Option<String> = row.get(1).map_err(metadata_source)?; - let instance: Option<String> = row.get(2).map_err(metadata_source)?; - let generation: Option<Vec<u8>> = row.get(3).map_err(metadata_source)?; - let schema: Option<i64> = row.get(4).map_err(metadata_source)?; - let created_at: Option<i64> = row.get(5).map_err(metadata_source)?; - crate::require_condition( - rows.next().map_err(metadata_source)?.is_none(), - ServiceSqliteErrorKind::Metadata, - )?; + let generation = row + .try_get::<Option<Vec<u8>>, _>(3) + .map_err(metadata_source)?; + let schema = row.try_get::<Option<i64>, _>(4).map_err(metadata_source)?; + let created_at = row.try_get::<Option<i64>, _>(5).map_err(metadata_source)?; + crate::require_condition(rows.len() == 1, ServiceSqliteErrorKind::Metadata)?; let (Some(1), Some(service), Some(instance), Some(generation), Some(schema), Some(created_at)) = (singleton, service, instance, generation, schema, created_at) else { @@ -666,22 +669,24 @@ fn verify_database_metadata( } #[cfg(any(target_os = "linux", target_os = "macos"))] -fn verify_integrity(connection: &Connection) -> Result<(), ServiceSqliteError> { - let mut statement = connection - .prepare("PRAGMA integrity_check(1)") +async fn verify_integrity(connection: &mut SqliteConnection) -> Result<(), ServiceSqliteError> { + let rows = sqlx::query("PRAGMA integrity_check(1)") + .fetch_all(&mut *connection) + .await .map_err(integrity_source)?; - let mut rows = statement.query([]).map_err(integrity_source)?; let row = rows - .next() - .map_err(integrity_source)? + .first() .ok_or_else(|| integrity_error(IntegrityFailureKind::Sqlite))?; - let projection = - verification_integrity_value_projection(row.get_ref(0).map_err(integrity_source)?); - let has_extra = rows.next().map_err(integrity_source)?.is_some(); - require_verification_integrity_projection(projection, has_extra)?; - let violation = connection - .query_row("PRAGMA foreign_key_check", [], |_| Ok(())) - .optional() + let value = row.try_get::<&str, _>(0).map_err(integrity_source)?; + let projection = verification_integrity_value_projection( + Some("text"), + i64::try_from(value.len()).ok(), + Some(value.as_bytes()), + ); + require_verification_integrity_projection(projection, rows.len() > 1)?; + let violation = sqlx::query_scalar::<_, i64>("SELECT 1 FROM pragma_foreign_key_check LIMIT 1") + .fetch_optional(connection) + .await .map_err(integrity_source)?; require_integrity_condition(violation.is_none(), IntegrityFailureKind::ForeignKeys)?; Ok(()) @@ -696,12 +701,18 @@ fn require_verification_metadata_projection(matches: [bool; 9]) -> Result<(), Se } #[cfg(any(target_os = "linux", target_os = "macos"))] -fn verification_integrity_value_projection(value: ValueRef<'_>) -> [bool; 4] { +fn verification_integrity_value_projection( + value_type: Option<&str>, + byte_length: Option<i64>, + value: Option<&[u8]>, +) -> [bool; 4] { [ - matches!(value, ValueRef::Text(_)), - matches!(value, ValueRef::Text(bytes) if !bytes.is_empty()), - matches!(value, ValueRef::Text(bytes) if bytes.len() <= MAX_INTEGRITY_RESULT_UTF8_BYTES), - matches!(value, ValueRef::Text(b"ok")), + value_type == Some("text"), + byte_length.is_some_and(|length| length > 0), + byte_length.is_some_and(|length| { + usize::try_from(length).is_ok_and(|length| length <= MAX_INTEGRITY_RESULT_UTF8_BYTES) + }), + value == Some(b"ok"), ] } @@ -858,7 +869,7 @@ fn require_integrity_condition( } #[cfg(any(target_os = "linux", target_os = "macos"))] -fn integrity_source(source: rusqlite::Error) -> ServiceSqliteError { +fn integrity_source(source: sqlx::Error) -> ServiceSqliteError { ServiceSqliteError::with_source(ServiceSqliteErrorKind::Integrity, source) } @@ -868,7 +879,7 @@ fn metadata_error() -> ServiceSqliteError { } #[cfg(any(target_os = "linux", target_os = "macos"))] -fn metadata_source(source: rusqlite::Error) -> ServiceSqliteError { +fn metadata_source(source: sqlx::Error) -> ServiceSqliteError { ServiceSqliteError::with_source(ServiceSqliteErrorKind::Metadata, source) } @@ -882,7 +893,6 @@ mod tests { RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, RuntimeContext, RuntimeContextBootstrap, RuntimeContextSource, }, - rusqlite::params, std::{ collections::BTreeSet, fs, io::Write, os::unix::fs::PermissionsExt, path::Path, process::Command, @@ -890,6 +900,18 @@ mod tests { }; #[cfg(any(target_os = "linux", target_os = "macos"))] + fn open_test_database(path: &Path) -> SqliteConnection { + futures::executor::block_on(SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(path) + .create_if_missing(true) + .foreign_keys(false) + .disable_statement_logging(), + )) + .expect("database") + } + + #[cfg(any(target_os = "linux", target_os = "macos"))] #[test] fn verification_projection_helpers_reject_every_independent_drift() { let digest = [7_u8; 32]; @@ -924,24 +946,27 @@ mod tests { assert!( require_verification_integrity_projection( - verification_integrity_value_projection(ValueRef::Text(b"ok")), + verification_integrity_value_projection(Some("text"), Some(2), Some(b"ok")), false, ) .is_ok() ); - for (value, extra) in [ - (ValueRef::Null, false), - (ValueRef::Text(b""), false), + let oversized = [b'x'; MAX_INTEGRITY_RESULT_UTF8_BYTES + 1]; + for (value_type, byte_length, value, extra) in [ + (None, None, None, false), + (Some("text"), Some(0), Some(b"".as_slice()), false), ( - ValueRef::Text(&[b'x'; MAX_INTEGRITY_RESULT_UTF8_BYTES + 1]), + Some("text"), + i64::try_from(oversized.len()).ok(), + Some(oversized.as_slice()), false, ), - (ValueRef::Text(b"not ok"), false), - (ValueRef::Text(b"ok"), true), + (Some("text"), Some(6), Some(b"not ok".as_slice()), false), + (Some("text"), Some(2), Some(b"ok".as_slice()), true), ] { assert!( require_verification_integrity_projection( - verification_integrity_value_projection(value), + verification_integrity_value_projection(value_type, byte_length, value), extra, ) .is_err() @@ -1120,9 +1145,9 @@ mod tests { #[cfg(any(target_os = "linux", target_os = "macos"))] fn create_database(path: &Path, metadata: &ServiceDatabaseMetadata) { - let connection = Connection::open(path).expect("database"); - connection - .execute_batch( + let mut connection = open_test_database(path); + futures::executor::block_on(async { + sqlx::raw_sql( "CREATE TABLE radroots_service_metadata ( singleton INTEGER PRIMARY KEY, service_id TEXT NOT NULL, @@ -1134,26 +1159,33 @@ mod tests { CREATE TABLE verify_probe (value INTEGER NOT NULL); INSERT INTO verify_probe (value) VALUES (41), (42);", ) + .execute(&mut connection) + .await .expect("schema"); - connection - .execute( + sqlx::query( "INSERT INTO radroots_service_metadata ( singleton, service_id, instance_id, source_generation, state_schema_version, created_at_unix_ms - ) VALUES (1, ?1, ?2, ?3, ?4, ?5)", - params![ - metadata.service().as_str(), - metadata.instance().as_str(), - metadata.source_generation().as_bytes().as_slice(), - i64::from(metadata.state_schema_version().get()), - i64::try_from(metadata.created_at_unix_ms()).expect("creation time"), - ], + ) VALUES (1, ?, ?, ?, ?, ?)", ) + .bind(metadata.service().as_str()) + .bind(metadata.instance().as_str()) + .bind(metadata.source_generation().as_bytes().as_slice()) + .bind(i64::from(metadata.state_schema_version().get())) + .bind(i64::try_from(metadata.created_at_unix_ms()).expect("creation time")) + .execute(&mut connection) + .await .expect("metadata row"); - connection - .pragma_update(None, "application_id", metadata.application_id().get()) - .expect("application ID"); - connection.close().expect("close fixture"); + let application_id = format!( + "PRAGMA application_id = {}", + metadata.application_id().get() + ); + sqlx::query(sqlx::AssertSqlSafe(application_id.as_str())) + .execute(&mut connection) + .await + .expect("application ID"); + connection.close().await.expect("close fixture"); + }); fs::set_permissions(path, fs::Permissions::from_mode(0o600)).expect("state mode"); } @@ -1520,14 +1552,15 @@ mod tests { let state = metadata_fixture .bundle .join(crate::BACKUP_STATE_MEMBER_NAME); - let connection = Connection::open(&state).expect("database"); - connection - .execute( - "UPDATE radroots_service_metadata SET service_id = ?1", - ["x".repeat(129)], - ) - .expect("oversized metadata"); - connection.close().expect("close"); + let mut connection = open_test_database(&state); + futures::executor::block_on(async { + sqlx::query("UPDATE radroots_service_metadata SET service_id = ?") + .bind("x".repeat(129)) + .execute(&mut connection) + .await + .expect("oversized metadata"); + connection.close().await.expect("close"); + }); metadata_fixture.refresh_manifest(); assert_eq!( metadata_fixture @@ -1539,10 +1572,10 @@ mod tests { let mut view_fixture = Fixture::new("metadata-view"); let state = view_fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME); - let connection = Connection::open(&state).expect("database"); + let mut connection = open_test_database(&state); let generation = "09".repeat(32); - connection - .execute_batch(&format!( + futures::executor::block_on(async { + let statement = format!( "DROP TABLE radroots_service_metadata; CREATE VIEW radroots_service_metadata AS SELECT @@ -1552,9 +1585,13 @@ mod tests { X'{generation}' AS source_generation, 1 AS state_schema_version, 1700000000000 AS created_at_unix_ms;" - )) - .expect("metadata view"); - connection.close().expect("close"); + ); + sqlx::raw_sql(sqlx::AssertSqlSafe(statement.as_str())) + .execute(&mut connection) + .await + .expect("metadata view"); + connection.close().await.expect("close"); + }); view_fixture.refresh_manifest(); assert_eq!( view_fixture.verify().expect_err("metadata view").kind(), @@ -1565,9 +1602,9 @@ mod tests { let state = foreign_key_fixture .bundle .join(crate::BACKUP_STATE_MEMBER_NAME); - let connection = Connection::open(&state).expect("database"); - connection - .execute_batch( + let mut connection = open_test_database(&state); + futures::executor::block_on(async { + sqlx::raw_sql( "PRAGMA foreign_keys = OFF; CREATE TABLE parent (id INTEGER PRIMARY KEY); CREATE TABLE child ( @@ -1576,8 +1613,11 @@ mod tests { ); INSERT INTO child (id, parent_id) VALUES (1, 99);", ) + .execute(&mut connection) + .await .expect("foreign-key violation"); - connection.close().expect("close"); + connection.close().await.expect("close"); + }); foreign_key_fixture.refresh_manifest(); assert_eq!( foreign_key_fixture @@ -1589,20 +1629,23 @@ mod tests { let mut corrupt_fixture = Fixture::new("corrupt-sqlite"); let state = corrupt_fixture.bundle.join(crate::BACKUP_STATE_MEMBER_NAME); - let connection = Connection::open(&state).expect("database"); - let page_size: i64 = connection - .pragma_query_value(None, "page_size", |row| row.get(0)) - .expect("page size"); - let page_size = u64::try_from(page_size).expect("positive page size"); - let root_page: i64 = connection - .query_row( + let mut connection = open_test_database(&state); + let (page_size, root_page) = futures::executor::block_on(async { + let page_size = sqlx::query_scalar::<_, i64>("PRAGMA page_size") + .fetch_one(&mut connection) + .await + .expect("page size"); + let root_page = sqlx::query_scalar::<_, i64>( "SELECT rootpage FROM sqlite_schema WHERE name = 'verify_probe'", - [], - |row| row.get(0), ) + .fetch_one(&mut connection) + .await .expect("probe root page"); + connection.close().await.expect("close"); + (page_size, root_page) + }); + let page_size = u64::try_from(page_size).expect("positive page size"); let root_page = u64::try_from(root_page).expect("positive root page"); - connection.close().expect("close"); let corrupt_offset = root_page .checked_sub(1) .and_then(|page| page.checked_mul(page_size)) @@ -1630,47 +1673,66 @@ mod tests { #[cfg(any(target_os = "linux", target_os = "macos"))] #[test] fn sqlite_connection_policy_drift_fails_closed() { - let connection = Connection::open_in_memory().expect("memory database"); - apply_connection_policy(&connection).expect("governed policy"); - verify_connection_policy(&connection).expect("policy readback"); - - connection - .pragma_update(None, "trusted_schema", true) - .expect("drift trusted schema"); - assert_eq!( - verify_connection_policy(&connection) - .expect_err("trusted-schema drift") - .kind(), - ServiceSqliteErrorKind::Integrity - ); + futures::executor::block_on(async { + let mut connection = SqliteConnection::connect("sqlite::memory:") + .await + .expect("memory database"); + apply_connection_policy(&mut connection) + .await + .expect("governed policy"); + verify_connection_policy(&mut connection) + .await + .expect("policy readback"); + + sqlx::query("PRAGMA trusted_schema = ON") + .execute(&mut connection) + .await + .expect("drift trusted schema"); + assert_eq!( + verify_connection_policy(&mut connection) + .await + .expect_err("trusted-schema drift") + .kind(), + ServiceSqliteErrorKind::Integrity + ); - connection - .pragma_update(None, "trusted_schema", false) - .expect("restore trusted schema"); - connection - .pragma_update(None, "query_only", false) - .expect("drift query-only"); - assert_eq!( - verify_connection_policy(&connection) - .expect_err("query-only drift") - .kind(), - ServiceSqliteErrorKind::Integrity - ); + sqlx::query("PRAGMA trusted_schema = OFF") + .execute(&mut connection) + .await + .expect("restore trusted schema"); + sqlx::query("PRAGMA query_only = OFF") + .execute(&mut connection) + .await + .expect("drift query-only"); + assert_eq!( + verify_connection_policy(&mut connection) + .await + .expect_err("query-only drift") + .kind(), + ServiceSqliteErrorKind::Integrity + ); + }); } #[cfg(any(target_os = "linux", target_os = "macos"))] #[test] fn attached_database_and_replaced_bindings_fail_closed() { - let connection = Connection::open_in_memory().expect("memory database"); - connection - .execute("ATTACH DATABASE ':memory:' AS extra", []) - .expect("attach"); - assert_eq!( - verify_database_inventory(&connection) - .expect_err("extra attachment") - .kind(), - ServiceSqliteErrorKind::Integrity - ); + futures::executor::block_on(async { + let mut connection = SqliteConnection::connect("sqlite::memory:") + .await + .expect("memory database"); + sqlx::query("ATTACH DATABASE ':memory:' AS extra") + .execute(&mut connection) + .await + .expect("attach"); + assert_eq!( + verify_database_inventory(&mut connection) + .await + .expect_err("extra attachment") + .kind(), + ServiceSqliteErrorKind::Integrity + ); + }); let fixture = Fixture::new("replace-directory"); let binding = VerifiedBundleBinding::open( @@ -1694,17 +1756,34 @@ mod tests { ) .expect("replacement state mode"); - let retained_connection = - open_sqlite_from_retained_state(&binding).expect("open retained member"); - apply_connection_policy(&retained_connection).expect("retained connection policy"); - verify_database_inventory(&retained_connection).expect("retained database inventory"); - assert_eq!( - verify_database_metadata(&retained_connection, &fixture.manifest, &fixture.identity,) + futures::executor::block_on(async { + let mut retained_connection = open_sqlite_from_retained_state(&binding) + .await + .expect("open retained member"); + apply_connection_policy(&mut retained_connection) + .await + .expect("retained connection policy"); + verify_database_inventory(&mut retained_connection) + .await + .expect("retained database inventory"); + assert_eq!( + verify_database_metadata( + &mut retained_connection, + &fixture.manifest, + &fixture.identity, + ) + .await .expect("retained metadata"), - fixture.metadata - ); - verify_integrity(&retained_connection).expect("retained integrity"); - retained_connection.close().expect("close retained member"); + fixture.metadata + ); + verify_integrity(&mut retained_connection) + .await + .expect("retained integrity"); + retained_connection + .close() + .await + .expect("close retained member"); + }); assert_eq!( binding diff --git a/crates/service_sqlite/src/connection.rs b/crates/service_sqlite/src/connection.rs @@ -2064,19 +2064,19 @@ mod tests { vec![std::ffi::OsString::from("state.sqlite")] ); - let backup = rusqlite::Connection::open_with_flags( - &state, - rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NOFOLLOW, + let mut backup = SqliteConnection::connect_with( + &SqliteConnectOptions::new().filename(&state).read_only(true), ) + .await .expect("open captured database"); assert_eq!( - backup - .query_row("SELECT COUNT(*) FROM host_probe", [], |row| row - .get::<_, i64>(0)) + sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM host_probe") + .fetch_one(&mut backup) + .await .expect("captured row count"), 2 ); - backup.close().expect("close captured database"); + backup.close().await.expect("close captured database"); assert_eq!( fs::read(paths.state_database()).expect("source bytes after capture"), @@ -2352,30 +2352,30 @@ mod tests { .expect("capture remains consistent"); crate::backup::test_capture_reset(); - let backup = rusqlite::Connection::open_with_flags( - stage.join("state.sqlite"), - rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NOFOLLOW, + let mut backup = SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(stage.join("state.sqlite")) + .read_only(true), ) + .await .expect("open backup"); - let updated: i64 = backup - .query_row( - "SELECT COUNT(*) FROM host_probe WHERE value = 1", - [], - |row| row.get(0), - ) - .expect("count transaction projection"); + let updated = + sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM host_probe WHERE value = 1") + .fetch_one(&mut backup) + .await + .expect("count transaction projection"); assert!( updated == 0 || updated == 2, "backup must not tear a transaction" ); assert_eq!( - backup - .query_row("PRAGMA integrity_check(1)", [], |row| row - .get::<_, String>(0)) + sqlx::query_scalar::<_, String>("PRAGMA integrity_check(1)") + .fetch_one(&mut backup) + .await .expect("backup integrity"), "ok" ); - backup.close().expect("close backup"); + backup.close().await.expect("close backup"); host.close().await.expect("close writer host"); } diff --git a/crates/service_sqlite/src/open.rs b/crates/service_sqlite/src/open.rs @@ -317,10 +317,6 @@ impl BackupSourceValidator { .validate_for(&self.paths)?; self.binding.validate(&self.paths) } - - pub(crate) fn database_path(&self) -> &Path { - self.paths.state_database() - } } #[cfg(any(target_os = "linux", target_os = "macos"))] diff --git a/crates/service_sqlite/src/restore/process_tests.rs b/crates/service_sqlite/src/restore/process_tests.rs @@ -80,14 +80,23 @@ impl Fixture { .release() .expect("release initialization authority"); { - let connection = rusqlite::Connection::open(paths.state_database()) - .expect("open live database for WAL posture"); - connection - .pragma_update(None, "journal_mode", "WAL") + let mut connection = sqlx::SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(paths.state_database()) + .create_if_missing(false) + .disable_statement_logging(), + ) + .await + .expect("open live database for WAL posture"); + sqlx::query("PRAGMA journal_mode = WAL") + .execute(&mut connection) + .await .expect("set WAL posture"); - connection - .pragma_update(None, "wal_checkpoint", "TRUNCATE") + sqlx::query("PRAGMA wal_checkpoint(TRUNCATE)") + .execute(&mut connection) + .await .expect("checkpoint WAL posture"); + connection.close().await.expect("close live database"); } let bundle = root.path().join(BUNDLE_DIRECTORY_NAME); @@ -99,13 +108,24 @@ impl Fixture { fs::set_permissions(&member, fs::Permissions::from_mode(0o600)) .expect("restrict process member"); { - let connection = rusqlite::Connection::open(&member).expect("open process member"); - connection - .pragma_update(None, "user_version", REPLACEMENT_USER_VERSION) + let mut connection = sqlx::SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(&member) + .create_if_missing(false) + .disable_statement_logging(), + ) + .await + .expect("open process member"); + let user_version = format!("PRAGMA user_version = {REPLACEMENT_USER_VERSION}"); + sqlx::query(sqlx::AssertSqlSafe(user_version.as_str())) + .execute(&mut connection) + .await .expect("set replacement probe"); - connection - .pragma_update(None, "wal_checkpoint", "TRUNCATE") + sqlx::query("PRAGMA wal_checkpoint(TRUNCATE)") + .execute(&mut connection) + .await .expect("checkpoint replacement probe"); + connection.close().await.expect("close process member"); } let bytes = fs::read(&member).expect("read process member"); let manifest = ServiceBackupManifest::from_capture( @@ -264,7 +284,10 @@ async fn run_parent_scenario(scenario: Scenario) { assert_eq!(outcome.applied_count(), 0); host.close().await.expect("close recovered host"); assert_no_recovery_evidence(&fixture.paths); - assert_eq!(database_user_version(&fixture.paths), expected_user_version); + assert_eq!( + database_user_version(&fixture.paths).await, + expected_user_version + ); assert_live_permissions(&fixture.paths); } else { let staged = recovery_path(&fixture.paths, STAGED_FILE_NAME); @@ -461,15 +484,22 @@ fn assert_no_recovery_evidence(paths: &ServiceSqlitePaths) { } } -fn database_user_version(paths: &ServiceSqlitePaths) -> i64 { - let connection = rusqlite::Connection::open_with_flags( - paths.state_database(), - rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX, +async fn database_user_version(paths: &ServiceSqlitePaths) -> i64 { + let mut connection = sqlx::SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(paths.state_database()) + .read_only(true) + .create_if_missing(false) + .disable_statement_logging(), ) + .await .expect("open recovered database"); - connection - .pragma_query_value(None, "user_version", |row| row.get(0)) - .expect("read recovery probe") + let version = sqlx::query_scalar::<_, i64>("PRAGMA user_version") + .fetch_one(&mut connection) + .await + .expect("read recovery probe"); + connection.close().await.expect("close recovered database"); + version } #[derive(Debug, PartialEq, Eq)] diff --git a/crates/service_sqlite/src/restore/stage.rs b/crates/service_sqlite/src/restore/stage.rs @@ -1618,14 +1618,23 @@ mod tests { .release() .expect("release initialization authority"); { - let connection = rusqlite::Connection::open(paths.state_database()) - .expect("open live database for WAL posture"); - connection - .pragma_update(None, "journal_mode", "WAL") + let mut connection = SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(paths.state_database()) + .create_if_missing(false) + .disable_statement_logging(), + ) + .await + .expect("open live database for WAL posture"); + sqlx::query("PRAGMA journal_mode = WAL") + .execute(&mut connection) + .await .expect("set WAL posture"); - connection - .pragma_update(None, "wal_checkpoint", "TRUNCATE") + sqlx::query("PRAGMA wal_checkpoint(TRUNCATE)") + .execute(&mut connection) + .await .expect("checkpoint WAL posture"); + connection.close().await.expect("close live database"); } let bundle = root.path().join("verified-bundle"); @@ -2428,13 +2437,19 @@ mod tests { async fn schema_catalog_and_migration_ledger_drift_are_rejected() { let mut schema_drift = Fixture::new().await; { - let connection = rusqlite::Connection::open( - schema_drift.bundle.join(crate::BACKUP_STATE_MEMBER_NAME), + let mut connection = SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(schema_drift.bundle.join(crate::BACKUP_STATE_MEMBER_NAME)) + .create_if_missing(false) + .disable_statement_logging(), ) + .await .expect("open bundle"); - connection - .execute("CREATE TABLE unexpected (id INTEGER PRIMARY KEY)", []) + sqlx::query("CREATE TABLE unexpected (id INTEGER PRIMARY KEY)") + .execute(&mut connection) + .await .expect("add unexpected table"); + connection.close().await.expect("close bundle"); } schema_drift.refresh_manifest(); let error = stage_verified_restore( @@ -2451,13 +2466,16 @@ mod tests { let mut ledger_drift = Fixture::new().await; { - let connection = rusqlite::Connection::open( - ledger_drift.bundle.join(crate::BACKUP_STATE_MEMBER_NAME), + let mut connection = SqliteConnection::connect_with( + &SqliteConnectOptions::new() + .filename(ledger_drift.bundle.join(crate::BACKUP_STATE_MEMBER_NAME)) + .create_if_missing(false) + .disable_statement_logging(), ) + .await .expect("open bundle"); - connection - .execute( - "INSERT INTO schema_migrations ( + sqlx::query( + "INSERT INTO schema_migrations ( version, name, checksum, applied_at_unix_s, service_version, service_commit, lib_revision, rust_version, target, feature_profile, config_contract_version, @@ -2469,9 +2487,11 @@ mod tests { '89abcdef0123456789abcdef0123456789abcdef', '1.97.1', 'x86_64-unknown-linux-gnu', 'test', 1, 1, 1, 1, 1 )", - [], - ) - .expect("insert ledger drift"); + ) + .execute(&mut connection) + .await + .expect("insert ledger drift"); + connection.close().await.expect("close bundle"); } ledger_drift.refresh_manifest(); let error = stage_verified_restore( diff --git a/crates/service_sqlite/tests/package_boundary.rs b/crates/service_sqlite/tests/package_boundary.rs @@ -59,7 +59,6 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() { "libsqlite3-sys", "radroots_runtime_paths", "radroots_storage", - "rusqlite", "rustix", "serde", "serde_json", @@ -937,9 +936,8 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() { "Mode::RUSR | Mode::WUSR | Mode::XUSR", "Mode::RUSR | Mode::WUSR", "PRAGMA integrity_check(1)", - "ValueRef::Text", "MAX_INTEGRITY_RESULT_UTF8_BYTES", - "PRAGMA foreign_key_check", + "FROM pragma_foreign_key_check", "BackupSourceValidator", "PoolConnection<Sqlite>", "CaptureCancellation", @@ -1008,15 +1006,16 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() { "open_sqlite_from_retained_state", "/proc/self/fd/{descriptor}", "/dev/fd/{descriptor}", - "?mode=ro&immutable=1", - "pragma_update(None, \"query_only\", true)", - "pragma_update(None, \"trusted_schema\", false)", + ".filename(descriptor_path)", + ".immutable(true)", + "PRAGMA query_only = ON", + "PRAGMA trusted_schema = OFF", "verify_connection_policy", - "PRAGMA database_list", + "FROM pragma_database_list", "FROM main.sqlite_schema", - "ValueRef::Text(b\"table\")", + "Some(\"table\")", "PRAGMA integrity_check(1)", - "PRAGMA foreign_key_check", + "FROM pragma_foreign_key_check", "state_schema_version() <= expected.supported_state_schema_version()", "binding.hash_state(maximum_state_bytes)", ] { diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs @@ -4162,11 +4162,8 @@ fn validate_sqlite_runtime_contract(workspace_root: &Path) -> Result<(), String> const FORBIDDEN_HIGH_LEVEL_DEPENDENCIES: [&str; 6] = [ "diesel", "refinery", "rusqlite", "sea-orm", "sqlite", "sqlite3", ]; - const TEMPORARY_DIRECT_DEPENDENCIES: [&str; 3] = [ - "radroots_geonames:rusqlite", - "radroots_service_sqlite:rusqlite", - "workspace:rusqlite", - ]; + const TEMPORARY_DIRECT_DEPENDENCIES: [&str; 2] = + ["radroots_geonames:rusqlite", "workspace:rusqlite"]; let contract = parse_toml::<SqliteRuntimeContract>( &workspace_root.join(SQLITE_RUNTIME_CONTRACT_RELATIVE),