app

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

lib.rs (48586B)


      1 #![forbid(unsafe_code)]
      2 
      3 mod error;
      4 mod interop;
      5 mod migration_audit;
      6 mod migrations;
      7 mod repo;
      8 mod sdk_migration_receipts;
      9 mod sync;
     10 
     11 use std::{collections::BTreeSet, fs, path::PathBuf, time::Duration};
     12 
     13 use radroots_app_sync::{
     14     AppRelayIngestScopeFreshness, PendingSyncOperation, SyncCheckpointStatus, SyncConflict,
     15     SyncConflictResolutionStatus,
     16 };
     17 use radroots_app_view::{
     18     AccountSurfaceActivationProjection, AppActivityContext, AppActivityEvent, AppActivityKind,
     19     BuyerCartProjection, BuyerContext, BuyerListingsProjection, BuyerOrderDetailProjection,
     20     BuyerOrderReviewDraft, BuyerOrderReviewProjection, BuyerOrdersProjection,
     21     BuyerProductDetailProjection, FarmId, FarmOrderMethod, FarmRulesProjection,
     22     FarmSetupProjection, FarmSummary, FulfillmentWindowId, OrderDetailProjection, OrderId,
     23     OrdersListProjection, OrdersScreenQueryState, PackDayOutputSource, PackDayProjection,
     24     PackDayScreenQueryState, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductsFilter,
     25     ProductsListProjection, ProductsSort, ReminderFeedProjection, ReminderLogEntryProjection,
     26     ReminderLogProjection, TodayAgendaProjection,
     27 };
     28 use rusqlite::Connection;
     29 
     30 pub use error::AppSqliteError;
     31 pub use interop::{
     32     AppLocalInteropImportReport, AppLocalInteropRepository, StoredLocalInteropRecord,
     33     projected_order_id_from_trade_request,
     34 };
     35 pub use migration_audit::{
     36     APP_SDK_MIGRATION_AUDIT_DEFAULT_BATCH_SIZE, APP_SDK_MIGRATION_AUDIT_MAX_BATCH_SIZE,
     37     AppSdkMigrationAuditClassification, AppSdkMigrationAuditCount,
     38     AppSdkMigrationAuditDuplicateCandidate, AppSdkMigrationAuditIssue, AppSdkMigrationAuditReport,
     39     AppSdkMigrationAuditRequest, AppSdkMigrationAuditSource, AppSdkMigrationAuditSourceReport,
     40 };
     41 pub use migrations::latest_schema_version;
     42 pub use repo::{
     43     APP_ACTIVITY_CONTEXT_LIMIT, APP_ACTIVITY_RETENTION_LIMIT, AppActivationRepository,
     44     AppActivityRepository, AppBuyerRepository, AppFarmRulesRepository, AppFarmSetupRepository,
     45     AppOrdersRepository, AppProductsRepository, AppRemindersRepository, AppTodayAgendaRepository,
     46     BuyerOrderCoordinationRecord, BuyerOrderCoordinationState, BuyerOrderLocalEventExport,
     47     BuyerOrderLocalEventLine, BuyerRepeatDemandApplyOutcome, SelectedBuyerOrderScope,
     48     SellerOrderDecisionExport, SellerOrderDecisionLineExport, TODAY_AGENDA_LIST_LIMIT,
     49     TODAY_AGENDA_LOW_STOCK_THRESHOLD, derive_farm_rules_readiness,
     50 };
     51 pub use sdk_migration_receipts::{
     52     AppSdkMigrationReceipt, AppSdkMigrationReceiptInput, AppSdkMigrationReceiptRepository,
     53     AppSdkMigrationReceiptSourceKind, AppSdkMigrationState,
     54 };
     55 pub use sync::{
     56     AppSyncRepository, StoredPendingSyncOperation, StoredRelayIngestCursor, StoredSyncConflict,
     57 };
     58 
     59 const SQLITE_BUSY_TIMEOUT_MS: u64 = 5_000;
     60 
     61 #[derive(Clone, Debug, Eq, PartialEq)]
     62 pub enum DatabaseTarget {
     63     InMemory,
     64     Path(PathBuf),
     65 }
     66 
     67 pub struct AppSqliteStore {
     68     connection: Connection,
     69 }
     70 
     71 impl AppSqliteStore {
     72     pub fn open(target: DatabaseTarget) -> Result<Self, AppSqliteError> {
     73         let mut connection = open_connection(&target)?;
     74         bootstrap_connection(&mut connection, &target)?;
     75 
     76         Ok(Self { connection })
     77     }
     78 
     79     pub fn connection(&self) -> &Connection {
     80         &self.connection
     81     }
     82 
     83     pub fn into_connection(self) -> Connection {
     84         self.connection
     85     }
     86 
     87     pub fn schema_version(&self) -> Result<u32, AppSqliteError> {
     88         schema_version(&self.connection)
     89     }
     90 
     91     pub fn today_agenda_repository(&self) -> AppTodayAgendaRepository<'_> {
     92         AppTodayAgendaRepository::new(&self.connection)
     93     }
     94 
     95     pub fn activity_repository(&self) -> AppActivityRepository<'_> {
     96         AppActivityRepository::new(&self.connection)
     97     }
     98 
     99     pub fn activation_repository(&self) -> AppActivationRepository<'_> {
    100         AppActivationRepository::new(&self.connection)
    101     }
    102 
    103     pub fn farm_setup_repository(&self) -> AppFarmSetupRepository<'_> {
    104         AppFarmSetupRepository::new(&self.connection)
    105     }
    106 
    107     pub fn farm_rules_repository(&self) -> AppFarmRulesRepository<'_> {
    108         AppFarmRulesRepository::new(&self.connection)
    109     }
    110 
    111     pub fn buyer_repository(&self) -> AppBuyerRepository<'_> {
    112         AppBuyerRepository::new(&self.connection)
    113     }
    114 
    115     pub fn products_repository(&self) -> AppProductsRepository<'_> {
    116         AppProductsRepository::new(&self.connection)
    117     }
    118 
    119     pub fn orders_repository(&self) -> AppOrdersRepository<'_> {
    120         AppOrdersRepository::new(&self.connection)
    121     }
    122 
    123     pub fn sync_repository(&self) -> AppSyncRepository<'_> {
    124         AppSyncRepository::new(&self.connection)
    125     }
    126 
    127     pub fn sdk_migration_receipt_repository(&self) -> AppSdkMigrationReceiptRepository<'_> {
    128         AppSdkMigrationReceiptRepository::new(&self.connection)
    129     }
    130 
    131     pub fn reminders_repository(&self) -> AppRemindersRepository<'_> {
    132         AppRemindersRepository::new(&self.connection)
    133     }
    134 
    135     pub fn load_today_agenda(
    136         &self,
    137         farm_id: Option<FarmId>,
    138     ) -> Result<TodayAgendaProjection, AppSqliteError> {
    139         self.today_agenda_repository().load(farm_id)
    140     }
    141 
    142     pub fn save_farm_summary(&self, farm: &FarmSummary) -> Result<(), AppSqliteError> {
    143         self.today_agenda_repository().save_farm_summary(farm)
    144     }
    145 
    146     pub fn record_activity_event(&self, kind: &AppActivityKind) -> Result<(), AppSqliteError> {
    147         self.activity_repository().record(kind)
    148     }
    149 
    150     pub fn load_recent_activity_events(
    151         &self,
    152         limit: usize,
    153     ) -> Result<Vec<AppActivityEvent>, AppSqliteError> {
    154         self.activity_repository().load_recent(limit)
    155     }
    156 
    157     pub fn load_activity_context(
    158         &self,
    159         limit: usize,
    160     ) -> Result<AppActivityContext, AppSqliteError> {
    161         self.activity_repository().load_context(limit)
    162     }
    163 
    164     pub fn load_surface_activation(
    165         &self,
    166         account_id: &str,
    167     ) -> Result<Option<AccountSurfaceActivationProjection>, AppSqliteError> {
    168         self.activation_repository()
    169             .load_surface_activation(account_id)
    170     }
    171 
    172     pub fn save_surface_activation(
    173         &self,
    174         projection: &AccountSurfaceActivationProjection,
    175     ) -> Result<(), AppSqliteError> {
    176         self.activation_repository()
    177             .save_surface_activation(projection)
    178     }
    179 
    180     pub fn clear_surface_activation(&self, account_id: &str) -> Result<(), AppSqliteError> {
    181         self.activation_repository()
    182             .clear_surface_activation(account_id)
    183     }
    184 
    185     pub fn load_farm_setup(&self, account_id: &str) -> Result<FarmSetupProjection, AppSqliteError> {
    186         self.farm_setup_repository().load_farm_setup(account_id)
    187     }
    188 
    189     pub fn save_farm_setup(
    190         &self,
    191         account_id: &str,
    192         projection: &FarmSetupProjection,
    193     ) -> Result<(), AppSqliteError> {
    194         self.farm_setup_repository()
    195             .save_farm_setup(account_id, projection)
    196     }
    197 
    198     pub fn clear_farm_setup(&self, account_id: &str) -> Result<(), AppSqliteError> {
    199         self.farm_setup_repository().clear_farm_setup(account_id)
    200     }
    201 
    202     pub fn load_farm_rules(&self, farm_id: FarmId) -> Result<FarmRulesProjection, AppSqliteError> {
    203         self.farm_rules_repository().load_farm_rules(farm_id)
    204     }
    205 
    206     pub fn save_farm_rules(&self, projection: &FarmRulesProjection) -> Result<(), AppSqliteError> {
    207         self.farm_rules_repository().save_farm_rules(projection)
    208     }
    209 
    210     pub fn load_products(
    211         &self,
    212         farm_id: FarmId,
    213         search_query: &str,
    214         filter: ProductsFilter,
    215         sort: ProductsSort,
    216     ) -> Result<ProductsListProjection, AppSqliteError> {
    217         self.products_repository()
    218             .load_products(farm_id, search_query, filter, sort)
    219     }
    220 
    221     pub fn load_product_editor_draft(
    222         &self,
    223         product_id: ProductId,
    224     ) -> Result<Option<ProductEditorDraft>, AppSqliteError> {
    225         self.products_repository()
    226             .load_product_editor_draft(product_id)
    227     }
    228 
    229     pub fn create_product_draft(&self, farm_id: FarmId) -> Result<ProductId, AppSqliteError> {
    230         self.products_repository().create_product_draft(farm_id)
    231     }
    232 
    233     pub fn load_orders_list(
    234         &self,
    235         farm_id: FarmId,
    236         query: &OrdersScreenQueryState,
    237     ) -> Result<OrdersListProjection, AppSqliteError> {
    238         self.orders_repository().load_orders_list(farm_id, query)
    239     }
    240 
    241     pub fn load_order_detail(
    242         &self,
    243         farm_id: FarmId,
    244         order_id: OrderId,
    245     ) -> Result<Option<OrderDetailProjection>, AppSqliteError> {
    246         self.orders_repository()
    247             .load_order_detail(farm_id, order_id)
    248     }
    249 
    250     pub fn load_seller_order_decision_export(
    251         &self,
    252         farm_id: FarmId,
    253         order_id: OrderId,
    254     ) -> Result<Option<SellerOrderDecisionExport>, AppSqliteError> {
    255         self.orders_repository()
    256             .load_seller_order_decision_export(farm_id, order_id)
    257     }
    258 
    259     pub fn load_pack_day(
    260         &self,
    261         farm_id: FarmId,
    262         query: &PackDayScreenQueryState,
    263     ) -> Result<PackDayProjection, AppSqliteError> {
    264         self.orders_repository().load_pack_day(farm_id, query)
    265     }
    266 
    267     pub fn load_pack_day_output_source(
    268         &self,
    269         farm_id: FarmId,
    270         fulfillment_window_id: FulfillmentWindowId,
    271     ) -> Result<Option<PackDayOutputSource>, AppSqliteError> {
    272         self.orders_repository()
    273             .load_pack_day_output_source(farm_id, fulfillment_window_id)
    274     }
    275 
    276     pub fn load_reminder_schedule(
    277         &self,
    278         account_id: &str,
    279         farm_id: FarmId,
    280     ) -> Result<ReminderFeedProjection, AppSqliteError> {
    281         self.reminders_repository()
    282             .load_reminder_schedule(account_id, farm_id)
    283     }
    284 
    285     pub fn replace_reminder_schedule(
    286         &self,
    287         account_id: &str,
    288         farm_id: FarmId,
    289         projection: &ReminderFeedProjection,
    290     ) -> Result<(), AppSqliteError> {
    291         self.reminders_repository()
    292             .replace_reminder_schedule(account_id, farm_id, projection)
    293     }
    294 
    295     pub fn apply_reminder_schedule_update(
    296         &self,
    297         account_id: &str,
    298         farm_id: FarmId,
    299         projection: &ReminderFeedProjection,
    300         log_entries: &[ReminderLogEntryProjection],
    301     ) -> Result<(), AppSqliteError> {
    302         self.reminders_repository().apply_reminder_schedule_update(
    303             account_id,
    304             farm_id,
    305             projection,
    306             log_entries,
    307         )
    308     }
    309 
    310     pub fn record_reminder_log_entry(
    311         &self,
    312         account_id: &str,
    313         farm_id: FarmId,
    314         entry: &ReminderLogEntryProjection,
    315     ) -> Result<String, AppSqliteError> {
    316         self.reminders_repository()
    317             .record_reminder_log_entry(account_id, farm_id, entry)
    318     }
    319 
    320     pub fn load_reminder_log(
    321         &self,
    322         account_id: &str,
    323         farm_id: FarmId,
    324         limit: usize,
    325     ) -> Result<ReminderLogProjection, AppSqliteError> {
    326         self.reminders_repository()
    327             .load_reminder_log(account_id, farm_id, limit)
    328     }
    329 
    330     pub fn save_product_editor_draft(
    331         &self,
    332         product_id: ProductId,
    333         draft: &ProductEditorDraft,
    334     ) -> Result<bool, AppSqliteError> {
    335         self.products_repository()
    336             .save_product_editor_draft(product_id, draft)
    337     }
    338 
    339     pub fn update_product_stock(
    340         &self,
    341         product_id: ProductId,
    342         stock_quantity: u32,
    343     ) -> Result<bool, AppSqliteError> {
    344         self.products_repository()
    345             .update_product_stock(product_id, stock_quantity)
    346     }
    347 
    348     pub fn evaluate_product_publish_blockers(
    349         &self,
    350         product_id: ProductId,
    351     ) -> Result<Option<Vec<ProductPublishBlocker>>, AppSqliteError> {
    352         self.products_repository()
    353             .evaluate_product_publish_blockers(product_id)
    354     }
    355 
    356     pub fn load_buyer_listings(
    357         &self,
    358         search_query: &str,
    359         fulfillment_methods: &BTreeSet<FarmOrderMethod>,
    360     ) -> Result<BuyerListingsProjection, AppSqliteError> {
    361         self.buyer_repository()
    362             .load_buyer_listings(search_query, fulfillment_methods)
    363     }
    364 
    365     pub fn load_buyer_product_detail(
    366         &self,
    367         product_id: ProductId,
    368     ) -> Result<Option<BuyerProductDetailProjection>, AppSqliteError> {
    369         self.buyer_repository()
    370             .load_buyer_product_detail(product_id)
    371     }
    372 
    373     pub fn load_buyer_cart(
    374         &self,
    375         context: &BuyerContext,
    376     ) -> Result<BuyerCartProjection, AppSqliteError> {
    377         self.buyer_repository().load_buyer_cart(context)
    378     }
    379 
    380     pub fn replace_buyer_cart(
    381         &self,
    382         context: &BuyerContext,
    383         cart: &BuyerCartProjection,
    384     ) -> Result<(), AppSqliteError> {
    385         self.buyer_repository().replace_buyer_cart(context, cart)
    386     }
    387 
    388     pub fn clear_buyer_cart(&self, context: &BuyerContext) -> Result<(), AppSqliteError> {
    389         self.buyer_repository().clear_buyer_cart(context)
    390     }
    391 
    392     pub fn load_buyer_order_review(
    393         &self,
    394         context: &BuyerContext,
    395     ) -> Result<BuyerOrderReviewProjection, AppSqliteError> {
    396         self.buyer_repository().load_buyer_order_review(context)
    397     }
    398 
    399     pub fn save_buyer_order_review_draft(
    400         &self,
    401         context: &BuyerContext,
    402         draft: &BuyerOrderReviewDraft,
    403     ) -> Result<(), AppSqliteError> {
    404         self.buyer_repository()
    405             .save_buyer_order_review_draft(context, draft)
    406     }
    407 
    408     pub fn place_buyer_order(&self, context: &BuyerContext) -> Result<OrderId, AppSqliteError> {
    409         self.buyer_repository().place_buyer_order(context)
    410     }
    411 
    412     pub fn load_buyer_orders(
    413         &self,
    414         context: &BuyerContext,
    415     ) -> Result<BuyerOrdersProjection, AppSqliteError> {
    416         self.buyer_repository().load_buyer_orders(context)
    417     }
    418 
    419     pub fn load_buyer_orders_for_scope(
    420         &self,
    421         scope: &SelectedBuyerOrderScope,
    422     ) -> Result<BuyerOrdersProjection, AppSqliteError> {
    423         self.buyer_repository().load_buyer_orders_for_scope(scope)
    424     }
    425 
    426     pub fn load_buyer_order_detail(
    427         &self,
    428         context: &BuyerContext,
    429         order_id: OrderId,
    430     ) -> Result<Option<BuyerOrderDetailProjection>, AppSqliteError> {
    431         self.buyer_repository()
    432             .load_buyer_order_detail(context, order_id)
    433     }
    434 
    435     pub fn load_buyer_order_detail_for_scope(
    436         &self,
    437         scope: &SelectedBuyerOrderScope,
    438         order_id: OrderId,
    439     ) -> Result<Option<BuyerOrderDetailProjection>, AppSqliteError> {
    440         self.buyer_repository()
    441             .load_buyer_order_detail_for_scope(scope, order_id)
    442     }
    443 
    444     pub fn load_buyer_order_local_event_export(
    445         &self,
    446         context: &BuyerContext,
    447         order_id: OrderId,
    448     ) -> Result<Option<BuyerOrderLocalEventExport>, AppSqliteError> {
    449         self.buyer_repository()
    450             .load_buyer_order_local_event_export(context, order_id)
    451     }
    452 
    453     pub fn load_buyer_order_coordination_record(
    454         &self,
    455         context: &BuyerContext,
    456         order_id: OrderId,
    457     ) -> Result<Option<BuyerOrderCoordinationRecord>, AppSqliteError> {
    458         self.buyer_repository()
    459             .load_buyer_order_coordination_record(context, order_id)
    460     }
    461 
    462     pub fn load_recoverable_buyer_order_coordination_records(
    463         &self,
    464         context: &BuyerContext,
    465     ) -> Result<Vec<BuyerOrderCoordinationRecord>, AppSqliteError> {
    466         self.buyer_repository()
    467             .load_recoverable_buyer_order_coordination_records(context)
    468     }
    469 
    470     pub fn buyer_order_coordination_is_synced(
    471         &self,
    472         context: &BuyerContext,
    473         order_id: OrderId,
    474     ) -> Result<bool, AppSqliteError> {
    475         self.buyer_repository()
    476             .buyer_order_coordination_is_synced(context, order_id)
    477     }
    478 
    479     pub fn prepare_buyer_order_coordination_attempt(
    480         &self,
    481         context: &BuyerContext,
    482         order_id: OrderId,
    483         record_id: &str,
    484         payload_json: &str,
    485     ) -> Result<bool, AppSqliteError> {
    486         self.buyer_repository()
    487             .prepare_buyer_order_coordination_attempt(context, order_id, record_id, payload_json)
    488     }
    489 
    490     pub fn mark_buyer_order_coordination_synced(
    491         &self,
    492         context: &BuyerContext,
    493         order_id: OrderId,
    494     ) -> Result<bool, AppSqliteError> {
    495         self.buyer_repository()
    496             .mark_buyer_order_coordination_synced(context, order_id)
    497     }
    498 
    499     pub fn mark_buyer_order_coordination_failed(
    500         &self,
    501         context: &BuyerContext,
    502         order_id: OrderId,
    503         error_message: &str,
    504     ) -> Result<bool, AppSqliteError> {
    505         self.buyer_repository()
    506             .mark_buyer_order_coordination_failed(context, order_id, error_message)
    507     }
    508 
    509     pub fn apply_buyer_repeat_demand_to_cart(
    510         &self,
    511         context: &BuyerContext,
    512         order_id: OrderId,
    513         replace_existing: bool,
    514     ) -> Result<BuyerRepeatDemandApplyOutcome, AppSqliteError> {
    515         self.buyer_repository().apply_buyer_repeat_demand_to_cart(
    516             context,
    517             order_id,
    518             replace_existing,
    519         )
    520     }
    521 
    522     pub fn apply_buyer_repeat_demand_from_scope_to_cart(
    523         &self,
    524         source_scope: &SelectedBuyerOrderScope,
    525         cart_context: &BuyerContext,
    526         order_id: OrderId,
    527         replace_existing: bool,
    528     ) -> Result<BuyerRepeatDemandApplyOutcome, AppSqliteError> {
    529         self.buyer_repository()
    530             .apply_buyer_repeat_demand_from_scope_to_cart(
    531                 source_scope,
    532                 cart_context,
    533                 order_id,
    534                 replace_existing,
    535             )
    536     }
    537 
    538     pub fn enqueue_pending_sync_operation(
    539         &self,
    540         account_id: &str,
    541         operation: &PendingSyncOperation,
    542     ) -> Result<String, AppSqliteError> {
    543         self.sync_repository()
    544             .enqueue_pending_operation(account_id, operation)
    545     }
    546 
    547     pub fn load_pending_sync_operations(
    548         &self,
    549         account_id: &str,
    550     ) -> Result<Vec<StoredPendingSyncOperation>, AppSqliteError> {
    551         self.sync_repository().load_pending_operations(account_id)
    552     }
    553 
    554     pub fn update_pending_sync_operation_retry(
    555         &self,
    556         account_id: &str,
    557         operation_id: &str,
    558         available_at: &str,
    559         attempt_count: u32,
    560         last_error_message: Option<&str>,
    561     ) -> Result<bool, AppSqliteError> {
    562         self.sync_repository().update_pending_operation_retry(
    563             account_id,
    564             operation_id,
    565             available_at,
    566             attempt_count,
    567             last_error_message,
    568         )
    569     }
    570 
    571     pub fn dequeue_pending_sync_operation(
    572         &self,
    573         account_id: &str,
    574         operation_id: &str,
    575     ) -> Result<bool, AppSqliteError> {
    576         self.sync_repository()
    577             .dequeue_pending_operation(account_id, operation_id)
    578     }
    579 
    580     pub fn load_sync_checkpoint(
    581         &self,
    582         account_id: &str,
    583     ) -> Result<SyncCheckpointStatus, AppSqliteError> {
    584         self.sync_repository().load_checkpoint(account_id)
    585     }
    586 
    587     pub fn save_sync_checkpoint(
    588         &self,
    589         account_id: &str,
    590         checkpoint: &SyncCheckpointStatus,
    591     ) -> Result<(), AppSqliteError> {
    592         self.sync_repository()
    593             .save_checkpoint(account_id, checkpoint)
    594     }
    595 
    596     pub fn load_relay_ingest_cursors(
    597         &self,
    598         scope_key: &str,
    599         relay_urls: &[String],
    600     ) -> Result<Vec<StoredRelayIngestCursor>, AppSqliteError> {
    601         self.sync_repository()
    602             .load_relay_ingest_cursors(scope_key, relay_urls)
    603     }
    604 
    605     pub fn load_relay_ingest_freshness(
    606         &self,
    607         scope_key: &str,
    608         relay_urls: &[String],
    609         now_unix_seconds: i64,
    610         stale_after_seconds: i64,
    611     ) -> Result<AppRelayIngestScopeFreshness, AppSqliteError> {
    612         self.sync_repository().load_relay_ingest_freshness(
    613             scope_key,
    614             relay_urls,
    615             now_unix_seconds,
    616             stale_after_seconds,
    617         )
    618     }
    619 
    620     pub fn record_relay_ingest_success(
    621         &self,
    622         scope_key: &str,
    623         relay_url: &str,
    624         cursor_since_unix_seconds: i64,
    625         last_event_created_at_unix_seconds: Option<i64>,
    626         started_at: &str,
    627         started_unix_seconds: i64,
    628         completed_at: &str,
    629         completed_unix_seconds: i64,
    630     ) -> Result<(), AppSqliteError> {
    631         self.sync_repository().record_relay_ingest_success(
    632             scope_key,
    633             relay_url,
    634             cursor_since_unix_seconds,
    635             last_event_created_at_unix_seconds,
    636             started_at,
    637             started_unix_seconds,
    638             completed_at,
    639             completed_unix_seconds,
    640         )
    641     }
    642 
    643     pub fn record_relay_ingest_failure(
    644         &self,
    645         scope_key: &str,
    646         relay_url: &str,
    647         started_at: &str,
    648         started_unix_seconds: i64,
    649         completed_at: &str,
    650         completed_unix_seconds: i64,
    651         error_message: &str,
    652     ) -> Result<(), AppSqliteError> {
    653         self.sync_repository().record_relay_ingest_failure(
    654             scope_key,
    655             relay_url,
    656             started_at,
    657             started_unix_seconds,
    658             completed_at,
    659             completed_unix_seconds,
    660             error_message,
    661         )
    662     }
    663 
    664     pub fn record_sync_conflict(
    665         &self,
    666         account_id: &str,
    667         conflict: &SyncConflict,
    668     ) -> Result<String, AppSqliteError> {
    669         self.sync_repository().record_conflict(account_id, conflict)
    670     }
    671 
    672     pub fn replace_sync_conflicts(
    673         &self,
    674         account_id: &str,
    675         conflicts: &[SyncConflict],
    676     ) -> Result<(), AppSqliteError> {
    677         self.sync_repository()
    678             .replace_conflicts(account_id, conflicts)
    679     }
    680 
    681     pub fn load_sync_conflicts(
    682         &self,
    683         account_id: &str,
    684     ) -> Result<Vec<StoredSyncConflict>, AppSqliteError> {
    685         self.sync_repository().load_conflicts(account_id)
    686     }
    687 
    688     pub fn resolve_sync_conflict(
    689         &self,
    690         account_id: &str,
    691         conflict_id: &str,
    692         resolution: SyncConflictResolutionStatus,
    693         resolved_at: &str,
    694     ) -> Result<bool, AppSqliteError> {
    695         self.sync_repository()
    696             .resolve_conflict(account_id, conflict_id, resolution, resolved_at)
    697     }
    698 }
    699 
    700 fn open_connection(target: &DatabaseTarget) -> Result<Connection, AppSqliteError> {
    701     match target {
    702         DatabaseTarget::InMemory => {
    703             Connection::open_in_memory().map_err(|source| AppSqliteError::OpenInMemory { source })
    704         }
    705         DatabaseTarget::Path(path) => {
    706             if let Some(parent) = path.parent() {
    707                 if !parent.as_os_str().is_empty() {
    708                     fs::create_dir_all(parent).map_err(|source| {
    709                         AppSqliteError::CreateParentDirectory {
    710                             path: parent.to_path_buf(),
    711                             source,
    712                         }
    713                     })?;
    714                 }
    715             }
    716 
    717             Connection::open(path).map_err(|source| AppSqliteError::OpenPath {
    718                 path: path.clone(),
    719                 source,
    720             })
    721         }
    722     }
    723 }
    724 
    725 fn bootstrap_connection(
    726     connection: &mut Connection,
    727     target: &DatabaseTarget,
    728 ) -> Result<(), AppSqliteError> {
    729     connection
    730         .busy_timeout(Duration::from_millis(SQLITE_BUSY_TIMEOUT_MS))
    731         .map_err(|source| AppSqliteError::ConfigureBusyTimeout { source })?;
    732 
    733     apply_pragma(connection, "foreign_keys", "ON")?;
    734     apply_pragma(connection, "synchronous", "NORMAL")?;
    735 
    736     if matches!(target, DatabaseTarget::Path(_)) {
    737         connection
    738             .query_row("PRAGMA journal_mode = WAL", [], |row| {
    739                 row.get::<_, String>(0)
    740             })
    741             .map_err(|source| AppSqliteError::ApplyPragma {
    742                 pragma: "journal_mode",
    743                 source,
    744             })?;
    745     }
    746 
    747     apply_migrations(connection)
    748 }
    749 
    750 fn apply_pragma(
    751     connection: &Connection,
    752     pragma: &'static str,
    753     value: &str,
    754 ) -> Result<(), AppSqliteError> {
    755     let sql = format!("PRAGMA {pragma} = {value}");
    756     connection
    757         .execute_batch(&sql)
    758         .map_err(|source| AppSqliteError::ApplyPragma { pragma, source })
    759 }
    760 
    761 fn schema_version(connection: &Connection) -> Result<u32, AppSqliteError> {
    762     connection
    763         .query_row("PRAGMA user_version", [], |row| row.get(0))
    764         .map_err(|source| AppSqliteError::ReadSchemaVersion { source })
    765 }
    766 
    767 fn apply_migrations(connection: &mut Connection) -> Result<(), AppSqliteError> {
    768     let current_version = schema_version(connection)?;
    769     let latest_version = migrations::latest_schema_version();
    770 
    771     if current_version > latest_version {
    772         return Err(AppSqliteError::UnsupportedSchemaVersion {
    773             current: current_version,
    774             latest: latest_version,
    775         });
    776     }
    777 
    778     for (version, sql) in migrations::pending_migrations(current_version) {
    779         let transaction = connection
    780             .transaction()
    781             .map_err(|source| AppSqliteError::BeginMigration { version, source })?;
    782 
    783         transaction
    784             .execute_batch(sql)
    785             .map_err(|source| AppSqliteError::ExecuteMigration { version, source })?;
    786         transaction
    787             .pragma_update(None, "user_version", version)
    788             .map_err(|source| AppSqliteError::RecordSchemaVersion { version, source })?;
    789         transaction
    790             .commit()
    791             .map_err(|source| AppSqliteError::CommitMigration { version, source })?;
    792     }
    793 
    794     Ok(())
    795 }
    796 
    797 #[cfg(test)]
    798 mod tests {
    799     use super::{AppSqliteStore, DatabaseTarget, latest_schema_version, migrations};
    800     use rusqlite::{Connection, params};
    801     use std::{
    802         env, fs,
    803         path::PathBuf,
    804         time::{SystemTime, UNIX_EPOCH},
    805     };
    806 
    807     #[test]
    808     fn file_store_bootstrap_applies_pragmas_and_migrations() {
    809         let path = temp_database_path("bootstrap");
    810         let store =
    811             AppSqliteStore::open(DatabaseTarget::Path(path.clone())).expect("store should open");
    812         let connection = store.connection();
    813 
    814         assert_eq!(
    815             store.schema_version().expect("schema version"),
    816             latest_schema_version()
    817         );
    818         assert_eq!(pragma_i64(connection, "foreign_keys"), 1);
    819         assert_eq!(pragma_text(connection, "journal_mode"), "wal");
    820         assert!(table_exists(connection, "farms"));
    821         assert!(table_exists(connection, "products"));
    822         assert!(table_exists(connection, "orders"));
    823         assert!(table_exists(connection, "local_outbox"));
    824         assert!(table_exists(connection, "local_conflicts"));
    825         assert!(table_exists(connection, "sync_checkpoints"));
    826         assert!(table_exists(connection, "app_relay_ingest_freshness"));
    827         assert!(table_exists(connection, "activity_events"));
    828         assert!(table_exists(connection, "account_surface_activations"));
    829         assert!(table_exists(connection, "account_farm_setups"));
    830         assert!(table_exists(connection, "farm_operating_rules"));
    831         assert!(table_exists(connection, "pickup_locations"));
    832         assert!(table_exists(connection, "blackout_periods"));
    833         assert!(table_exists(connection, "order_lines"));
    834         assert!(table_exists(connection, "buyer_carts"));
    835         assert!(table_exists(connection, "buyer_cart_lines"));
    836         assert!(table_exists(connection, "reminder_schedules"));
    837         assert!(table_exists(connection, "reminder_log_entries"));
    838         assert!(table_exists(connection, "buyer_order_coordination_records"));
    839         assert!(table_exists(connection, "order_validation_receipts"));
    840         assert!(table_exists(connection, "app_sdk_migration_receipts"));
    841         assert!(column_exists(connection, "farms", "timezone"));
    842         assert!(column_exists(connection, "farms", "currency_code"));
    843         assert!(column_exists(connection, "local_outbox", "account_id"));
    844         assert!(column_exists(connection, "local_outbox", "operation_key"));
    845         assert!(column_exists(connection, "local_outbox", "state"));
    846         assert!(column_exists(
    847             connection,
    848             "local_outbox",
    849             "last_error_message"
    850         ));
    851         assert!(column_exists(connection, "local_conflicts", "account_id"));
    852         assert!(column_exists(connection, "local_conflicts", "severity"));
    853         assert!(column_exists(
    854             connection,
    855             "local_conflicts",
    856             "resolution_status"
    857         ));
    858         assert!(column_exists(connection, "sync_checkpoints", "account_id"));
    859         assert!(column_exists(connection, "sync_checkpoints", "state"));
    860         assert!(column_exists(
    861             connection,
    862             "app_relay_ingest_freshness",
    863             "scope_key"
    864         ));
    865         assert!(column_exists(
    866             connection,
    867             "app_relay_ingest_freshness",
    868             "relay_url"
    869         ));
    870         assert!(column_exists(
    871             connection,
    872             "app_relay_ingest_freshness",
    873             "cursor_since_unix_seconds"
    874         ));
    875         assert!(column_exists(
    876             connection,
    877             "fulfillment_windows",
    878             "pickup_location_id"
    879         ));
    880         assert!(column_exists(connection, "fulfillment_windows", "label"));
    881         assert!(column_exists(
    882             connection,
    883             "fulfillment_windows",
    884             "order_cutoff_at"
    885         ));
    886         assert!(column_exists(connection, "order_lines", "quantity_value"));
    887         assert!(column_exists(
    888             connection,
    889             "order_lines",
    890             "quantity_unit_label"
    891         ));
    892         assert!(column_exists(connection, "order_lines", "quantity_display"));
    893         assert!(column_exists(connection, "order_lines", "listing_bin_id"));
    894         assert!(column_exists(
    895             connection,
    896             "order_lines",
    897             "unit_price_minor_units"
    898         ));
    899         assert!(column_exists(connection, "order_lines", "price_currency"));
    900         assert!(column_exists(connection, "order_lines", "listing_addr"));
    901         assert!(column_exists(
    902             connection,
    903             "order_lines",
    904             "listing_relays_json"
    905         ));
    906         assert!(column_exists(connection, "products", "category"));
    907         assert!(column_exists(connection, "products", "listing_bin_id"));
    908         assert!(column_exists(connection, "buyer_carts", "buyer_email"));
    909         assert!(column_exists(connection, "buyer_carts", "buyer_phone"));
    910         assert!(column_exists(connection, "buyer_carts", "buyer_order_note"));
    911         assert!(column_exists(
    912             connection,
    913             "buyer_cart_lines",
    914             "listing_bin_id"
    915         ));
    916         assert!(column_exists(
    917             connection,
    918             "buyer_cart_lines",
    919             "quantity_unit_label"
    920         ));
    921         assert!(column_exists(
    922             connection,
    923             "buyer_cart_lines",
    924             "unit_price_minor_units"
    925         ));
    926         assert!(column_exists(connection, "buyer_cart_lines", "farm_key"));
    927         assert!(column_exists(
    928             connection,
    929             "buyer_cart_lines",
    930             "listing_event_id"
    931         ));
    932         assert!(column_exists(
    933             connection,
    934             "buyer_cart_lines",
    935             "listing_relays_json"
    936         ));
    937         assert!(column_exists(connection, "orders", "buyer_context_key"));
    938         assert!(column_exists(connection, "orders", "buyer_email"));
    939         assert!(column_exists(connection, "orders", "buyer_phone"));
    940         assert!(column_exists(connection, "orders", "buyer_order_note"));
    941         assert!(column_exists(
    942             connection,
    943             "reminder_schedules",
    944             "account_id"
    945         ));
    946         assert!(column_exists(
    947             connection,
    948             "reminder_schedules",
    949             "delivery_state"
    950         ));
    951         assert!(column_exists(
    952             connection,
    953             "reminder_log_entries",
    954             "recorded_at"
    955         ));
    956         assert!(column_exists(
    957             connection,
    958             "buyer_order_coordination_records",
    959             "state"
    960         ));
    961         assert!(column_exists(
    962             connection,
    963             "buyer_order_coordination_records",
    964             "payload_json"
    965         ));
    966         assert!(column_exists(
    967             connection,
    968             "buyer_order_coordination_records",
    969             "last_error_message"
    970         ));
    971         assert!(column_exists(
    972             connection,
    973             "order_validation_receipts",
    974             "event_id"
    975         ));
    976         assert!(column_exists(
    977             connection,
    978             "order_validation_receipts",
    979             "order_id"
    980         ));
    981         assert!(column_exists(
    982             connection,
    983             "order_validation_receipts",
    984             "raw_order_id"
    985         ));
    986         assert!(column_exists(
    987             connection,
    988             "order_validation_receipts",
    989             "root_event_id"
    990         ));
    991         assert!(column_exists(
    992             connection,
    993             "order_validation_receipts",
    994             "target_event_id"
    995         ));
    996         assert!(column_exists(
    997             connection,
    998             "order_validation_receipts",
    999             "result"
   1000         ));
   1001         assert!(column_exists(
   1002             connection,
   1003             "order_validation_receipts",
   1004             "proof_system"
   1005         ));
   1006         assert!(column_exists(
   1007             connection,
   1008             "app_sdk_migration_receipts",
   1009             "source_record_id"
   1010         ));
   1011         assert!(column_exists(
   1012             connection,
   1013             "app_sdk_migration_receipts",
   1014             "source_kind"
   1015         ));
   1016         assert!(column_exists(
   1017             connection,
   1018             "app_sdk_migration_receipts",
   1019             "sdk_operation_kind"
   1020         ));
   1021         assert!(column_exists(
   1022             connection,
   1023             "app_sdk_migration_receipts",
   1024             "sdk_outbox_event_ids_json"
   1025         ));
   1026         assert!(column_exists(
   1027             connection,
   1028             "app_sdk_migration_receipts",
   1029             "expected_event_id"
   1030         ));
   1031         assert!(column_exists(
   1032             connection,
   1033             "app_sdk_migration_receipts",
   1034             "actor_pubkey"
   1035         ));
   1036         assert!(column_exists(
   1037             connection,
   1038             "app_sdk_migration_receipts",
   1039             "idempotency_digest_prefix"
   1040         ));
   1041         assert!(column_exists(
   1042             connection,
   1043             "app_sdk_migration_receipts",
   1044             "migration_state"
   1045         ));
   1046         assert!(column_exists(
   1047             connection,
   1048             "app_sdk_migration_receipts",
   1049             "detail_json"
   1050         ));
   1051         connection
   1052             .execute(
   1053                 "INSERT INTO local_interop_imports (
   1054                     record_id,
   1055                     local_seq,
   1056                     record_family,
   1057                     local_status,
   1058                     source_runtime,
   1059                     projected_kind,
   1060                     outbox_status,
   1061                     imported_at
   1062                  ) VALUES (
   1063                     'schema_validation_receipt_projection_kind',
   1064                     0,
   1065                     'signed_event',
   1066                     'published',
   1067                     'cli',
   1068                     'validation_receipt',
   1069                     'acknowledged',
   1070                     '2026-01-01T00:00:00Z'
   1071                  )",
   1072                 [],
   1073             )
   1074             .expect("local interop imports should accept validation receipt projections");
   1075         assert_eq!(row_count(connection, "sync_checkpoints"), 0);
   1076 
   1077         drop(store);
   1078         remove_database_artifacts(&path);
   1079     }
   1080 
   1081     #[test]
   1082     fn reopening_existing_store_is_idempotent() {
   1083         let path = temp_database_path("reopen");
   1084         AppSqliteStore::open(DatabaseTarget::Path(path.clone())).expect("first open should work");
   1085         let reopened = AppSqliteStore::open(DatabaseTarget::Path(path.clone()))
   1086             .expect("second open should work");
   1087 
   1088         assert_eq!(
   1089             reopened.schema_version().expect("schema version"),
   1090             latest_schema_version()
   1091         );
   1092         assert_eq!(row_count(reopened.connection(), "sync_checkpoints"), 0);
   1093 
   1094         drop(reopened);
   1095         remove_database_artifacts(&path);
   1096     }
   1097 
   1098     #[test]
   1099     fn in_memory_store_bootstraps_without_file_only_pragmas() {
   1100         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   1101 
   1102         assert_eq!(
   1103             store.schema_version().expect("schema version"),
   1104             latest_schema_version()
   1105         );
   1106         assert_eq!(pragma_i64(store.connection(), "foreign_keys"), 1);
   1107         assert!(table_exists(store.connection(), "farms"));
   1108     }
   1109 
   1110     #[test]
   1111     fn order_workflow_schema_is_agreement_only() {
   1112         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
   1113         let connection = store.connection();
   1114         assert!(column_exists(connection, "orders", "workflow_agreement"));
   1115         assert!(column_exists(connection, "orders", "workflow_inventory"));
   1116         assert!(column_exists(
   1117             connection,
   1118             "orders",
   1119             "workflow_provenance_source"
   1120         ));
   1121         assert!(!column_exists(connection, "orders", "workflow_fulfillment"));
   1122         assert!(!column_exists(connection, "orders", "workflow_payment"));
   1123         assert!(!column_exists(
   1124             connection,
   1125             "orders",
   1126             "workflow_receipt_event_id"
   1127         ));
   1128         connection
   1129             .execute(
   1130                 "INSERT INTO farms (id, display_name, readiness, created_at, updated_at)
   1131                  VALUES (?1, 'Schema Farm', 'ready', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')",
   1132                 params!["farm_schema"],
   1133             )
   1134             .expect("farm should insert");
   1135         connection
   1136             .execute(
   1137                 "INSERT INTO orders (
   1138                     id,
   1139                     farm_id,
   1140                     order_number,
   1141                     customer_display_name,
   1142                     status,
   1143                     updated_at,
   1144                     workflow_agreement,
   1145                     workflow_inventory
   1146                  ) VALUES (
   1147                     'order_needs_review',
   1148                     'farm_schema',
   1149                     'needs review',
   1150                     'Buyer',
   1151                     'needs_review',
   1152                     '2026-01-01T00:00:00Z',
   1153                     'needs_review',
   1154                     'needs_review'
   1155                  )",
   1156                 [],
   1157             )
   1158             .expect("agreement-only workflow projection should insert");
   1159 
   1160         let invalid_result = connection.execute(
   1161             "INSERT INTO orders (
   1162                 id,
   1163                 farm_id,
   1164                 order_number,
   1165                 customer_display_name,
   1166                 status,
   1167                 updated_at,
   1168                 workflow_agreement
   1169              ) VALUES ('order_agreement_invalid', 'farm_schema', 'invalid', 'Buyer', 'scheduled', '2026-01-01T00:00:00Z', 'complete')",
   1170             [],
   1171         );
   1172         assert!(invalid_result.is_err());
   1173     }
   1174 
   1175     #[test]
   1176     fn legacy_sync_scaffolding_migrates_to_account_scoped_contract() {
   1177         let path = temp_database_path("legacy-sync-contract");
   1178         fs::create_dir_all(path.parent().expect("temp database should have a parent"))
   1179             .expect("legacy database parent should exist");
   1180         let connection = Connection::open(&path).expect("legacy database should open");
   1181 
   1182         for (version, sql) in migrations::pending_migrations(0)
   1183             .filter(|(version, _)| *version < latest_schema_version())
   1184         {
   1185             connection
   1186                 .execute_batch(sql)
   1187                 .expect("legacy migration should apply");
   1188             connection
   1189                 .pragma_update(None, "user_version", version)
   1190                 .expect("legacy schema version should record");
   1191         }
   1192 
   1193         drop(connection);
   1194 
   1195         let store =
   1196             AppSqliteStore::open(DatabaseTarget::Path(path.clone())).expect("store should open");
   1197         let connection = store.connection();
   1198 
   1199         assert_eq!(
   1200             store.schema_version().expect("schema version"),
   1201             latest_schema_version()
   1202         );
   1203         assert!(column_exists(connection, "local_outbox", "account_id"));
   1204         assert!(column_exists(connection, "local_outbox", "operation_key"));
   1205         assert!(column_exists(connection, "local_outbox", "state"));
   1206         assert!(column_exists(connection, "local_conflicts", "severity"));
   1207         assert!(column_exists(
   1208             connection,
   1209             "local_conflicts",
   1210             "resolution_status"
   1211         ));
   1212         assert!(column_exists(connection, "sync_checkpoints", "state"));
   1213         assert!(table_exists(connection, "app_relay_ingest_freshness"));
   1214         assert_eq!(row_count(connection, "sync_checkpoints"), 0);
   1215 
   1216         drop(store);
   1217         remove_database_artifacts(&path);
   1218     }
   1219 
   1220     #[test]
   1221     fn legacy_orders_status_migration_preserves_child_rows_and_accepts_declined() {
   1222         let path = temp_database_path("legacy-declined-orders");
   1223         fs::create_dir_all(path.parent().expect("temp database should have a parent"))
   1224             .expect("legacy database parent should exist");
   1225         let connection = Connection::open(&path).expect("legacy database should open");
   1226         connection
   1227             .execute_batch("PRAGMA foreign_keys = ON")
   1228             .expect("foreign keys should enable");
   1229 
   1230         for (version, sql) in migrations::pending_migrations(0).filter(|(version, _)| *version < 20)
   1231         {
   1232             connection
   1233                 .execute_batch(sql)
   1234                 .expect("legacy migration should apply");
   1235             connection
   1236                 .pragma_update(None, "user_version", version)
   1237                 .expect("legacy schema version should record");
   1238         }
   1239 
   1240         connection
   1241             .execute(
   1242                 "INSERT INTO farms (id, display_name, readiness, created_at, updated_at)
   1243                  VALUES (?1, 'Legacy Farm', 'ready', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')",
   1244                 params!["farm_legacy"],
   1245             )
   1246             .expect("legacy farm should insert");
   1247         connection
   1248             .execute(
   1249                 "INSERT INTO orders (
   1250                     id,
   1251                     farm_id,
   1252                     fulfillment_window_id,
   1253                     order_number,
   1254                     customer_display_name,
   1255                     status,
   1256                     updated_at,
   1257                     buyer_context_key,
   1258                     buyer_email,
   1259                     buyer_phone,
   1260                     buyer_order_note
   1261                  ) VALUES (
   1262                     'order_legacy',
   1263                     'farm_legacy',
   1264                     NULL,
   1265                     'R-900',
   1266                     'Legacy Buyer',
   1267                     'needs_action',
   1268                     '2026-01-01T00:00:00Z',
   1269                     'account:buyer',
   1270                     '',
   1271                     '',
   1272                     ''
   1273                  )",
   1274                 [],
   1275             )
   1276             .expect("legacy order should insert");
   1277         connection
   1278             .execute(
   1279                 "INSERT INTO order_lines (
   1280                     id,
   1281                     order_id,
   1282                     title,
   1283                     quantity_value,
   1284                     quantity_display
   1285                  ) VALUES (
   1286                     'line_legacy',
   1287                     'order_legacy',
   1288                     'Legacy Eggs',
   1289                     2,
   1290                     '2 each'
   1291                  )",
   1292                 [],
   1293             )
   1294             .expect("legacy order line should insert");
   1295         connection
   1296             .execute(
   1297                 "INSERT INTO buyer_order_coordination_records (
   1298                     order_id,
   1299                     buyer_context_key,
   1300                     state,
   1301                     created_at,
   1302                     updated_at
   1303                  ) VALUES (
   1304                     'order_legacy',
   1305                     'account:buyer',
   1306                     'pending',
   1307                     '2026-01-01T00:00:00Z',
   1308                     '2026-01-01T00:00:00Z'
   1309                  )",
   1310                 [],
   1311             )
   1312             .expect("legacy buyer coordination should insert");
   1313 
   1314         drop(connection);
   1315 
   1316         let store =
   1317             AppSqliteStore::open(DatabaseTarget::Path(path.clone())).expect("store should open");
   1318         let connection = store.connection();
   1319 
   1320         assert_eq!(
   1321             store.schema_version().expect("schema version"),
   1322             latest_schema_version()
   1323         );
   1324         assert_eq!(row_count(connection, "orders"), 1);
   1325         assert_eq!(row_count(connection, "order_lines"), 1);
   1326         assert_eq!(row_count(connection, "buyer_order_coordination_records"), 1);
   1327         assert_eq!(foreign_key_violation_count(connection), 0);
   1328 
   1329         connection
   1330             .execute(
   1331                 "UPDATE orders SET status = 'declined' WHERE id = 'order_legacy'",
   1332                 [],
   1333             )
   1334             .expect("declined status should satisfy migrated check");
   1335         connection
   1336             .execute(
   1337                 "UPDATE orders SET status = 'needs_review' WHERE id = 'order_legacy'",
   1338                 [],
   1339             )
   1340             .expect("needs review status should satisfy migrated check");
   1341 
   1342         let status: String = connection
   1343             .query_row(
   1344                 "SELECT status FROM orders WHERE id = 'order_legacy'",
   1345                 [],
   1346                 |row| row.get(0),
   1347             )
   1348             .expect("status should load");
   1349         assert_eq!(status, "needs_review");
   1350 
   1351         drop(store);
   1352         remove_database_artifacts(&path);
   1353     }
   1354 
   1355     fn table_exists(connection: &Connection, table_name: &str) -> bool {
   1356         connection
   1357             .query_row(
   1358                 "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1)",
   1359                 [table_name],
   1360                 |row| row.get::<_, i64>(0),
   1361             )
   1362             .expect("table existence query should succeed")
   1363             == 1
   1364     }
   1365 
   1366     fn row_count(connection: &Connection, table_name: &str) -> i64 {
   1367         let sql = format!("SELECT COUNT(*) FROM {table_name}");
   1368         connection
   1369             .query_row(&sql, [], |row| row.get(0))
   1370             .expect("row count query should succeed")
   1371     }
   1372 
   1373     fn column_exists(connection: &Connection, table_name: &str, column_name: &str) -> bool {
   1374         let sql = format!("PRAGMA table_info({table_name})");
   1375         let mut statement = connection
   1376             .prepare(&sql)
   1377             .expect("table info statement should prepare");
   1378         let mut rows = statement
   1379             .query([])
   1380             .expect("table info query should succeed");
   1381 
   1382         while let Some(row) = rows.next().expect("table info row should load") {
   1383             if row
   1384                 .get::<_, String>(1)
   1385                 .expect("table info name should load")
   1386                 == column_name
   1387             {
   1388                 return true;
   1389             }
   1390         }
   1391 
   1392         false
   1393     }
   1394 
   1395     fn foreign_key_violation_count(connection: &Connection) -> usize {
   1396         let mut statement = connection
   1397             .prepare("PRAGMA foreign_key_check")
   1398             .expect("foreign key check should prepare");
   1399         let mut rows = statement.query([]).expect("foreign key check should run");
   1400         let mut count = 0;
   1401         while rows
   1402             .next()
   1403             .expect("foreign key check row should load")
   1404             .is_some()
   1405         {
   1406             count += 1;
   1407         }
   1408         count
   1409     }
   1410 
   1411     fn pragma_i64(connection: &Connection, pragma_name: &str) -> i64 {
   1412         let sql = format!("PRAGMA {pragma_name}");
   1413         connection
   1414             .query_row(&sql, [], |row| row.get(0))
   1415             .expect("pragma query should succeed")
   1416     }
   1417 
   1418     fn pragma_text(connection: &Connection, pragma_name: &str) -> String {
   1419         let sql = format!("PRAGMA {pragma_name}");
   1420         connection
   1421             .query_row(&sql, [], |row| row.get(0))
   1422             .expect("pragma query should succeed")
   1423     }
   1424 
   1425     fn temp_database_path(test_name: &str) -> PathBuf {
   1426         let nonce = SystemTime::now()
   1427             .duration_since(UNIX_EPOCH)
   1428             .expect("time should move forward")
   1429             .as_nanos();
   1430 
   1431         env::temp_dir()
   1432             .join("radroots_app_sqlite_tests")
   1433             .join(format!("{test_name}-{nonce}"))
   1434             .join("app.sqlite3")
   1435     }
   1436 
   1437     fn remove_database_artifacts(database_path: &std::path::Path) {
   1438         if let Some(parent) = database_path.parent() {
   1439             let wal_path = database_path.with_extension("sqlite3-wal");
   1440             let shm_path = database_path.with_extension("sqlite3-shm");
   1441 
   1442             let _ = fs::remove_file(&wal_path);
   1443             let _ = fs::remove_file(&shm_path);
   1444             let _ = fs::remove_file(database_path);
   1445             let _ = fs::remove_dir_all(parent);
   1446         }
   1447     }
   1448 }