app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

db.rs (6839B)


      1 use core::num::NonZeroU32;
      2 
      3 use harvestcircle_domain::{SafeError, SafeErrorCode, SafeMessage};
      4 use radroots_runtime_paths::RuntimeContext;
      5 use radroots_service_sqlite::{
      6     ExistingServiceDatabaseIntent, MigrationAppliedAtUnixSeconds, MigrationBuildIdentity,
      7     ServiceDatabaseMetadata, ServiceSqliteConnectionOptions, ServiceSqliteErrorKind,
      8     ServiceSqliteHost, ServiceSqliteInitializer, ServiceSqliteInitializerFuture,
      9     ServiceSqliteTransactionError, ServiceSqliteTransactionErrorKind,
     10 };
     11 use radroots_storage::event::SourceGeneration;
     12 
     13 use crate::contract::{
     14     HARVESTCIRCLE_INITIAL_STATE_SCHEMA_VERSION, harvestcircle_initial_schema_sql,
     15 };
     16 use crate::{HARVESTCIRCLE_STATE_SCHEMA_VERSION, HarvestCircleStorageContract};
     17 
     18 pub const CURRENT_SCHEMA_VERSION: u32 = HARVESTCIRCLE_STATE_SCHEMA_VERSION;
     19 
     20 /// The sole HarvestCircle SQLite host.
     21 ///
     22 /// The underlying pool and connections remain sealed by `radroots_service_sqlite`.
     23 pub struct Database {
     24     host: ServiceSqliteHost,
     25     metadata: ServiceDatabaseMetadata,
     26 }
     27 
     28 impl Database {
     29     /// Opens or initializes the canonical HarvestCircle service-instance state.
     30     ///
     31     /// The caller injects creation and migration evidence. Fresh state receives
     32     /// a new opaque source generation from host entropy; existing state is
     33     /// admitted through its persisted identity without guessing that generation.
     34     pub async fn open(
     35         context: &RuntimeContext,
     36         created_at_unix_ms: u64,
     37         applied_at_unix_s: u64,
     38         build: &MigrationBuildIdentity,
     39     ) -> Result<Self, SafeError> {
     40         let contract = HarvestCircleStorageContract::from_runtime_context(context)
     41             .map_err(|_| invalid_storage_contract())?;
     42         let applied_at = MigrationAppliedAtUnixSeconds::new(applied_at_unix_s)
     43             .map_err(|_| invalid_storage_contract())?;
     44         let options = ServiceSqliteConnectionOptions::reviewed();
     45 
     46         let mut generation = [0_u8; 32];
     47         getrandom::getrandom(&mut generation).map_err(|_| storage_unavailable())?;
     48         let generation = SourceGeneration::new(generation).map_err(|_| storage_unavailable())?;
     49         let metadata = ServiceDatabaseMetadata::new(
     50             contract.paths(),
     51             generation,
     52             NonZeroU32::new(HARVESTCIRCLE_INITIAL_STATE_SCHEMA_VERSION)
     53                 .expect("initial schema version is nonzero"),
     54             created_at_unix_ms,
     55             contract.application_id(),
     56         )
     57         .map_err(|_| invalid_storage_contract())?;
     58         context
     59             .state_directory_plan()
     60             .map_err(|_| invalid_storage_contract())?
     61             .provision()
     62             .map_err(|_| storage_unavailable())?;
     63         let (opened, _) = ServiceSqliteHost::open_or_initialize(
     64             contract.paths(),
     65             &metadata,
     66             contract.migrations(),
     67             contract.schema(),
     68             options,
     69             applied_at,
     70             build,
     71             &[],
     72             initialize_application_schema,
     73         )
     74         .await
     75         .map_err(map_service_error)?;
     76         let (host, metadata) = opened.into_parts();
     77         Ok(Self { host, metadata })
     78     }
     79 
     80     /// Explicitly drains SQLite work, checkpoints WAL state, and releases authority.
     81     pub async fn close(&self) -> Result<(), SafeError> {
     82         self.host.close().await.map_err(map_service_error)
     83     }
     84 
     85     #[must_use]
     86     pub const fn metadata(&self) -> &ServiceDatabaseMetadata {
     87         &self.metadata
     88     }
     89 
     90     pub(crate) const fn host(&self) -> &ServiceSqliteHost {
     91         &self.host
     92     }
     93 
     94     pub(crate) async fn open_existing(
     95         contract: &HarvestCircleStorageContract,
     96         applied_at: MigrationAppliedAtUnixSeconds,
     97         build: &MigrationBuildIdentity,
     98     ) -> Result<Self, SafeError> {
     99         let intent = ExistingServiceDatabaseIntent::new(
    100             contract.paths(),
    101             contract.state_schema_version(),
    102             contract.application_id(),
    103         );
    104         let (opened, _) = ServiceSqliteHost::open_read_write_existing_with_intent(
    105             contract.paths(),
    106             &intent,
    107             contract.migrations(),
    108             contract.schema(),
    109             ServiceSqliteConnectionOptions::reviewed(),
    110             applied_at,
    111             build,
    112             &[],
    113         )
    114         .await
    115         .map_err(map_service_error)?;
    116         let (host, metadata) = opened.into_parts();
    117         Ok(Self { host, metadata })
    118     }
    119 }
    120 
    121 fn initialize_application_schema<'a>(
    122     initializer: &'a mut ServiceSqliteInitializer<'_>,
    123 ) -> ServiceSqliteInitializerFuture<'a, sqlx::Error> {
    124     Box::pin(async move {
    125         for statement in harvestcircle_initial_schema_sql() {
    126             sqlx::query(*statement).execute(&mut *initializer).await?;
    127         }
    128         sqlx::query("INSERT INTO runtime_state (singleton) VALUES (1)")
    129             .execute(&mut *initializer)
    130             .await?;
    131         Ok(())
    132     })
    133 }
    134 
    135 pub(crate) fn map_transaction_error(error: ServiceSqliteTransactionError<SafeError>) -> SafeError {
    136     if let Some(operation) = error.operation_error() {
    137         return *operation;
    138     }
    139     match error.kind() {
    140         ServiceSqliteTransactionErrorKind::CommitOutcomeUnknown => commit_outcome_unknown(),
    141         ServiceSqliteTransactionErrorKind::NotCommitted
    142         | ServiceSqliteTransactionErrorKind::OperationRolledBack
    143         | ServiceSqliteTransactionErrorKind::RollbackFailed => storage_unavailable(),
    144     }
    145 }
    146 
    147 pub(crate) fn map_service_error(error: radroots_service_sqlite::ServiceSqliteError) -> SafeError {
    148     match error.kind() {
    149         ServiceSqliteErrorKind::Metadata
    150         | ServiceSqliteErrorKind::Migration
    151         | ServiceSqliteErrorKind::Integrity
    152         | ServiceSqliteErrorKind::Recovery => corrupt_storage(),
    153         ServiceSqliteErrorKind::Authority
    154         | ServiceSqliteErrorKind::Open
    155         | ServiceSqliteErrorKind::Create
    156         | ServiceSqliteErrorKind::Pragma
    157         | ServiceSqliteErrorKind::Backup
    158         | ServiceSqliteErrorKind::Restore => storage_unavailable(),
    159     }
    160 }
    161 
    162 pub(crate) const fn storage_unavailable() -> SafeError {
    163     SafeError::new(
    164         SafeErrorCode::StorageUnavailable,
    165         SafeMessage::new("The application state is unavailable."),
    166     )
    167 }
    168 
    169 pub(crate) const fn corrupt_storage() -> SafeError {
    170     SafeError::new(
    171         SafeErrorCode::StorageCorrupt,
    172         SafeMessage::new("The application state could not be verified."),
    173     )
    174 }
    175 
    176 pub(crate) const fn invalid_storage_contract() -> SafeError {
    177     SafeError::new(
    178         SafeErrorCode::InvalidApplicationState,
    179         SafeMessage::new("The application storage contract is invalid."),
    180     )
    181 }
    182 
    183 const fn commit_outcome_unknown() -> SafeError {
    184     SafeError::new(
    185         SafeErrorCode::PendingOperationRecoveryRequired,
    186         SafeMessage::new("The storage commit outcome must be reconciled."),
    187     )
    188 }