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