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