commit a646d918daedbf60e17d90d77c045d127759a243
parent b44119fbac5985be8127ad1bf56d2950e6399427
Author: triesap <tyson@radroots.org>
Date: Sun, 23 Aug 2026 05:10:05 +0000
service-sqlite: bind existing opens to stored metadata
- add a sealed existing-database intent without caller-supplied generation
- retain writer or inspection authority with the verified actual metadata
- preserve exact recovery, migration, schema, and redaction boundaries
- verify unit, integration, package, API, Clippy, doctest, and Rustdoc gates
Diffstat:
10 files changed, 686 insertions(+), 31 deletions(-)
diff --git a/contracts/api_baselines/radroots_service_sqlite.txt b/contracts/api_baselines/radroots_service_sqlite.txt
@@ -176,6 +176,15 @@ pub const fn radroots_service_sqlite::BackupMemberSha256::as_bytes(&self) -> &[u
pub const fn radroots_service_sqlite::BackupMemberSha256::from_bytes([u8; 32]) -> Self
impl core::fmt::Debug for radroots_service_sqlite::BackupMemberSha256
pub fn radroots_service_sqlite::BackupMemberSha256::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
+pub struct radroots_service_sqlite::ExistingServiceDatabaseIntent
+impl radroots_service_sqlite::ExistingServiceDatabaseIntent
+pub const fn radroots_service_sqlite::ExistingServiceDatabaseIntent::application_id(&self) -> radroots_service_sqlite::ServiceSqliteApplicationId
+pub fn radroots_service_sqlite::ExistingServiceDatabaseIntent::instance(&self) -> &radroots_runtime_paths::identifier::InstanceId
+pub fn radroots_service_sqlite::ExistingServiceDatabaseIntent::new(&radroots_service_sqlite::ServiceSqlitePaths, core::num::nonzero::NonZeroU32, radroots_service_sqlite::ServiceSqliteApplicationId) -> Self
+pub fn radroots_service_sqlite::ExistingServiceDatabaseIntent::service(&self) -> &radroots_runtime_paths::identifier::ServiceId
+pub const fn radroots_service_sqlite::ExistingServiceDatabaseIntent::supported_state_schema_version(&self) -> core::num::nonzero::NonZeroU32
+impl core::fmt::Debug for radroots_service_sqlite::ExistingServiceDatabaseIntent
+pub fn radroots_service_sqlite::ExistingServiceDatabaseIntent::fmt(&self, &mut core::fmt::Formatter<'_>) -> core::fmt::Result
pub struct radroots_service_sqlite::IntegrityCheckedAtUnixMs(_)
impl radroots_service_sqlite::IntegrityCheckedAtUnixMs
pub const fn radroots_service_sqlite::IntegrityCheckedAtUnixMs::get(self) -> u64
@@ -249,6 +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::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>
@@ -368,7 +384,9 @@ pub async fn radroots_service_sqlite::ServiceSqliteHost::inspect_integrity(&self
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_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_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::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
@@ -15,6 +15,18 @@ runner-owned. Writable host opening finishes every pending governed migration
before returning, and read-only inspection opens only current migration and
schema state.
+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;
+`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`,
+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,
+artifact binding, migration prefix, and schema catalog remains governed.
+
Service-controlled SQL is screened before SQLite compilation through both the
borrowed transaction executor and migration callback executor. The closed
statement-control inventory is `PRAGMA`, `ATTACH`, `DETACH`, `BEGIN`, `COMMIT`,
diff --git a/crates/service_sqlite/src/connection.rs b/crates/service_sqlite/src/connection.rs
@@ -19,10 +19,11 @@ use sqlx::{
};
use crate::{
- MigrationApplicationOutcome, MigrationAppliedAtUnixSeconds, MigrationBuildIdentity,
- MigrationCallbackBinding, MigrationCatalog, OpenMode, SchemaCatalog, ServiceDatabaseIdentity,
- ServiceSqliteConnectionOptions, ServiceSqliteError, ServiceSqliteErrorKind,
- ServiceSqliteIntegrityReport, ServiceSqlitePaths, WriterAuthority,
+ ExistingServiceDatabaseIntent, MigrationApplicationOutcome, MigrationAppliedAtUnixSeconds,
+ MigrationBuildIdentity, MigrationCallbackBinding, MigrationCatalog, OpenMode, SchemaCatalog,
+ ServiceDatabaseIdentity, ServiceDatabaseMetadata, ServiceSqliteConnectionOptions,
+ ServiceSqliteError, ServiceSqliteErrorKind, ServiceSqliteIntegrityReport, ServiceSqlitePaths,
+ WriterAuthority,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
@@ -55,6 +56,56 @@ pub struct ServiceSqliteHost {
failpoints: crate::failpoint::DurabilityFailpoints,
}
+/// Existing 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};
+///
+/// fn forge(host: ServiceSqliteHost) {
+/// let _ = OpenedExistingServiceDatabase { host };
+/// }
+/// ```
+pub struct OpenedExistingServiceDatabase {
+ host: ServiceSqliteHost,
+ metadata: ServiceDatabaseMetadata,
+}
+
+impl OpenedExistingServiceDatabase {
+ fn new(host: ServiceSqliteHost, metadata: ServiceDatabaseMetadata) -> Self {
+ Self { host, metadata }
+ }
+
+ /// Borrows the authority-retaining host.
+ #[must_use]
+ pub const fn host(&self) -> &ServiceSqliteHost {
+ &self.host
+ }
+
+ /// Borrows the metadata discovered and verified by the retained open.
+ #[must_use]
+ pub const fn database_metadata(&self) -> &ServiceDatabaseMetadata {
+ &self.metadata
+ }
+
+ /// Consumes the binding into the authority-retaining host and actual metadata.
+ #[must_use]
+ pub fn into_parts(self) -> (ServiceSqliteHost, ServiceDatabaseMetadata) {
+ (self.host, self.metadata)
+ }
+}
+
+impl fmt::Debug for OpenedExistingServiceDatabase {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("OpenedExistingServiceDatabase")
+ .field("mode", &self.host.mode())
+ .field("database_metadata", &"[redacted]")
+ .finish()
+ }
+}
+
#[cfg(any(target_os = "linux", target_os = "macos"))]
enum ServiceSqliteHostCloseState {
Pending,
@@ -244,6 +295,62 @@ impl ServiceSqliteHost {
}
}
+ /// Opens existing writable state and discovers its stored generation under authority.
+ ///
+ /// The intent binds service, instance, application ID, and the supported
+ /// schema ceiling before filesystem or SQLite admission. The returned
+ /// metadata is read from the same authority-retaining host after governed
+ /// migrations finish, so callers never guess a source generation or reopen
+ /// the database between discovery and use.
+ #[allow(clippy::too_many_arguments)]
+ pub async fn open_read_write_existing_with_intent(
+ paths: &ServiceSqlitePaths,
+ intent: &ExistingServiceDatabaseIntent,
+ migrations: &MigrationCatalog,
+ schema: &SchemaCatalog,
+ options: ServiceSqliteConnectionOptions,
+ applied_at: MigrationAppliedAtUnixSeconds,
+ build: &MigrationBuildIdentity,
+ callbacks: &[MigrationCallbackBinding],
+ ) -> Result<(OpenedExistingServiceDatabase, MigrationApplicationOutcome), ServiceSqliteError>
+ {
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ {
+ let pool = crate::open::open_existing_connection_pool_with_intent(
+ paths,
+ intent,
+ migrations,
+ schema,
+ OpenMode::ReadWriteExisting,
+ options,
+ )
+ .await?;
+ let outcome = match pool.apply_migrations(applied_at, build, callbacks).await {
+ Ok(outcome) => outcome,
+ Err(error) => {
+ drop(pool.close().await);
+ 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(OpenMode::ReadWriteExisting, pool);
+ Ok((OpenedExistingServiceDatabase::new(host, metadata), outcome))
+ }
+ #[cfg(not(any(target_os = "linux", target_os = "macos")))]
+ {
+ let _ = (
+ paths, intent, migrations, schema, options, applied_at, build, callbacks,
+ );
+ Err(unsupported_host())
+ }
+ }
+
/// Opens state created under a retained initialization writer authority.
#[allow(clippy::too_many_arguments)]
pub async fn open_initialized(
@@ -309,6 +416,42 @@ impl ServiceSqliteHost {
}
}
+ /// Opens existing state for immutable inspection and discovers its metadata.
+ pub async fn open_read_only_inspection_with_intent(
+ paths: &ServiceSqlitePaths,
+ intent: &ExistingServiceDatabaseIntent,
+ migrations: &MigrationCatalog,
+ schema: &SchemaCatalog,
+ options: ServiceSqliteConnectionOptions,
+ ) -> Result<OpenedExistingServiceDatabase, ServiceSqliteError> {
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ {
+ let pool = crate::open::open_existing_connection_pool_with_intent(
+ paths,
+ intent,
+ migrations,
+ schema,
+ OpenMode::ReadOnlyInspection,
+ options,
+ )
+ .await?;
+ let metadata = match pool.database_metadata().await {
+ Ok(metadata) => metadata,
+ Err(error) => {
+ drop(pool.close().await);
+ return Err(error);
+ }
+ };
+ let host = Self::from_pool(OpenMode::ReadOnlyInspection, pool);
+ Ok(OpenedExistingServiceDatabase::new(host, metadata))
+ }
+ #[cfg(not(any(target_os = "linux", target_os = "macos")))]
+ {
+ let _ = (paths, intent, migrations, schema, options);
+ Err(unsupported_host())
+ }
+ }
+
/// Returns the fixed mode selected when the host was opened.
#[must_use]
pub const fn mode(&self) -> OpenMode {
@@ -1449,6 +1592,83 @@ mod tests {
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[tokio::test]
+ async fn existing_intent_returns_actual_metadata_without_releasing_authority() {
+ let (_root, paths, identity, migrations, schema, initialized) = initialized_host().await;
+ initialized.close().await.expect("close initialized host");
+ let intent = ExistingServiceDatabaseIntent::new(
+ &paths,
+ identity.supported_state_schema_version(),
+ identity.application_id(),
+ );
+
+ let wrong_application = ExistingServiceDatabaseIntent::new(
+ &paths,
+ identity.supported_state_schema_version(),
+ crate::ServiceSqliteApplicationId::new(7).expect("other application"),
+ );
+ let error = ServiceSqliteHost::open_read_write_existing_with_intent(
+ &paths,
+ &wrong_application,
+ &migrations,
+ &schema,
+ ServiceSqliteConnectionOptions::reviewed(),
+ MigrationAppliedAtUnixSeconds::new(1_700_000_001).expect("migration time"),
+ &build_identity(),
+ &[],
+ )
+ .await
+ .expect_err("application mismatch");
+ assert_eq!(error.kind(), ServiceSqliteErrorKind::Metadata);
+
+ let (opened, outcome) = ServiceSqliteHost::open_read_write_existing_with_intent(
+ &paths,
+ &intent,
+ &migrations,
+ &schema,
+ ServiceSqliteConnectionOptions::reviewed(),
+ MigrationAppliedAtUnixSeconds::new(1_700_000_001).expect("migration time"),
+ &build_identity(),
+ &[],
+ )
+ .await
+ .expect("open writable from intent");
+ assert_eq!(outcome.applied_count(), 0);
+ assert_eq!(
+ opened.database_metadata().source_generation(),
+ identity.source_generation()
+ );
+ assert_eq!(opened.host().mode(), OpenMode::ReadWriteExisting);
+ let debug = format!("{opened:?}");
+ assert!(debug.contains("OpenedExistingServiceDatabase"));
+ assert!(!debug.contains("09090909"));
+ assert!(WriterAuthority::acquire(&paths, OpenMode::ReadWriteExisting).is_err());
+ let (writable, actual) = opened.into_parts();
+ assert_eq!(actual.source_generation(), identity.source_generation());
+ assert_eq!(row_count(&writable).await, 0);
+ writable.close().await.expect("close writable host");
+
+ let inspected = ServiceSqliteHost::open_read_only_inspection_with_intent(
+ &paths,
+ &intent,
+ &migrations,
+ &schema,
+ ServiceSqliteConnectionOptions::reviewed(),
+ )
+ .await
+ .expect("open inspection from intent");
+ assert_eq!(inspected.host().mode(), OpenMode::ReadOnlyInspection);
+ assert_eq!(
+ inspected.database_metadata().source_generation(),
+ identity.source_generation()
+ );
+ assert!(WriterAuthority::acquire(&paths, OpenMode::ReadWriteExisting).is_err());
+ let (inspection, actual) = inspected.into_parts();
+ assert_eq!(actual.source_generation(), identity.source_generation());
+ inspection.close().await.expect("close inspection host");
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[tokio::test]
async fn integrity_inspection_is_explicit_safe_and_available_in_every_host_mode() {
let _serial = crate::integrity::integrity_test_seam::LOCK.lock().await;
crate::integrity::integrity_test_seam::release();
diff --git a/crates/service_sqlite/src/lib.rs b/crates/service_sqlite/src/lib.rs
@@ -55,8 +55,9 @@ pub use backup::{
};
pub use config::{ServiceSqliteConnectionOptions, ServiceSqliteConnectionOptionsError};
pub use connection::{
- ServiceSqliteHost, ServiceSqliteTransaction, ServiceSqliteTransactionError,
- ServiceSqliteTransactionErrorKind, ServiceSqliteTransactionFuture,
+ OpenedExistingServiceDatabase, ServiceSqliteHost, ServiceSqliteTransaction,
+ ServiceSqliteTransactionError, ServiceSqliteTransactionErrorKind,
+ ServiceSqliteTransactionFuture,
};
pub use error::{
SafeServiceSqliteError, ServiceSqliteError, ServiceSqliteErrorCode, ServiceSqliteErrorKind,
@@ -68,8 +69,8 @@ pub use integrity::{
ServiceSqliteIntegrityReport,
};
pub use metadata::{
- ServiceDatabaseIdentity, ServiceDatabaseMetadata, ServiceSqliteApplicationId,
- ServiceSqliteMetadataValueError,
+ ExistingServiceDatabaseIntent, ServiceDatabaseIdentity, ServiceDatabaseMetadata,
+ ServiceSqliteApplicationId, ServiceSqliteMetadataValueError,
};
pub use migration::{
MigrationApplicationOutcome, MigrationAppliedAtUnixSeconds, MigrationBuildIdentity,
diff --git a/crates/service_sqlite/src/metadata.rs b/crates/service_sqlite/src/metadata.rs
@@ -80,6 +80,15 @@ pub struct ServiceDatabaseIdentity {
application_id: ServiceSqliteApplicationId,
}
+/// Sealed expectation for opening an existing database without guessing its generation.
+#[derive(Clone, PartialEq, Eq)]
+pub struct ExistingServiceDatabaseIntent {
+ service: ServiceId,
+ instance: InstanceId,
+ supported_state_schema_version: NonZeroU32,
+ application_id: ServiceSqliteApplicationId,
+}
+
impl ServiceDatabaseMetadata {
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) fn from_verified_backup(
@@ -237,6 +246,67 @@ impl ServiceDatabaseIdentity {
}
}
+impl ExistingServiceDatabaseIntent {
+ /// Binds an existing-only open to canonical paths and the binary's fixed contract.
+ #[must_use]
+ pub fn new(
+ paths: &ServiceSqlitePaths,
+ supported_state_schema_version: NonZeroU32,
+ application_id: ServiceSqliteApplicationId,
+ ) -> Self {
+ Self {
+ service: paths.service().clone(),
+ instance: paths.instance().clone(),
+ supported_state_schema_version,
+ application_id,
+ }
+ }
+
+ /// Returns the bound service identity.
+ #[must_use]
+ pub fn service(&self) -> &ServiceId {
+ &self.service
+ }
+
+ /// Returns the bound instance identity.
+ #[must_use]
+ pub fn instance(&self) -> &InstanceId {
+ &self.instance
+ }
+
+ /// Returns the newest state schema version this binary accepts.
+ #[must_use]
+ pub const fn supported_state_schema_version(&self) -> NonZeroU32 {
+ self.supported_state_schema_version
+ }
+
+ /// Returns the expected SQLite application identifier.
+ #[must_use]
+ pub const fn application_id(&self) -> ServiceSqliteApplicationId {
+ self.application_id
+ }
+
+ pub(crate) fn matches_paths(&self, paths: &ServiceSqlitePaths) -> bool {
+ crate::all_constraints([
+ self.service == *paths.service(),
+ self.instance == *paths.instance(),
+ ])
+ }
+
+ pub(crate) fn identity_for(
+ &self,
+ metadata: &ServiceDatabaseMetadata,
+ ) -> ServiceDatabaseIdentity {
+ ServiceDatabaseIdentity {
+ service: self.service.clone(),
+ instance: self.instance.clone(),
+ source_generation: metadata.source_generation,
+ supported_state_schema_version: self.supported_state_schema_version,
+ application_id: self.application_id,
+ }
+ }
+}
+
impl fmt::Debug for ServiceDatabaseIdentity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
@@ -253,6 +323,21 @@ impl fmt::Debug for ServiceDatabaseIdentity {
}
}
+impl fmt::Debug for ExistingServiceDatabaseIntent {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter
+ .debug_struct("ExistingServiceDatabaseIntent")
+ .field("service", &"[redacted]")
+ .field("instance", &"[redacted]")
+ .field(
+ "supported_state_schema_version",
+ &self.supported_state_schema_version,
+ )
+ .field("application_id", &self.application_id)
+ .finish()
+ }
+}
+
impl fmt::Debug for ServiceDatabaseMetadata {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
@@ -420,6 +505,24 @@ pub(crate) async fn verify_database_metadata(
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
+pub(crate) async fn verify_existing_database_intent(
+ connection: &mut SqliteConnection,
+ intent: &ExistingServiceDatabaseIntent,
+) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> {
+ let actual = read_database_metadata(connection).await?;
+ require_metadata_condition(
+ crate::all_constraints([
+ actual.service == intent.service,
+ actual.instance == intent.instance,
+ actual.application_id == intent.application_id,
+ actual.state_schema_version <= intent.supported_state_schema_version,
+ ]),
+ MetadataFailureKind::Mismatch,
+ )?;
+ Ok(actual)
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
async fn read_database_metadata(
connection: &mut SqliteConnection,
) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> {
@@ -757,6 +860,84 @@ mod tests {
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[tokio::test(flavor = "current_thread")]
+ async fn existing_intent_discovers_generation_and_binds_every_trusted_dimension() {
+ let paths = sqlite_paths("myc", "primary");
+ let expected = metadata(&paths, 7, 1, 1_700_000_000_000, 0x5244_5351);
+ let mut connection = memory_connection().await;
+ write_database_metadata(&mut connection, &expected, &base_schema_catalog())
+ .await
+ .expect("write metadata");
+
+ let intent = ExistingServiceDatabaseIntent::new(
+ &paths,
+ NonZeroU32::new(2).expect("schema ceiling"),
+ expected.application_id(),
+ );
+ let actual = verify_existing_database_intent(&mut connection, &intent)
+ .await
+ .expect("discover metadata");
+ assert_eq!(actual, expected);
+ assert_eq!(actual.source_generation().as_bytes(), &[7; 32]);
+ assert_eq!(
+ intent.identity_for(&actual).source_generation(),
+ actual.source_generation()
+ );
+
+ let debug = format!("{intent:?}");
+ assert!(debug.contains("ExistingServiceDatabaseIntent"));
+ assert!(!debug.contains("myc"));
+ assert!(!debug.contains("primary"));
+ assert!(!debug.contains("07070707"));
+
+ let other_paths = sqlite_paths("rhi", "primary");
+ let wrong_service = ExistingServiceDatabaseIntent::new(
+ &other_paths,
+ intent.supported_state_schema_version(),
+ intent.application_id(),
+ );
+ let other_instance_paths = sqlite_paths("myc", "secondary");
+ let wrong_instance = ExistingServiceDatabaseIntent::new(
+ &other_instance_paths,
+ intent.supported_state_schema_version(),
+ intent.application_id(),
+ );
+ let wrong_application = ExistingServiceDatabaseIntent::new(
+ &paths,
+ intent.supported_state_schema_version(),
+ ServiceSqliteApplicationId::new(7).expect("other application"),
+ );
+ for rejected in [&wrong_service, &wrong_instance, &wrong_application] {
+ assert_eq!(
+ verify_existing_database_intent(&mut connection, rejected)
+ .await
+ .expect_err("intent mismatch")
+ .kind(),
+ ServiceSqliteErrorKind::Metadata
+ );
+ }
+
+ sqlx::query(
+ "UPDATE radroots_service_metadata SET state_schema_version = 2 WHERE singleton = 1",
+ )
+ .execute(&mut connection)
+ .await
+ .expect("advance stored schema");
+ let older_binary = ExistingServiceDatabaseIntent::new(
+ &paths,
+ NonZeroU32::new(1).expect("older ceiling"),
+ expected.application_id(),
+ );
+ assert_eq!(
+ verify_existing_database_intent(&mut connection, &older_binary)
+ .await
+ .expect_err("newer stored schema")
+ .kind(),
+ ServiceSqliteErrorKind::Metadata
+ );
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[tokio::test(flavor = "current_thread")]
async fn schema_mismatch_rolls_back_shared_objects_metadata_and_application_id() {
let paths = sqlite_paths("myc", "primary");
let expected = metadata(&paths, 7, 1, 1_700_000_000_000, 0x5244_5351);
diff --git a/crates/service_sqlite/src/open.rs b/crates/service_sqlite/src/open.rs
@@ -32,9 +32,9 @@ use sqlx::{
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::{
- MigrationAppliedAtUnixSeconds, MigrationBuildIdentity, MigrationCatalog, SchemaCatalog,
- ServiceDatabaseIdentity, ServiceSqliteConnectionOptions, ServiceSqliteError,
- ServiceSqliteErrorKind, WriterAuthority,
+ ExistingServiceDatabaseIntent, MigrationAppliedAtUnixSeconds, MigrationBuildIdentity,
+ MigrationCatalog, SchemaCatalog, ServiceDatabaseIdentity, ServiceDatabaseMetadata,
+ ServiceSqliteConnectionOptions, ServiceSqliteError, ServiceSqliteErrorKind, WriterAuthority,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
@@ -61,6 +61,51 @@ struct PoolConnectionValidation {
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
+#[derive(Clone, Copy)]
+enum ServiceDatabaseExpectation<'a> {
+ Exact(&'a ServiceDatabaseIdentity),
+ Existing(&'a ExistingServiceDatabaseIntent),
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+impl ServiceDatabaseExpectation<'_> {
+ fn matches_paths(self, paths: &ServiceSqlitePaths) -> bool {
+ match self {
+ Self::Exact(identity) => identity.matches_paths(paths),
+ Self::Existing(intent) => intent.matches_paths(paths),
+ }
+ }
+
+ fn supported_state_schema_version(self) -> core::num::NonZeroU32 {
+ match self {
+ Self::Exact(identity) => identity.supported_state_schema_version(),
+ Self::Existing(intent) => intent.supported_state_schema_version(),
+ }
+ }
+
+ async fn verify_metadata(
+ self,
+ connection: &mut SqliteConnection,
+ ) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> {
+ match self {
+ Self::Exact(identity) => {
+ crate::metadata::verify_database_metadata(connection, identity).await
+ }
+ Self::Existing(intent) => {
+ crate::metadata::verify_existing_database_intent(connection, intent).await
+ }
+ }
+ }
+
+ fn exact_identity(self, metadata: &ServiceDatabaseMetadata) -> ServiceDatabaseIdentity {
+ match self {
+ Self::Exact(identity) => identity.clone(),
+ Self::Existing(intent) => intent.identity_for(metadata),
+ }
+ }
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
enum PoolConnectionValidationFailure {
Authority,
Pragma(sqlx::Error),
@@ -383,6 +428,17 @@ impl PrivateConnectionPool {
&self.identity
}
+ pub(crate) async fn database_metadata(
+ &self,
+ ) -> Result<ServiceDatabaseMetadata, ServiceSqliteError> {
+ self.validate()?;
+ let mut connection = self.acquire().await?;
+ let result =
+ crate::metadata::verify_database_metadata(&mut connection, &self.identity).await;
+ self.validate()?;
+ result
+ }
+
pub(crate) fn backup_source_validator(&self) -> BackupSourceValidator {
BackupSourceValidator {
binding: self.binding.clone(),
@@ -765,6 +821,46 @@ pub(crate) async fn open_existing_connection_pool(
mode: OpenMode,
policy: ServiceSqliteConnectionOptions,
) -> Result<PrivateConnectionPool, ServiceSqliteError> {
+ open_existing_connection_pool_for(
+ paths,
+ ServiceDatabaseExpectation::Exact(identity),
+ catalog,
+ schema_catalog,
+ mode,
+ policy,
+ )
+ .await
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+pub(crate) async fn open_existing_connection_pool_with_intent(
+ paths: &ServiceSqlitePaths,
+ intent: &ExistingServiceDatabaseIntent,
+ catalog: &MigrationCatalog,
+ schema_catalog: &SchemaCatalog,
+ mode: OpenMode,
+ policy: ServiceSqliteConnectionOptions,
+) -> Result<PrivateConnectionPool, ServiceSqliteError> {
+ open_existing_connection_pool_for(
+ paths,
+ ServiceDatabaseExpectation::Existing(intent),
+ catalog,
+ schema_catalog,
+ mode,
+ policy,
+ )
+ .await
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+async fn open_existing_connection_pool_for(
+ paths: &ServiceSqlitePaths,
+ expectation: ServiceDatabaseExpectation<'_>,
+ catalog: &MigrationCatalog,
+ schema_catalog: &SchemaCatalog,
+ mode: OpenMode,
+ policy: ServiceSqliteConnectionOptions,
+) -> Result<PrivateConnectionPool, ServiceSqliteError> {
if mode == OpenMode::Initialize {
return Err(connection_error(
ServiceSqliteErrorKind::Open,
@@ -778,7 +874,7 @@ pub(crate) async fn open_existing_connection_pool(
};
open_connection_pool(
paths,
- identity,
+ expectation,
catalog,
schema_catalog,
mode,
@@ -801,7 +897,7 @@ pub(crate) async fn open_initialized_connection_pool(
authority.validate_for(paths)?;
open_connection_pool(
paths,
- identity,
+ ServiceDatabaseExpectation::Exact(identity),
catalog,
schema_catalog,
OpenMode::Initialize,
@@ -857,7 +953,7 @@ impl PoolConnectionValidation {
#[allow(clippy::too_many_arguments)]
async fn open_connection_pool(
paths: &ServiceSqlitePaths,
- identity: &ServiceDatabaseIdentity,
+ expectation: ServiceDatabaseExpectation<'_>,
catalog: &MigrationCatalog,
schema_catalog: &SchemaCatalog,
mode: OpenMode,
@@ -865,10 +961,10 @@ async fn open_connection_pool(
authority: Option<WriterAuthority>,
inspection_guard: Option<ReadOnlyInspectionGuard>,
) -> Result<PrivateConnectionPool, ServiceSqliteError> {
- if !identity.matches_paths(paths) {
+ if !expectation.matches_paths(paths) {
return Err(ServiceSqliteError::new(ServiceSqliteErrorKind::Metadata));
}
- if identity.supported_state_schema_version().get() != catalog.current_version() {
+ if expectation.supported_state_schema_version().get() != catalog.current_version() {
return Err(ServiceSqliteError::new(ServiceSqliteErrorKind::Migration));
}
if !schema_catalog.matches_migrations(catalog) {
@@ -881,9 +977,14 @@ async fn open_connection_pool(
authority.validate_for(paths)?;
result
}
- (OpenMode::ReadWriteExisting, Some(authority), None) => {
- crate::restore::recover_for_open(paths, identity, authority)
- }
+ (OpenMode::ReadWriteExisting, Some(authority), None) => match expectation {
+ ServiceDatabaseExpectation::Exact(identity) => {
+ crate::restore::recover_for_open(paths, identity, authority)
+ }
+ ServiceDatabaseExpectation::Existing(intent) => {
+ crate::restore::recover_for_open_with_intent(paths, intent, authority)
+ }
+ },
(OpenMode::ReadOnlyInspection, None, Some(inspection_guard)) => {
inspection_guard.validate_for(paths)?;
let result = crate::restore::refuse_unresolved_recovery(&inspection_guard.directory);
@@ -921,10 +1022,10 @@ async fn open_connection_pool(
let preflight_policy = verify_connection_policy(&mut preflight, mode, policy).await;
binding.validate(paths)?;
preflight_policy.map_err(|source| connection_source(ServiceSqliteErrorKind::Pragma, source))?;
- let preflight_metadata =
- crate::metadata::verify_database_metadata(&mut preflight, identity).await;
+ let preflight_metadata = expectation.verify_metadata(&mut preflight).await;
binding.validate(paths)?;
- preflight_metadata?;
+ let preflight_metadata = preflight_metadata?;
+ let identity = expectation.exact_identity(&preflight_metadata);
let preflight_history = crate::migration::verify_migration_history(
&mut preflight,
catalog,
@@ -2944,7 +3045,7 @@ mod tests {
);
let Err(error) = open_connection_pool(
&paths,
- &wrong_identity,
+ ServiceDatabaseExpectation::Exact(&wrong_identity),
&base_catalog(),
&base_schema_catalog(),
OpenMode::Initialize,
@@ -2968,7 +3069,7 @@ mod tests {
);
let Err(error) = open_connection_pool(
&paths,
- &newer_identity,
+ ServiceDatabaseExpectation::Exact(&newer_identity),
&base_catalog(),
&base_schema_catalog(),
OpenMode::Initialize,
@@ -2986,7 +3087,7 @@ mod tests {
initialized_authority(directory.path(), "schema-drift-preflight").await;
let Err(error) = open_connection_pool(
&paths,
- &identity,
+ ServiceDatabaseExpectation::Exact(&identity),
&base_catalog(),
&migration_schema_catalog(),
OpenMode::Initialize,
diff --git a/crates/service_sqlite/src/restore/marker.rs b/crates/service_sqlite/src/restore/marker.rs
@@ -14,8 +14,8 @@ use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::{
- BackupManifestSha256, ServiceDatabaseIdentity, ServiceDatabaseMetadata,
- ServiceSqliteApplicationId, ServiceSqlitePaths,
+ BackupManifestSha256, ExistingServiceDatabaseIntent, ServiceDatabaseIdentity,
+ ServiceDatabaseMetadata, ServiceSqliteApplicationId, ServiceSqlitePaths,
};
#[cfg(any(target_os = "linux", target_os = "macos"))]
@@ -397,6 +397,15 @@ impl RestoreRecoveryMarker {
self.state_schema_version <= identity.supported_state_schema_version(),
])
}
+
+ pub(crate) fn matches_existing_intent(&self, intent: &ExistingServiceDatabaseIntent) -> bool {
+ crate::all_constraints([
+ self.service == *intent.service(),
+ self.instance == *intent.instance(),
+ self.application_id == intent.application_id(),
+ self.state_schema_version <= intent.supported_state_schema_version(),
+ ])
+ }
}
impl fmt::Debug for RestoreRecoveryMarker {
@@ -1944,6 +1953,50 @@ mod tests {
}
#[test]
+ fn marker_existing_intent_discovers_generation_but_binds_other_dimensions() {
+ let root = tempfile::tempdir().expect("root");
+ let paths = paths(root.path());
+ let marker = marker(&paths);
+ let exact = ExistingServiceDatabaseIntent::new(
+ &paths,
+ NonZeroU32::new(3).expect("schema ceiling"),
+ ServiceSqliteApplicationId::new(0x5244_5254).expect("application"),
+ );
+ assert!(marker.matches_existing_intent(&exact));
+
+ let other_service_paths = paths_for(root.path(), "rhi", "primary");
+ assert!(
+ !marker.matches_existing_intent(&ExistingServiceDatabaseIntent::new(
+ &other_service_paths,
+ exact.supported_state_schema_version(),
+ exact.application_id(),
+ ))
+ );
+ let other_instance_paths = paths_for(root.path(), "myc", "secondary");
+ assert!(
+ !marker.matches_existing_intent(&ExistingServiceDatabaseIntent::new(
+ &other_instance_paths,
+ exact.supported_state_schema_version(),
+ exact.application_id(),
+ ))
+ );
+ assert!(
+ !marker.matches_existing_intent(&ExistingServiceDatabaseIntent::new(
+ &paths,
+ NonZeroU32::new(2).expect("older schema ceiling"),
+ exact.application_id(),
+ ))
+ );
+ assert!(
+ !marker.matches_existing_intent(&ExistingServiceDatabaseIntent::new(
+ &paths,
+ exact.supported_state_schema_version(),
+ ServiceSqliteApplicationId::new(7).expect("other application"),
+ ))
+ );
+ }
+
+ #[test]
fn all_phase_edges_and_idempotent_bytes_are_exact() {
let root = tempfile::tempdir().expect("root");
let prepared = marker(&paths(root.path()));
diff --git a/crates/service_sqlite/src/restore/mod.rs b/crates/service_sqlite/src/restore/mod.rs
@@ -16,7 +16,7 @@ pub use stage::{StagedServiceRestore, stage_verified_restore};
pub(crate) use recover::refuse_unresolved_recovery;
#[cfg(any(target_os = "linux", target_os = "macos"))]
-pub(crate) use recover::recover_for_open;
+pub(crate) use recover::{recover_for_open, recover_for_open_with_intent};
#[allow(unused_imports)]
pub(crate) use marker::{
diff --git a/crates/service_sqlite/src/restore/recover.rs b/crates/service_sqlite/src/restore/recover.rs
@@ -13,8 +13,8 @@ use {
},
},
crate::{
- ServiceDatabaseIdentity, ServiceSqliteError, ServiceSqliteErrorKind, ServiceSqlitePaths,
- WriterAuthority,
+ ExistingServiceDatabaseIntent, ServiceDatabaseIdentity, ServiceSqliteError,
+ ServiceSqliteErrorKind, ServiceSqlitePaths, WriterAuthority,
},
rustix::{
fs::{
@@ -37,6 +37,39 @@ pub(crate) fn recover_for_open(
identity: &ServiceDatabaseIdentity,
authority: &WriterAuthority,
) -> Result<(), ServiceSqliteError> {
+ recover_for_open_expectation(
+ paths,
+ RecoveryDatabaseExpectation::Exact(identity),
+ authority,
+ )
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+pub(crate) fn recover_for_open_with_intent(
+ paths: &ServiceSqlitePaths,
+ intent: &ExistingServiceDatabaseIntent,
+ authority: &WriterAuthority,
+) -> Result<(), ServiceSqliteError> {
+ recover_for_open_expectation(
+ paths,
+ RecoveryDatabaseExpectation::Existing(intent),
+ authority,
+ )
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+#[derive(Clone, Copy)]
+enum RecoveryDatabaseExpectation<'a> {
+ Exact(&'a ServiceDatabaseIdentity),
+ Existing(&'a ExistingServiceDatabaseIntent),
+}
+
+#[cfg(any(target_os = "linux", target_os = "macos"))]
+fn recover_for_open_expectation(
+ paths: &ServiceSqlitePaths,
+ expectation: RecoveryDatabaseExpectation<'_>,
+ authority: &WriterAuthority,
+) -> Result<(), ServiceSqliteError> {
authority.validate_for(paths)?;
let Some(mut marker) = RestoreMarkerBinding::load_for_recovery(paths, authority)? else {
authority_checked(authority, paths, || {
@@ -44,7 +77,13 @@ pub(crate) fn recover_for_open(
})?;
return Ok(());
};
- if !marker.marker().matches_identity(identity) {
+ let intent_matches = match expectation {
+ RecoveryDatabaseExpectation::Exact(identity) => marker.marker().matches_identity(identity),
+ RecoveryDatabaseExpectation::Existing(intent) => {
+ marker.marker().matches_existing_intent(intent)
+ }
+ };
+ if !intent_matches {
return Err(recovery_error(RecoveryFailureKind::Intent));
}
@@ -759,6 +798,15 @@ mod tests {
fn recover(&self) -> Result<(), ServiceSqliteError> {
recover_for_open(&self.paths, &self.identity, &self.authority)
}
+
+ fn recover_with_intent(&self) -> Result<(), ServiceSqliteError> {
+ let intent = ExistingServiceDatabaseIntent::new(
+ &self.paths,
+ self.identity.supported_state_schema_version(),
+ self.identity.application_id(),
+ );
+ recover_for_open_with_intent(&self.paths, &intent, &self.authority)
+ }
}
#[test]
@@ -790,6 +838,17 @@ mod tests {
}
#[test]
+ fn generation_discovering_intent_recovers_when_live_database_is_retained() {
+ let fixture = Fixture::new();
+ fixture.retain_live(false);
+ fixture
+ .recover_with_intent()
+ .expect("recover from marker-bound existing intent");
+ assert_eq!(fs::read(fixture.paths.state_database()).unwrap(), NEW_BYTES);
+ assert_no_recovery_evidence(&fixture.paths);
+ }
+
+ #[test]
fn live_retained_with_proven_second_rename_rolls_forward() {
let fixture = Fixture::new();
fixture.retain_live(true);
diff --git a/crates/service_sqlite/tests/package_boundary.rs b/crates/service_sqlite/tests/package_boundary.rs
@@ -170,6 +170,12 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
"is revalidated before commit and before a connection can return to the pool",
"Writable host opening finishes every pending governed migration",
"read-only inspection opens only current migration and",
+ "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`",
+ "keeps the host and verified metadata inseparable until the caller consumes them together",
+ "Recovery remains fail closed",
"with raw database authority",
"before the runner enables outer commit",
"leaves no authoritative transaction effect",
@@ -391,6 +397,8 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
"pub struct radroots_service_sqlite::ServiceSqliteHost",
"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::MigrationCatalog",
"pub struct radroots_service_sqlite::SchemaCatalog",
"pub struct radroots_service_sqlite::VerifiedServiceBackup",
@@ -399,6 +407,8 @@ fn service_sqlite_is_unpublished_lint_governed_and_dependency_bounded() {
"pub fn radroots_service_sqlite::verify_backup_bundle",
"pub async fn radroots_service_sqlite::finalize_staged_restore",
"pub async fn radroots_service_sqlite::stage_verified_restore",
+ "pub async fn radroots_service_sqlite::ServiceSqliteHost::open_read_write_existing_with_intent",
+ "pub async fn radroots_service_sqlite::ServiceSqliteHost::open_read_only_inspection_with_intent",
"impl<'executor, 'connection> sqlx_core::executor::Executor<'executor> for &'executor mut radroots_service_sqlite::ServiceSqliteTransaction<'connection>",
] {
assert!(