lib

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

commit ad17b7d3455a7147cfa303d976fc5c70c3a4c0cb
parent 053d0c750bf9cd683c6ea37cefe7e79617ba629f
Author: triesap <tyson@radroots.org>
Date:   Thu, 27 Aug 2026 22:50:27 +0000

service-sqlite: bind atomic open metadata

Diffstat:
Mcontracts/api_baselines/radroots_service_sqlite.txt | 20++++++++++----------
Mcrates/service_sqlite/README.md | 7+++++--
Mcrates/service_sqlite/src/connection.rs | 67+++++++++++++++++++++++++++++++++++++++++++++----------------------
Mcrates/service_sqlite/src/lib.rs | 2+-
Mcrates/service_sqlite/tests/package_boundary.rs | 4++--
5 files changed, 63 insertions(+), 37 deletions(-)

diff --git a/contracts/api_baselines/radroots_service_sqlite.txt b/contracts/api_baselines/radroots_service_sqlite.txt @@ -258,13 +258,13 @@ pub const fn radroots_service_sqlite::MinimumFreeBytes::get(self) -> u64 pub const fn radroots_service_sqlite::MinimumFreeBytes::new(u64) -> core::result::Result<Self, radroots_service_sqlite::StateFilesystemCapacityError> impl<'de> serde_core::de::Deserialize<'de> for radroots_service_sqlite::MinimumFreeBytes pub fn radroots_service_sqlite::MinimumFreeBytes::deserialize<D>(D) -> core::result::Result<Self, <D as serde_core::de::Deserializer>::Error> where D: serde_core::de::Deserializer<'de> -pub struct radroots_service_sqlite::OpenedExistingServiceDatabase -impl radroots_service_sqlite::OpenedExistingServiceDatabase -pub const fn radroots_service_sqlite::OpenedExistingServiceDatabase::database_metadata(&self) -> &radroots_service_sqlite::ServiceDatabaseMetadata -pub const fn radroots_service_sqlite::OpenedExistingServiceDatabase::host(&self) -> &radroots_service_sqlite::ServiceSqliteHost -pub fn radroots_service_sqlite::OpenedExistingServiceDatabase::into_parts(self) -> (radroots_service_sqlite::ServiceSqliteHost, radroots_service_sqlite::ServiceDatabaseMetadata) -impl core::fmt::Debug for radroots_service_sqlite::OpenedExistingServiceDatabase -pub fn radroots_service_sqlite::OpenedExistingServiceDatabase::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result +pub struct radroots_service_sqlite::OpenedServiceDatabase +impl radroots_service_sqlite::OpenedServiceDatabase +pub const fn radroots_service_sqlite::OpenedServiceDatabase::database_metadata(&self) -> &radroots_service_sqlite::ServiceDatabaseMetadata +pub const fn radroots_service_sqlite::OpenedServiceDatabase::host(&self) -> &radroots_service_sqlite::ServiceSqliteHost +pub fn radroots_service_sqlite::OpenedServiceDatabase::into_parts(self) -> (radroots_service_sqlite::ServiceSqliteHost, radroots_service_sqlite::ServiceDatabaseMetadata) +impl core::fmt::Debug for radroots_service_sqlite::OpenedServiceDatabase +pub fn radroots_service_sqlite::OpenedServiceDatabase::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result pub struct radroots_service_sqlite::PlatformStateFilesystemCapacitySource impl radroots_service_sqlite::StateFilesystemCapacitySource for radroots_service_sqlite::PlatformStateFilesystemCapacitySource pub fn radroots_service_sqlite::PlatformStateFilesystemCapacitySource::available_bytes(&self, &radroots_service_sqlite::ServiceSqlitePaths) -> core::result::Result<u64, radroots_service_sqlite::StateFilesystemCapacityError> @@ -383,11 +383,11 @@ pub async fn radroots_service_sqlite::ServiceSqliteHost::close(&self) -> core::r pub async fn radroots_service_sqlite::ServiceSqliteHost::inspect_integrity(&self, radroots_service_sqlite::IntegrityCheckedAtUnixMs) -> core::result::Result<radroots_service_sqlite::ServiceSqliteIntegrityReport, radroots_service_sqlite::ServiceSqliteError> pub const fn radroots_service_sqlite::ServiceSqliteHost::mode(&self) -> radroots_service_sqlite::OpenMode pub async fn radroots_service_sqlite::ServiceSqliteHost::open_initialized(&radroots_service_sqlite::ServiceSqlitePaths, &radroots_service_sqlite::ServiceDatabaseIdentity, &radroots_service_sqlite::MigrationCatalog, &radroots_service_sqlite::SchemaCatalog, radroots_service_sqlite::ServiceSqliteConnectionOptions, radroots_service_sqlite::WriterAuthority, radroots_service_sqlite::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::MigrationBuildIdentity, &[radroots_service_sqlite::MigrationCallbackBinding]) -> core::result::Result<(Self, radroots_service_sqlite::MigrationApplicationOutcome), radroots_service_sqlite::ServiceSqliteError> -pub async fn radroots_service_sqlite::ServiceSqliteHost::open_or_initialize<F, E>(&radroots_service_sqlite::ServiceSqlitePaths, &radroots_service_sqlite::ServiceDatabaseMetadata, &radroots_service_sqlite::MigrationCatalog, &radroots_service_sqlite::SchemaCatalog, radroots_service_sqlite::ServiceSqliteConnectionOptions, radroots_service_sqlite::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::MigrationBuildIdentity, &[radroots_service_sqlite::MigrationCallbackBinding], F) -> core::result::Result<(Self, radroots_service_sqlite::MigrationApplicationOutcome), radroots_service_sqlite::ServiceSqliteError> where F: for<'a> core::ops::function::FnOnce(&'a mut radroots_service_sqlite::ServiceSqliteInitializer<'_>) -> radroots_service_sqlite::ServiceSqliteInitializerFuture<'a, E>, E: core::error::Error + core::marker::Send + core::marker::Sync + 'static +pub async fn radroots_service_sqlite::ServiceSqliteHost::open_or_initialize<F, E>(&radroots_service_sqlite::ServiceSqlitePaths, &radroots_service_sqlite::ServiceDatabaseMetadata, &radroots_service_sqlite::MigrationCatalog, &radroots_service_sqlite::SchemaCatalog, radroots_service_sqlite::ServiceSqliteConnectionOptions, radroots_service_sqlite::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::MigrationBuildIdentity, &[radroots_service_sqlite::MigrationCallbackBinding], F) -> core::result::Result<(radroots_service_sqlite::OpenedServiceDatabase, radroots_service_sqlite::MigrationApplicationOutcome), radroots_service_sqlite::ServiceSqliteError> where F: for<'a> core::ops::function::FnOnce(&'a mut radroots_service_sqlite::ServiceSqliteInitializer<'_>) -> radroots_service_sqlite::ServiceSqliteInitializerFuture<'a, E>, E: core::error::Error + core::marker::Send + core::marker::Sync + 'static pub async fn radroots_service_sqlite::ServiceSqliteHost::open_read_only_inspection(&radroots_service_sqlite::ServiceSqlitePaths, &radroots_service_sqlite::ServiceDatabaseIdentity, &radroots_service_sqlite::MigrationCatalog, &radroots_service_sqlite::SchemaCatalog, radroots_service_sqlite::ServiceSqliteConnectionOptions) -> core::result::Result<Self, radroots_service_sqlite::ServiceSqliteError> -pub async fn radroots_service_sqlite::ServiceSqliteHost::open_read_only_inspection_with_intent(&radroots_service_sqlite::ServiceSqlitePaths, &radroots_service_sqlite::ExistingServiceDatabaseIntent, &radroots_service_sqlite::MigrationCatalog, &radroots_service_sqlite::SchemaCatalog, radroots_service_sqlite::ServiceSqliteConnectionOptions) -> core::result::Result<radroots_service_sqlite::OpenedExistingServiceDatabase, radroots_service_sqlite::ServiceSqliteError> +pub async fn radroots_service_sqlite::ServiceSqliteHost::open_read_only_inspection_with_intent(&radroots_service_sqlite::ServiceSqlitePaths, &radroots_service_sqlite::ExistingServiceDatabaseIntent, &radroots_service_sqlite::MigrationCatalog, &radroots_service_sqlite::SchemaCatalog, radroots_service_sqlite::ServiceSqliteConnectionOptions) -> core::result::Result<radroots_service_sqlite::OpenedServiceDatabase, radroots_service_sqlite::ServiceSqliteError> pub async fn radroots_service_sqlite::ServiceSqliteHost::open_read_write_existing(&radroots_service_sqlite::ServiceSqlitePaths, &radroots_service_sqlite::ServiceDatabaseIdentity, &radroots_service_sqlite::MigrationCatalog, &radroots_service_sqlite::SchemaCatalog, radroots_service_sqlite::ServiceSqliteConnectionOptions, radroots_service_sqlite::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::MigrationBuildIdentity, &[radroots_service_sqlite::MigrationCallbackBinding]) -> core::result::Result<(Self, radroots_service_sqlite::MigrationApplicationOutcome), radroots_service_sqlite::ServiceSqliteError> -pub async fn radroots_service_sqlite::ServiceSqliteHost::open_read_write_existing_with_intent(&radroots_service_sqlite::ServiceSqlitePaths, &radroots_service_sqlite::ExistingServiceDatabaseIntent, &radroots_service_sqlite::MigrationCatalog, &radroots_service_sqlite::SchemaCatalog, radroots_service_sqlite::ServiceSqliteConnectionOptions, radroots_service_sqlite::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::MigrationBuildIdentity, &[radroots_service_sqlite::MigrationCallbackBinding]) -> core::result::Result<(radroots_service_sqlite::OpenedExistingServiceDatabase, radroots_service_sqlite::MigrationApplicationOutcome), radroots_service_sqlite::ServiceSqliteError> +pub async fn radroots_service_sqlite::ServiceSqliteHost::open_read_write_existing_with_intent(&radroots_service_sqlite::ServiceSqlitePaths, &radroots_service_sqlite::ExistingServiceDatabaseIntent, &radroots_service_sqlite::MigrationCatalog, &radroots_service_sqlite::SchemaCatalog, radroots_service_sqlite::ServiceSqliteConnectionOptions, radroots_service_sqlite::MigrationAppliedAtUnixSeconds, &radroots_service_sqlite::MigrationBuildIdentity, &[radroots_service_sqlite::MigrationCallbackBinding]) -> core::result::Result<(radroots_service_sqlite::OpenedServiceDatabase, radroots_service_sqlite::MigrationApplicationOutcome), radroots_service_sqlite::ServiceSqliteError> pub async fn radroots_service_sqlite::ServiceSqliteHost::transaction<T, E, F>(&self, F) -> core::result::Result<T, radroots_service_sqlite::ServiceSqliteTransactionError<E>> where T: core::marker::Send + 'static, E: core::marker::Send + 'static, F: for<'a> core::ops::function::FnOnce(&'a mut radroots_service_sqlite::ServiceSqliteTransaction<'_>) -> radroots_service_sqlite::ServiceSqliteTransactionFuture<'a, T, E> + core::marker::Send impl core::fmt::Debug for radroots_service_sqlite::ServiceSqliteHost pub fn radroots_service_sqlite::ServiceSqliteHost::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result diff --git a/crates/service_sqlite/README.md b/crates/service_sqlite/README.md @@ -32,7 +32,10 @@ writer authority and an exclusive create decide whether the sealed initializer runs or the exact existing database is opened. The existing branch never runs the initializer. Callers therefore do not use pathname probes, error-text matching, recursive directory creation, permission repair, or direct SQLx -connections to choose the bootstrap branch. +connections to choose the bootstrap branch. Success returns an +`OpenedServiceDatabase` that binds the retained host to the actual verified +metadata from the selected branch, including the persisted source generation +when state already existed. Existing databases can be admitted without a caller guessing their stored source generation. `ExistingServiceDatabaseIntent` seals the canonical @@ -40,7 +43,7 @@ service and instance, supported schema ceiling, and SQLite application ID; `ServiceSqliteHost::open_read_write_existing_with_intent` and `ServiceSqliteHost::open_read_only_inspection_with_intent` discover and verify the actual immutable metadata while retaining the corresponding writer or -inspection authority. Success returns an `OpenedExistingServiceDatabase`, +inspection authority. Success returns an `OpenedServiceDatabase`, which keeps the host and verified metadata inseparable until the caller consumes them together. Recovery remains fail closed and may use this intent only to discover the marker-bound generation; every other identity dimension, diff --git a/crates/service_sqlite/src/connection.rs b/crates/service_sqlite/src/connection.rs @@ -56,23 +56,23 @@ pub struct ServiceSqliteHost { failpoints: crate::failpoint::DurabilityFailpoints, } -/// Existing database opened under retained authority with its verified metadata. +/// Database opened under retained authority with its verified metadata. /// /// This result cannot be assembled independently from a host and metadata: /// /// ```compile_fail -/// use radroots_service_sqlite::{OpenedExistingServiceDatabase, ServiceSqliteHost}; +/// use radroots_service_sqlite::{OpenedServiceDatabase, ServiceSqliteHost}; /// /// fn forge(host: ServiceSqliteHost) { -/// let _ = OpenedExistingServiceDatabase { host }; +/// let _ = OpenedServiceDatabase { host }; /// } /// ``` -pub struct OpenedExistingServiceDatabase { +pub struct OpenedServiceDatabase { host: ServiceSqliteHost, metadata: ServiceDatabaseMetadata, } -impl OpenedExistingServiceDatabase { +impl OpenedServiceDatabase { fn new(host: ServiceSqliteHost, metadata: ServiceDatabaseMetadata) -> Self { Self { host, metadata } } @@ -96,10 +96,10 @@ impl OpenedExistingServiceDatabase { } } -impl fmt::Debug for OpenedExistingServiceDatabase { +impl fmt::Debug for OpenedServiceDatabase { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter - .debug_struct("OpenedExistingServiceDatabase") + .debug_struct("OpenedServiceDatabase") .field("mode", &self.host.mode()) .field("database_metadata", &"[redacted]") .finish() @@ -312,8 +312,7 @@ impl ServiceSqliteHost { applied_at: MigrationAppliedAtUnixSeconds, build: &MigrationBuildIdentity, callbacks: &[MigrationCallbackBinding], - ) -> Result<(OpenedExistingServiceDatabase, MigrationApplicationOutcome), ServiceSqliteError> - { + ) -> Result<(OpenedServiceDatabase, MigrationApplicationOutcome), ServiceSqliteError> { #[cfg(any(target_os = "linux", target_os = "macos"))] { let pool = crate::open::open_existing_connection_pool_with_intent( @@ -340,7 +339,7 @@ impl ServiceSqliteHost { } }; let host = Self::from_pool(OpenMode::ReadWriteExisting, pool); - Ok((OpenedExistingServiceDatabase::new(host, metadata), outcome)) + Ok((OpenedServiceDatabase::new(host, metadata), outcome)) } #[cfg(not(any(target_os = "linux", target_os = "macos")))] { @@ -359,7 +358,8 @@ impl ServiceSqliteHost { /// and schema-catalog verification commit together before the host opens and /// applies governed migrations. On exact create collision, the same retained /// authority is transferred to an existing-only open and the initialization - /// callback is never invoked. + /// callback is never invoked. Success binds the retained host to the actual + /// verified metadata selected by that atomic decision. #[allow(clippy::too_many_arguments)] pub async fn open_or_initialize<F, E>( paths: &ServiceSqlitePaths, @@ -371,7 +371,7 @@ impl ServiceSqliteHost { build: &MigrationBuildIdentity, callbacks: &[MigrationCallbackBinding], initialize_schema: F, - ) -> Result<(Self, MigrationApplicationOutcome), ServiceSqliteError> + ) -> Result<(OpenedServiceDatabase, MigrationApplicationOutcome), ServiceSqliteError> where F: for<'a> FnOnce( &'a mut crate::ServiceSqliteInitializer<'_>, @@ -420,13 +420,22 @@ impl ServiceSqliteHost { (OpenMode::ReadWriteExisting, pool) } }; - match pool.apply_migrations(applied_at, build, callbacks).await { - Ok(outcome) => Ok((Self::from_pool(mode, pool), outcome)), + let outcome = match pool.apply_migrations(applied_at, build, callbacks).await { + Ok(outcome) => outcome, Err(error) => { drop(pool.close().await); - Err(error) + return Err(error); } - } + }; + let metadata = match pool.database_metadata().await { + Ok(metadata) => metadata, + Err(error) => { + drop(pool.close().await); + return Err(error); + } + }; + let host = Self::from_pool(mode, pool); + Ok((OpenedServiceDatabase::new(host, metadata), outcome)) } #[cfg(not(any(target_os = "linux", target_os = "macos")))] { @@ -517,7 +526,7 @@ impl ServiceSqliteHost { migrations: &MigrationCatalog, schema: &SchemaCatalog, options: ServiceSqliteConnectionOptions, - ) -> Result<OpenedExistingServiceDatabase, ServiceSqliteError> { + ) -> Result<OpenedServiceDatabase, ServiceSqliteError> { #[cfg(any(target_os = "linux", target_os = "macos"))] { let pool = crate::open::open_existing_connection_pool_with_intent( @@ -537,7 +546,7 @@ impl ServiceSqliteHost { } }; let host = Self::from_pool(OpenMode::ReadOnlyInspection, pool); - Ok(OpenedExistingServiceDatabase::new(host, metadata)) + Ok(OpenedServiceDatabase::new(host, metadata)) } #[cfg(not(any(target_os = "linux", target_os = "macos")))] { @@ -1728,7 +1737,7 @@ mod tests { ); assert_eq!(opened.host().mode(), OpenMode::ReadWriteExisting); let debug = format!("{opened:?}"); - assert!(debug.contains("OpenedExistingServiceDatabase")); + assert!(debug.contains("OpenedServiceDatabase")); assert!(!debug.contains("09090909")); assert!(WriterAuthority::acquire(&paths, OpenMode::ReadWriteExisting).is_err()); let (writable, actual) = opened.into_parts(); @@ -1796,15 +1805,26 @@ mod tests { .await .expect("initialize missing state"); assert!(initialized_callback.load(Ordering::Acquire)); - assert_eq!(initialized.mode(), OpenMode::Initialize); + assert_eq!(initialized.host().mode(), OpenMode::Initialize); + assert_eq!(initialized.database_metadata(), &metadata); assert_eq!(outcome.applied_count(), 0); + let (initialized, initialized_metadata) = initialized.into_parts(); + assert_eq!(initialized_metadata, metadata); initialized.close().await.expect("close initialized host"); let existing_callback = Arc::new(AtomicBool::new(false)); let called = Arc::clone(&existing_callback); + let alternative_metadata = ServiceDatabaseMetadata::new( + &paths, + SourceGeneration::new([32; 32]).expect("alternative source generation"), + NonZeroU32::new(1).expect("schema version"), + 1_700_000_000_001, + crate::ServiceSqliteApplicationId::new(0x5244_5351).expect("application ID"), + ) + .expect("alternative database metadata"); let (existing, outcome) = ServiceSqliteHost::open_or_initialize( &paths, - &metadata, + &alternative_metadata, &migrations, &schema, ServiceSqliteConnectionOptions::reviewed(), @@ -1819,8 +1839,11 @@ mod tests { .await .expect("open exact existing state"); assert!(!existing_callback.load(Ordering::Acquire)); - assert_eq!(existing.mode(), OpenMode::ReadWriteExisting); + assert_eq!(existing.host().mode(), OpenMode::ReadWriteExisting); + assert_eq!(existing.database_metadata(), &metadata); assert_eq!(outcome.applied_count(), 0); + let (existing, existing_metadata) = existing.into_parts(); + assert_eq!(existing_metadata, metadata); assert_eq!(row_count(&existing).await, 0); existing.close().await.expect("close existing host"); } diff --git a/crates/service_sqlite/src/lib.rs b/crates/service_sqlite/src/lib.rs @@ -55,7 +55,7 @@ pub use backup::{ }; pub use config::{ServiceSqliteConnectionOptions, ServiceSqliteConnectionOptionsError}; pub use connection::{ - OpenedExistingServiceDatabase, ServiceSqliteHost, ServiceSqliteTransaction, + OpenedServiceDatabase, ServiceSqliteHost, ServiceSqliteTransaction, ServiceSqliteTransactionError, ServiceSqliteTransactionErrorKind, ServiceSqliteTransactionFuture, }; diff --git a/crates/service_sqlite/tests/package_boundary.rs b/crates/service_sqlite/tests/package_boundary.rs @@ -173,7 +173,7 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() { "Existing databases can be admitted without a caller guessing their stored source generation", "`ExistingServiceDatabaseIntent` seals the canonical service and instance, supported schema ceiling, and SQLite application ID", "discover and verify the actual immutable metadata while retaining the corresponding writer or inspection authority", - "Success returns an `OpenedExistingServiceDatabase`", + "Success returns an `OpenedServiceDatabase`", "keeps the host and verified metadata inseparable until the caller consumes them together", "Recovery remains fail closed", "with raw database authority", @@ -403,7 +403,7 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() { "pub struct radroots_service_sqlite::ServiceSqliteTransaction", "pub struct radroots_service_sqlite::ServiceSqlitePaths", "pub struct radroots_service_sqlite::ExistingServiceDatabaseIntent", - "pub struct radroots_service_sqlite::OpenedExistingServiceDatabase", + "pub struct radroots_service_sqlite::OpenedServiceDatabase", "pub struct radroots_service_sqlite::MigrationCatalog", "pub struct radroots_service_sqlite::SchemaCatalog", "pub struct radroots_service_sqlite::VerifiedServiceBackup",