app

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

commit f79415c78ea2080a3eb518023f307a3e53d91b7e
parent 7bc5a312248ea985861e342e6dc0e70ab1f50ccf
Author: triesap <tyson@radroots.org>
Date:   Wed, 15 Jul 2026 05:31:55 +0000

sqlite: move app store to SQLx

- Replace the app store rusqlite runtime with the shared SQLx bundled SQLite path.
- Port store repositories, migrations, interop imports, and workflow receipt persistence to SQLx row/query handling.
- Add store-owned SQLite drift guards, typed test-support SQL helpers, and clippy-clean relay ingest inputs.
- Align desktop runtime tests and typed ID generation with direct target-state APIs.

Diffstat:
MCargo.lock | 58++++++----------------------------------------------------
MCargo.toml | 3++-
Mcrates/desktop/Cargo.toml | 1+
Mcrates/desktop/src/accounts.rs | 12++++++------
Mcrates/desktop/src/pack_day_host_handoff.rs | 4++--
Mcrates/desktop/src/pack_day_print.rs | 10+++++-----
Mcrates/desktop/src/runtime.rs | 330+++++++++++++++++++++++++++++++++++--------------------------------------------
Mcrates/desktop/src/window.rs | 75+++++++++++++++++++++++++++++++++++++++------------------------------------
Mcrates/runtime/src/pack_day_export.rs | 14+++++++-------
Mcrates/state/src/lib.rs | 82++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/store/Cargo.toml | 6+++++-
Acrates/store/src/db.rs | 371+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/store/src/error.rs | 20++++++++++----------
Mcrates/store/src/interop.rs | 524++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mcrates/store/src/lib.rs | 232++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mcrates/store/src/repo/activation.rs | 31+++++++++++++++----------------
Mcrates/store/src/repo/activity.rs | 79+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/store/src/repo/buyer.rs | 464+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mcrates/store/src/repo/farm_rules.rs | 120++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mcrates/store/src/repo/farm_setup.rs | 39+++++++++++++++++++--------------------
Mcrates/store/src/repo/order_detail.rs | 26++++++++++++++------------
Mcrates/store/src/repo/orders.rs | 315++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcrates/store/src/repo/products.rs | 112++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mcrates/store/src/repo/reminders.rs | 158++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mcrates/store/src/repo/today.rs | 120++++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/store/src/sdk_workflow_receipts.rs | 44++++++++++++++++++++++++--------------------
Acrates/store/src/source_guards.rs | 170+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/store/src/sync.rs | 228++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mcrates/sync/src/lib.rs | 8++++----
Mcrates/sync/src/publish.rs | 24++++++++++++------------
Mcrates/types/src/lib.rs | 2+-
Mcrates/view/src/lib.rs | 90++++++++++++++++++++++++++++++++++++++++----------------------------------------
32 files changed, 2214 insertions(+), 1558 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1903,18 +1903,6 @@ dependencies = [ ] [[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - -[[package]] name = "fastrand" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -5159,12 +5147,13 @@ dependencies = [ name = "radroots_geocoder" version = "0.1.0-alpha.2" dependencies = [ + "futures-executor", "hex", "radroots_runtime_paths", "reqwest", - "rusqlite", "serde", "sha2", + "sqlx", "tempfile", "thiserror 1.0.69", "url", @@ -5376,9 +5365,10 @@ name = "radroots_sql_core" version = "0.1.0-alpha.2" dependencies = [ "chrono", - "rusqlite", + "futures-executor", "serde", "serde_json", + "sqlx", "uuid", ] @@ -5469,6 +5459,7 @@ dependencies = [ name = "radroots_studio_app_sqlite" version = "0.1.0" dependencies = [ + "futures-executor", "radroots_core", "radroots_event", "radroots_event_codec", @@ -5477,8 +5468,8 @@ dependencies = [ "radroots_studio_app_sync", "radroots_studio_app_view", "radroots_trade", - "rusqlite", "serde_json", + "sqlx", "thiserror 2.0.18", "uuid", ] @@ -5944,31 +5935,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] -name = "rsqlite-vfs" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" -dependencies = [ - "hashbrown 0.16.1", - "thiserror 2.0.18", -] - -[[package]] -name = "rusqlite" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d2b0146dd9661bf67bb107c0bb2a55064d556eeb3fc314151b957f313bcd4e" -dependencies = [ - "bitflags 2.11.1", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink 0.11.1", - "libsqlite3-sys", - "smallvec", - "sqlite-wasm-rs", -] - -[[package]] name = "rust-embed" version = "8.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -6726,18 +6692,6 @@ dependencies = [ ] [[package]] -name = "sqlite-wasm-rs" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" -dependencies = [ - "cc", - "js-sys", - "rsqlite-vfs", - "wasm-bindgen", -] - -[[package]] name = "sqlx" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -26,6 +26,7 @@ readme = "README.md" [workspace.dependencies] chrono = { version = "0.4", default-features = false, features = ["clock"] } +futures-executor = "0.3" futures-util = "0.3" gpui = "0.2.2" gpui-component = "0.5.1" @@ -54,9 +55,9 @@ radroots_studio_app_sqlite = { path = "crates/store", version = "0.1.0" } radroots_studio_app_state = { path = "crates/state", version = "0.1.0" } radroots_studio_app_sync = { path = "crates/sync", version = "0.1.0" } radroots_studio_app_ui = { path = "crates/ui", version = "0.1.0" } -rusqlite = { version = "0.39.0", features = ["bundled"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +sqlx = { version = "0.9.0", default-features = false, features = ["derive", "sqlite-bundled"] } thiserror = "2" tokio = { version = "1", features = ["macros", "net", "rt", "sync"] } tokio-tungstenite = "0.26.2" diff --git a/crates/desktop/Cargo.toml b/crates/desktop/Cargo.toml @@ -42,6 +42,7 @@ tracing.workspace = true uuid.workspace = true [dev-dependencies] +radroots_studio_app_sqlite = { workspace = true, features = ["test-support"] } tracing-subscriber.workspace = true [lints] diff --git a/crates/desktop/src/accounts.rs b/crates/desktop/src/accounts.rs @@ -292,7 +292,7 @@ fn default_farmer_surface_activation(account_id: &str) -> AccountSurfaceActivati AccountSurfaceActivationProjection::new( account_id, SelectedSurfaceProjection::new(ActiveSurface::Farmer), - FarmerActivationProjection::active(FarmId::new()), + FarmerActivationProjection::active(FarmId::generate()), ) } @@ -454,7 +454,7 @@ mod tests { account_id.as_str(), SelectedSurfaceProjection::new(ActiveSurface::Farmer), radroots_studio_app_view::FarmerActivationProjection::active( - radroots_studio_app_view::FarmId::new(), + radroots_studio_app_view::FarmId::generate(), ), ); sqlite_store @@ -592,7 +592,7 @@ mod tests { second_account_id.as_str(), SelectedSurfaceProjection::new(ActiveSurface::Farmer), radroots_studio_app_view::FarmerActivationProjection::active( - radroots_studio_app_view::FarmId::new(), + radroots_studio_app_view::FarmId::generate(), ), ); sqlite_store @@ -653,7 +653,7 @@ mod tests { first_account_id.as_str(), SelectedSurfaceProjection::new(ActiveSurface::Farmer), radroots_studio_app_view::FarmerActivationProjection::active( - radroots_studio_app_view::FarmId::new(), + radroots_studio_app_view::FarmId::generate(), ), ); sqlite_store @@ -703,7 +703,7 @@ mod tests { first_account_id.as_str(), SelectedSurfaceProjection::new(ActiveSurface::Farmer), radroots_studio_app_view::FarmerActivationProjection::active( - radroots_studio_app_view::FarmId::new(), + radroots_studio_app_view::FarmId::generate(), ), )) .expect("first activation should save"); @@ -712,7 +712,7 @@ mod tests { second_account_id.as_str(), SelectedSurfaceProjection::new(ActiveSurface::Farmer), radroots_studio_app_view::FarmerActivationProjection::active( - radroots_studio_app_view::FarmId::new(), + radroots_studio_app_view::FarmId::generate(), ), )) .expect("second activation should save"); diff --git a/crates/desktop/src/pack_day_host_handoff.rs b/crates/desktop/src/pack_day_host_handoff.rs @@ -348,8 +348,8 @@ mod tests { fn sample_bundle(bundle_directory: &PathBuf) -> PackDayExportBundle { PackDayExportBundle { - fulfillment_window_id: radroots_studio_app_view::FulfillmentWindowId::new(), - export_instance_id: radroots_studio_app_view::PackDayExportInstanceId::new(), + fulfillment_window_id: radroots_studio_app_view::FulfillmentWindowId::generate(), + export_instance_id: radroots_studio_app_view::PackDayExportInstanceId::generate(), generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), bundle_directory: bundle_directory.to_string_lossy().into_owned(), artifacts: vec![ diff --git a/crates/desktop/src/pack_day_print.rs b/crates/desktop/src/pack_day_print.rs @@ -936,8 +936,8 @@ mod tests { fn sample_bundle(bundle_directory: &PathBuf) -> PackDayExportBundle { PackDayExportBundle { - fulfillment_window_id: radroots_studio_app_view::FulfillmentWindowId::new(), - export_instance_id: PackDayExportInstanceId::new(), + fulfillment_window_id: radroots_studio_app_view::FulfillmentWindowId::generate(), + export_instance_id: PackDayExportInstanceId::generate(), generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), bundle_directory: bundle_directory.to_string_lossy().into_owned(), artifacts: vec![ @@ -1285,7 +1285,7 @@ mod tests { write_all_artifacts(temp_dir.path()); let bundle = sample_bundle(temp_dir.path()); let mut request = sample_batch_request(&bundle); - request.export_instance_id = PackDayExportInstanceId::new(); + request.export_instance_id = PackDayExportInstanceId::generate(); let artifacts = request.artifacts.clone(); let error = plan_pack_day_batch_print(&bundle, &request) @@ -1434,7 +1434,7 @@ mod tests { #[test] fn batch_execution_rejects_empty_command_plan_without_submitting_artifacts() { let plan = PackDayBatchPrintCommandPlan { - export_instance_id: PackDayExportInstanceId::new(), + export_instance_id: PackDayExportInstanceId::generate(), plans: Vec::new(), }; let mut submitted = false; @@ -1576,7 +1576,7 @@ mod tests { #[test] fn cleanup_prepared_customer_label_asset_root_removes_existing_directories() { let root = prepared_customer_label_asset_root(); - let stale_directory = root.join(PackDayExportInstanceId::new().to_string()); + let stale_directory = root.join(PackDayExportInstanceId::generate().to_string()); fs::create_dir_all(&stale_directory).expect("stale prepared directory should create"); fs::write(stale_directory.join("stale.ps"), "stale").expect("stale asset should write"); diff --git a/crates/desktop/src/runtime.rs b/crates/desktop/src/runtime.rs @@ -58,7 +58,7 @@ use radroots_sdk::{ TRADE_REVISION_DECISION_OPERATION_KIND, TRADE_REVISION_PROPOSAL_OPERATION_KIND, TRADE_SUBMIT_OPERATION_KIND, TargetSet, }; -use radroots_sql_core::SqliteExecutor; +use radroots_sql_core::SqlxSqliteExecutor; use radroots_studio_app_core::{ AppBuildIdentity, AppDesktopRuntimePaths, AppRuntimeCapture, AppRuntimeMode, AppRuntimePathsError, AppRuntimeSnapshot, AppSdkConfig, AppSdkDiagnostics, @@ -75,10 +75,11 @@ use radroots_studio_app_remote_signer::{ RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession, }; use radroots_studio_app_sqlite::{ - APP_ACTIVITY_CONTEXT_LIMIT, AppLocalInteropImportReport, AppSdkWorkflowReceiptInput, - AppSdkWorkflowReceiptSourceKind, AppSdkWorkflowReceiptState, AppSqliteError, AppSqliteStore, - BuyerOrderRuntimeStoreExport, BuyerOrderRuntimeStoreLine, BuyerRepeatDemandApplyOutcome, - DatabaseTarget, SelectedBuyerOrderScope, SellerOrderDecisionExport, StoredPendingSyncOperation, + APP_ACTIVITY_CONTEXT_LIMIT, AppLocalInteropImportReport, AppRelayIngestFailureInput, + AppRelayIngestSuccessInput, AppSdkWorkflowReceiptInput, AppSdkWorkflowReceiptSourceKind, + AppSdkWorkflowReceiptState, AppSqliteError, AppSqliteStore, BuyerOrderRuntimeStoreExport, + BuyerOrderRuntimeStoreLine, BuyerRepeatDemandApplyOutcome, DatabaseTarget, + SelectedBuyerOrderScope, SellerOrderDecisionExport, StoredPendingSyncOperation, StoredRelayIngestCursor, StoredSyncConflict, derive_farm_rules_readiness, projected_order_id_from_trade_request, }; @@ -4220,27 +4221,27 @@ impl DesktopAppRuntimeState { .map_or(started_unix_seconds, |last_event_created_at| { started_unix_seconds.max(last_event_created_at) }); - sqlite_store.record_relay_ingest_success( - APP_DIRECT_RELAY_INGEST_SCOPE_KEY, - relay.relay_url.as_str(), + sqlite_store.record_relay_ingest_success(AppRelayIngestSuccessInput { + scope_key: APP_DIRECT_RELAY_INGEST_SCOPE_KEY, + relay_url: relay.relay_url.as_str(), cursor_since_unix_seconds, - relay.last_event_created_at_unix_seconds, + last_event_created_at_unix_seconds: relay.last_event_created_at_unix_seconds, started_at, started_unix_seconds, completed_at, completed_unix_seconds, - )?; + })?; } for failure in &receipt.failed_relays { - sqlite_store.record_relay_ingest_failure( - APP_DIRECT_RELAY_INGEST_SCOPE_KEY, - failure.relay_url.as_str(), + sqlite_store.record_relay_ingest_failure(AppRelayIngestFailureInput { + scope_key: APP_DIRECT_RELAY_INGEST_SCOPE_KEY, + relay_url: failure.relay_url.as_str(), started_at, started_unix_seconds, completed_at, completed_unix_seconds, - failure.error.as_str(), - )?; + error_message: failure.error.as_str(), + })?; } Ok(()) @@ -5910,7 +5911,7 @@ impl DesktopAppRuntimeState { fn open_shared_runtime_store_store( &self, - ) -> Result<Option<RuntimeStore<SqliteExecutor>>, AppSqliteError> { + ) -> Result<Option<RuntimeStore<SqlxSqliteExecutor>>, AppSqliteError> { let Some(shared_accounts_paths) = self.shared_accounts_paths.as_ref() else { return Ok(None); }; @@ -5925,7 +5926,7 @@ impl DesktopAppRuntimeState { source, })?; } - let executor = SqliteExecutor::open(database_path.as_path()).map_err(|source| { + let executor = SqlxSqliteExecutor::open(database_path.as_path()).map_err(|source| { AppSqliteError::RuntimeStoreSql { operation: "open shared runtime store database", source, @@ -6246,7 +6247,7 @@ impl DesktopAppRuntimeState { source, })?; } - let executor = SqliteExecutor::open(database_path.as_path()).map_err(|source| { + let executor = SqlxSqliteExecutor::open(database_path.as_path()).map_err(|source| { AppSqliteError::RuntimeStoreSql { operation: "open shared runtime store database", source, @@ -6289,7 +6290,7 @@ impl DesktopAppRuntimeState { source, })?; } - let executor = SqliteExecutor::open(database_path.as_path()).map_err(|source| { + let executor = SqlxSqliteExecutor::open(database_path.as_path()).map_err(|source| { AppSqliteError::RuntimeStoreSql { operation: "open shared runtime store database", source, @@ -9880,7 +9881,7 @@ mod tests { TRADE_DECISION_OPERATION_KIND, TRADE_REVISION_DECISION_OPERATION_KIND, TRADE_SUBMIT_OPERATION_KIND, }; - use radroots_sql_core::{SqlExecutor, SqliteExecutor}; + use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor}; use radroots_studio_app_core::{ AppDesktopRuntimePaths, AppRuntimeHostEnvironment, AppRuntimePlatform, AppSdkLifecycleState, AppSdkProjectionLifecycleState, AppSdkPublicFarmLocation, @@ -10169,8 +10170,8 @@ mod tests { farm_pubkey: String, source: &str, ) -> AppPublishPayload { - let farm_id = FarmId::new(); - let product_id = ProductId::new(); + let farm_id = FarmId::generate(); + let product_id = ProductId::generate(); AppPublishPayload::Listing(AppListingPublishPayload { context: AppPublishContext::new(account_id.to_owned(), source), product_id, @@ -10185,7 +10186,7 @@ mod tests { price_minor_units: Some(750), price_currency: "USD".to_owned(), stock_quantity: Some(12), - availability_window_id: Some(FulfillmentWindowId::new()), + availability_window_id: Some(FulfillmentWindowId::generate()), availability_starts_at: Some("2099-05-25T14:00:00Z".to_owned()), availability_ends_at: Some("2099-05-25T18:00:00Z".to_owned()), fulfillment_method: Some("pickup".to_owned()), @@ -10463,7 +10464,7 @@ mod tests { let account_id = manager .generate_identity(Some("Farmer".to_owned()), true) .expect("local signing account should generate"); - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); let payload = AppPublishPayload::FarmProfile(AppFarmProfilePublishPayload { context: AppPublishContext::new(account_id.to_string(), "farm_setup") .with_source_local_event_id("app:local_work:farm:direct"), @@ -10538,13 +10539,13 @@ mod tests { .expect("buyer signer lookup should succeed") .expect("buyer account should have local signer"); let seller_identity = RadrootsIdentity::generate(); - let product_id = ProductId::new(); - let order_id = OrderId::new(); + let product_id = ProductId::generate(); + let order_id = OrderId::generate(); let listing_event_id = "1".repeat(64); let listing_addr = format!( "30402:{}:{}", seller_identity.public_key_hex(), - super::d_tag_from_uuid(ProductId::new().as_uuid()) + super::d_tag_from_uuid(ProductId::generate().as_uuid()) ); let order_document = RadrootsOrderRequest { order_id: test_order_id(order_id.to_string().as_str()), @@ -10584,7 +10585,7 @@ mod tests { context: AppPublishContext::new(account_id.to_string(), "place_personal_order") .with_source_local_event_id("app:local_work:order_request:direct"), order_id, - farm_id: FarmId::new(), + farm_id: FarmId::generate(), status: Some("needs_action".to_owned()), order_document_json: Some(json!({"document": {"order": order_document}})), listing_addr: Some(listing_addr), @@ -10633,8 +10634,8 @@ mod tests { let buyer_pubkey = "1111111111111111111111111111111111111111111111111111111111111111"; let payload = AppPublishPayload::OrderDecision(AppOrderDecisionPublishPayload { context: AppPublishContext::new(account_id.to_string(), "seller_order_decision"), - app_order_id: OrderId::new(), - farm_id: FarmId::new(), + app_order_id: OrderId::generate(), + farm_id: FarmId::generate(), trade_order_id: "order-1".to_owned(), request_event_id: test_event_id_seed("order-request-event-1"), listing_event_id: Some(test_event_id_seed("listing-event-1")), @@ -10685,8 +10686,8 @@ mod tests { .get_signing_identity(&seller_account_id) .expect("seller signer lookup should succeed") .expect("seller account should have local signer"); - let app_order_id = OrderId::new(); - let farm_id = FarmId::new(); + let app_order_id = OrderId::generate(); + let farm_id = FarmId::generate(); let listing_addr = format!( "30402:{}:AAAAAAAAAAAAAAAAAAAAAg", seller_identity.public_key_hex() @@ -10908,8 +10909,8 @@ mod tests { .expect("seller signing lookup should succeed") .expect("seller account should have local signer"); let seller_pubkey = identity.public_key_hex(); - let farm_id = FarmId::new(); - let product_id = ProductId::new(); + let farm_id = FarmId::generate(); + let product_id = ProductId::generate(); let listing_d_tag = super::d_tag_from_uuid(product_id.as_uuid()); let projected_product_id = deterministic_cli_listing_product_id( Some(seller_pubkey.as_str()), @@ -10929,7 +10930,7 @@ mod tests { price_minor_units: Some(450), price_currency: "USD".to_owned(), stock_quantity: Some(6), - availability_window_id: Some(FulfillmentWindowId::new()), + availability_window_id: Some(FulfillmentWindowId::generate()), availability_starts_at: Some("2099-04-25T14:00:00Z".to_owned()), availability_ends_at: Some("2099-04-25T18:00:00Z".to_owned()), fulfillment_method: Some("pickup".to_owned()), @@ -11117,7 +11118,7 @@ mod tests { .account .account_id .clone(); - let pending_farm_id = FarmId::new(); + let pending_farm_id = FarmId::generate(); runtime .lock_state_mut() .enqueue_selected_account_sync_operations(vec![pending_sync_upsert( @@ -11220,7 +11221,7 @@ mod tests { PendingSyncOperation::from_publish_payload(payload, "2026-05-24T12:00:00Z") .expect("typed listing publish work should serialize"); let unsupported_operation = PendingSyncOperation::new( - SyncAggregateRef::Product(ProductId::new()), + SyncAggregateRef::Product(ProductId::generate()), SyncOperationKind::Delete, "{}", "2026-05-24T12:01:00Z", @@ -11316,8 +11317,8 @@ mod tests { let seller_pubkey = "2222222222222222222222222222222222222222222222222222222222222222"; let payload = AppPublishPayload::OrderRequest(AppOrderRequestPublishPayload { context: AppPublishContext::new(account_id.to_string(), "order_missing_listing_relay"), - order_id: OrderId::new(), - farm_id: FarmId::new(), + order_id: OrderId::generate(), + farm_id: FarmId::generate(), status: Some("needs_action".to_owned()), order_document_json: Some(json!({"document": {"order": {}}})), listing_addr: Some(format!("30402:{seller_pubkey}:listing-key")), @@ -11326,7 +11327,7 @@ mod tests { buyer_pubkey: Some(identity.public_key_hex()), seller_pubkey: Some(seller_pubkey.to_owned()), items: vec![AppOrderRequestItemPayload { - product_id: ProductId::new(), + product_id: ProductId::generate(), quantity: 1, }], currency_code: Some("USD".to_owned()), @@ -11390,7 +11391,7 @@ mod tests { fn runtime_direct_relay_transport_rejects_missing_account_publish_work() { let relay = ThreadedAckRelay::spawn(); let manager = RadrootsNostrAccountsManager::new_in_memory(); - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); let missing_account_id = RadrootsIdentity::generate().id(); let payload = AppPublishPayload::FarmProfile(AppFarmProfilePublishPayload { context: AppPublishContext::new(missing_account_id.to_string(), "farm_setup"), @@ -11426,7 +11427,7 @@ mod tests { .expect("watch-only account"); let payload = AppPublishPayload::FarmProfile(AppFarmProfilePublishPayload { context: AppPublishContext::new(account_id.to_string(), "farm_setup"), - farm_id: FarmId::new(), + farm_id: FarmId::generate(), display_name: "North field farm".to_owned(), readiness: Some(FarmReadiness::Ready), }); @@ -11472,7 +11473,7 @@ mod tests { .expect("mismatched secret"); let payload = AppPublishPayload::FarmProfile(AppFarmProfilePublishPayload { context: AppPublishContext::new(account_id.to_string(), "farm_setup"), - farm_id: FarmId::new(), + farm_id: FarmId::generate(), display_name: "North field farm".to_owned(), readiness: Some(FarmReadiness::Ready), }); @@ -11811,8 +11812,8 @@ mod tests { fn runtime_product_publishable_save_enqueues_typed_listing_publish_work() { let (runtime, paths) = bootstrapped_runtime("publishable_product_listing_work"); let (account_id, farm_id) = provision_ready_farmer_account(&runtime); - let pickup_location_id = PickupLocationId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); + let pickup_location_id = PickupLocationId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); runtime .save_farm_rules_projection(FarmRulesProjection { @@ -11958,8 +11959,8 @@ mod tests { fn runtime_product_publishable_save_returns_error_when_sdk_listing_enqueue_fails() { let (runtime, paths) = bootstrapped_runtime("publishable_product_listing_sdk_failure"); let (_account_id, farm_id) = provision_ready_farmer_account(&runtime); - let pickup_location_id = PickupLocationId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); + let pickup_location_id = PickupLocationId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); runtime .save_farm_rules_projection(FarmRulesProjection { @@ -12128,8 +12129,8 @@ mod tests { fn runtime_product_stock_update_retries_sdk_listing_enqueue_after_local_save() { let (runtime, paths) = bootstrapped_runtime("stock_listing_sdk_retry"); let (_account_id, farm_id) = provision_ready_farmer_account(&runtime); - let pickup_location_id = PickupLocationId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); + let pickup_location_id = PickupLocationId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); runtime .save_farm_rules_projection(FarmRulesProjection { @@ -12275,9 +12276,9 @@ mod tests { fn runtime_product_stale_availability_save_records_blocker_without_publish_work() { let (runtime, paths) = bootstrapped_runtime("stale_product_listing_work"); let (account_id, farm_id) = provision_ready_farmer_account(&runtime); - let pickup_location_id = PickupLocationId::new(); - let active_window_id = FulfillmentWindowId::new(); - let stale_window_id = FulfillmentWindowId::new(); + let pickup_location_id = PickupLocationId::generate(); + let active_window_id = FulfillmentWindowId::generate(); + let stale_window_id = FulfillmentWindowId::generate(); runtime .save_farm_rules_projection(FarmRulesProjection { @@ -12335,8 +12336,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch("PRAGMA foreign_keys = OFF;") + .execute_test_sql("PRAGMA foreign_keys = OFF;") .expect("foreign keys should disable for stale fixture"); let save_result = runtime.save_product_editor_draft(ProductEditorDraft { title: "Salad mix".to_owned(), @@ -12354,8 +12354,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch("PRAGMA foreign_keys = ON;") + .execute_test_sql("PRAGMA foreign_keys = ON;") .expect("foreign keys should restore"); assert!(save_result.expect("stale product editor save should succeed")); @@ -12586,7 +12585,7 @@ mod tests { fn runtime_partial_sync_result_dequeues_successful_prefix_only() { let runtime = memory_runtime(); let (account_id, farm_id) = provision_ready_farmer_account(&runtime); - let product_id = ProductId::new(); + let product_id = ProductId::generate(); runtime .lock_state_mut() .enqueue_selected_account_sync_operations(vec![ @@ -13257,8 +13256,8 @@ mod tests { let database_path = paths .shared_runtime_store_database_path() .expect("shared runtime store path"); - let executor = - SqliteExecutor::open(database_path.as_path()).expect("open shared runtime store db"); + let executor = SqlxSqliteExecutor::open(database_path.as_path()) + .expect("open shared runtime store db"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate shared runtime store"); store @@ -13299,7 +13298,7 @@ mod tests { #[test] fn runtime_app_local_work_without_resolved_pubkey_is_non_exportable() { let (runtime, paths) = bootstrapped_runtime("app_local_work_unresolved_pubkey"); - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); let account = SelectedAccountProjection::new( AccountSummary { account_id: "acct_unresolved".to_owned(), @@ -13329,7 +13328,7 @@ mod tests { .expect("unresolved farm local work should append"); state .append_app_listing_local_work_record( - ProductId::new(), + ProductId::generate(), &ProductEditorDraft { title: "Eggs".to_owned(), subtitle: "Fresh eggs".to_owned(), @@ -14086,8 +14085,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update products set availability_window_id = '{fulfillment_window_id}' where id = '{product_id}'" @@ -14200,8 +14198,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update orders set status = 'packed', updated_at = '2026-04-20T09:45:00Z' where id = '{order_id}' and farm_id = '{farm_id}'" @@ -14252,7 +14249,7 @@ mod tests { ); let mut persisted_state = runtime.lock_state().state_store.persisted_state().clone(); persisted_state.seller.orders_query.fulfillment_window_id = - Some(FulfillmentWindowId::new()); + Some(FulfillmentWindowId::generate()); let mut repository = FileBackedAppStateRepository::new(paths.app.data.join(APP_STATE_FILE_NAME)); repository @@ -14287,7 +14284,7 @@ mod tests { assert!(runtime.open_pack_day(None).expect("pack day should open")); let mut persisted_state = runtime.lock_state().state_store.persisted_state().clone(); - let stale_fulfillment_window_id = FulfillmentWindowId::new(); + let stale_fulfillment_window_id = FulfillmentWindowId::generate(); persisted_state.seller.pack_day_query.fulfillment_window_id = Some(stale_fulfillment_window_id); let mut repository = @@ -14346,12 +14343,12 @@ mod tests { let cloned_runtime = runtime.clone(); let today_agenda = TodayAgendaProjection { farm: Some(FarmSummary { - farm_id: radroots_studio_app_view::FarmId::new(), + farm_id: radroots_studio_app_view::FarmId::generate(), display_name: "North field farm".to_owned(), readiness: FarmReadiness::Incomplete, }), summary: Some(TodaySummary { - farm_id: radroots_studio_app_view::FarmId::new(), + farm_id: radroots_studio_app_view::FarmId::generate(), orders_needing_action: 2, low_stock_products: 1, draft_products: 3, @@ -14546,8 +14543,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch("DROP TABLE activity_events") + .execute_test_sql("DROP TABLE activity_events") .expect("activity table should drop"); assert!(matches!( @@ -14701,8 +14697,8 @@ mod tests { fn runtime_personal_search_queries_refresh_repository_backed_marketplace_projection() { let runtime = memory_runtime(); let (account_id, farm_id) = provision_ready_farmer_account(&runtime); - let pickup_location_id = PickupLocationId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); + let pickup_location_id = PickupLocationId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let sql = format!( "insert into pickup_locations ( id, @@ -14762,8 +14758,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&sql) + .execute_test_sql(&sql) .expect("buyer search workspace should seed"); let salad_mix_id = seed_product( &runtime, @@ -14788,8 +14783,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update products set availability_window_id = '{fulfillment_window_id}' where id in ('{salad_mix_id}', '{pea_shoots_id}')" @@ -14875,8 +14869,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update products set availability_window_id = '{fulfillment_window_id}' where id = '{product_id}'" @@ -14969,8 +14962,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update products set availability_window_id = '{first_window_id}' where id = '{first_product_id}'" @@ -14987,7 +14979,7 @@ mod tests { .expect("first buyer product should add to cart") ); - let other_farm_id = FarmId::new(); + let other_farm_id = FarmId::generate(); runtime .lock_state() .sqlite_store @@ -15020,8 +15012,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update products set availability_window_id = '{second_window_id}' where id = '{second_product_id}'" @@ -15136,8 +15127,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update products set availability_window_id = '{fulfillment_window_id}' where id = '{product_id}'" @@ -15211,8 +15201,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update products set availability_window_id = '{fulfillment_window_id}' where id = '{product_id}'" @@ -15306,7 +15295,7 @@ mod tests { #[test] fn runtime_guest_order_review_requires_account_before_order_write() { let runtime = memory_runtime(); - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); runtime .lock_state() .sqlite_store @@ -15344,8 +15333,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update products set availability_window_id = '{fulfillment_window_id}' where id = '{product_id}'" @@ -15416,20 +15404,14 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .query_row("select count(*) from orders", [], |row| row.get(0)) + .query_test_i64("select count(*) from orders") .expect("order count should load"); let coordination_count: i64 = runtime .lock_state() .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .query_row( - "select count(*) from buyer_order_coordination_records", - [], - |row| row.get(0), - ) + .query_test_i64("select count(*) from buyer_order_coordination_records") .expect("coordination count should load"); assert_eq!(order_count, 0); assert_eq!(coordination_count, 0); @@ -15797,11 +15779,9 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute( - "update products set listing_bin_id = 'mutated-bin' where id = ?1", - [product_id.to_string()], - ) + .execute_test_sql(&format!( + "update products set listing_bin_id = 'mutated-bin' where id = '{product_id}'" + )) .expect("listing projection should mutate after cart snapshot"); assert!( runtime @@ -15969,11 +15949,9 @@ mod tests { let state = runtime.lock_state_mut(); let sqlite_store = state.sqlite_store.as_ref().expect("sqlite store"); sqlite_store - .connection() - .execute( - "update orders set status = 'scheduled' where id = ?1", - [order_id.to_string()], - ) + .execute_test_sql(&format!( + "update orders set status = 'scheduled' where id = '{order_id}'" + )) .expect("buyer order status should mutate before retry refresh"); } unblock_shared_runtime_store_database(&paths); @@ -16217,8 +16195,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update products set availability_window_id = '{fulfillment_window_id}' where id = '{product_id}'" @@ -16591,8 +16568,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&format!( + .execute_test_sql(&format!( "update products set availability_window_id = '{fulfillment_window_id}' where id in ('{available_product_id}', '{unavailable_product_id}')" @@ -16641,11 +16617,9 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute( - "update products set status = 'archived' where id = ?1", - [unavailable_product_id.to_string()], - ) + .execute_test_sql(&format!( + "update products set status = 'archived' where id = '{unavailable_product_id}'" + )) .expect("product should archive"); assert!( @@ -16989,7 +16963,7 @@ mod tests { quantity_display: "Do not trust screen strings".to_owned(), }], pickup_roster: vec![PackDayRosterRow { - order_id: OrderId::new(), + order_id: OrderId::generate(), order_number: "R-999".to_owned(), customer_display_name: "Bogus".to_owned(), }], @@ -17045,7 +17019,7 @@ mod tests { fn runtime_bootstrap_sweeps_prepared_pack_day_print_assets() { let paths = temp_desktop_runtime_paths("pack_day_print_bootstrap_sweep"); let stale_root = prepared_customer_label_asset_root(); - let stale_directory = stale_root.join(PackDayExportInstanceId::new().to_string()); + let stale_directory = stale_root.join(PackDayExportInstanceId::generate().to_string()); let _ = fs::remove_file(&stale_root); let _ = fs::remove_dir_all(&stale_root); fs::create_dir_all(&stale_directory).expect("stale prepared directory should create"); @@ -18248,8 +18222,8 @@ mod tests { let runtime = memory_runtime(); let (_, farm_id) = provision_ready_farmer_account(&runtime); let (fulfillment_window_id, order_id) = seed_order_workspace(&runtime, farm_id); - let other_fulfillment_window_id = FulfillmentWindowId::new(); - let other_order_id = OrderId::new(); + let other_fulfillment_window_id = FulfillmentWindowId::generate(); + let other_order_id = OrderId::generate(); let sql = format!( "insert into fulfillment_windows ( id, @@ -18299,8 +18273,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&sql) + .execute_test_sql(&sql) .expect("second orders workspace should seed"); assert!( @@ -18329,8 +18302,8 @@ mod tests { let runtime = memory_runtime(); let (_, farm_id) = provision_ready_farmer_account(&runtime); let (fulfillment_window_id, scheduled_order_id) = seed_order_workspace(&runtime, farm_id); - let packed_order_id = OrderId::new(); - let completed_order_id = OrderId::new(); + let packed_order_id = OrderId::generate(); + let completed_order_id = OrderId::generate(); let sql = format!( "update orders @@ -18376,8 +18349,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&sql) + .execute_test_sql(&sql) .expect("order should update to scheduled"); assert!( @@ -19222,10 +19194,10 @@ mod tests { .expect("account should select") ); - let default_pickup_location_id = PickupLocationId::new(); - let market_pickup_location_id = PickupLocationId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); - let blackout_period_id = BlackoutPeriodId::new(); + let default_pickup_location_id = PickupLocationId::generate(); + let market_pickup_location_id = PickupLocationId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let blackout_period_id = BlackoutPeriodId::generate(); let saved_projection = runtime .save_farm_rules_projection(radroots_studio_app_view::FarmRulesProjection { @@ -19628,8 +19600,8 @@ mod tests { if let Some(parent) = database_path.parent() { fs::create_dir_all(parent).expect("shared runtime store directory should create"); } - let executor = - SqliteExecutor::open(database_path.as_path()).expect("open shared runtime store db"); + let executor = SqlxSqliteExecutor::open(database_path.as_path()) + .expect("open shared runtime store db"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate shared runtime store"); let farm_key = "AAAAAAAAAAAAAAAAAAAAAA"; @@ -19745,8 +19717,8 @@ mod tests { if let Some(parent) = database_path.parent() { fs::create_dir_all(parent).expect("shared runtime store directory should create"); } - let executor = - SqliteExecutor::open(database_path.as_path()).expect("open shared runtime store db"); + let executor = SqlxSqliteExecutor::open(database_path.as_path()) + .expect("open shared runtime store db"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate shared runtime store"); let farm_key = "CCCCCCCCCCCCCCCCCCCCCC"; @@ -19892,8 +19864,8 @@ mod tests { .expect("selected buyer account should resolve") .public_identity .public_key_hex; - let farm_key = super::d_tag_from_uuid(FarmId::new().as_uuid()); - let listing_key = super::d_tag_from_uuid(ProductId::new().as_uuid()); + let farm_key = super::d_tag_from_uuid(FarmId::generate().as_uuid()); + let listing_key = super::d_tag_from_uuid(ProductId::generate().as_uuid()); let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); let listing_event_id = signed_listing_event_id(label); let trade_order_id = format!("{label}-trade-order"); @@ -19972,7 +19944,7 @@ mod tests { .public_identity .public_key_hex; let buyer_pubkey = SDK_TEST_BUYER_PUBLIC_KEY_HEX.to_owned(); - let product_id = ProductId::new(); + let product_id = ProductId::generate(); let trade_order_id = "seller-order-decision-1"; let farm_key = super::d_tag_from_uuid(farm_id.as_uuid()); let listing_key = super::d_tag_from_uuid(product_id.as_uuid()); @@ -20038,7 +20010,7 @@ mod tests { .public_identity .public_key_hex; let buyer_pubkey = SDK_TEST_BUYER_PUBLIC_KEY_HEX.to_owned(); - let product_id = ProductId::new(); + let product_id = ProductId::generate(); let trade_order_id = "seller-order-decision-1"; let farm_key = super::d_tag_from_uuid(farm_id.as_uuid()); let listing_key = super::d_tag_from_uuid(product_id.as_uuid()); @@ -20171,8 +20143,8 @@ mod tests { if let Some(parent) = database_path.parent() { fs::create_dir_all(parent).expect("shared runtime store directory should create"); } - let executor = - SqliteExecutor::open(database_path.as_path()).expect("open shared runtime store db"); + let executor = SqlxSqliteExecutor::open(database_path.as_path()) + .expect("open shared runtime store db"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate shared runtime store"); let listing_addr = format!("30402:{seller_pubkey}:{listing_key}"); @@ -20277,8 +20249,8 @@ mod tests { if let Some(parent) = database_path.parent() { fs::create_dir_all(parent).expect("shared runtime store directory should create"); } - let executor = - SqliteExecutor::open(database_path.as_path()).expect("open shared runtime store db"); + let executor = SqlxSqliteExecutor::open(database_path.as_path()) + .expect("open shared runtime store db"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate shared runtime store"); let order = RadrootsOrderRequest { @@ -20374,8 +20346,8 @@ mod tests { if let Some(parent) = database_path.parent() { fs::create_dir_all(parent).expect("shared runtime store directory should create"); } - let executor = - SqliteExecutor::open(database_path.as_path()).expect("open shared runtime store db"); + let executor = SqlxSqliteExecutor::open(database_path.as_path()) + .expect("open shared runtime store db"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate shared runtime store"); let order = RadrootsOrderRequest { @@ -20660,8 +20632,8 @@ mod tests { if let Some(parent) = database_path.parent() { fs::create_dir_all(parent).expect("shared runtime store directory should create"); } - let executor = - SqliteExecutor::open(database_path.as_path()).expect("open shared runtime store db"); + let executor = SqlxSqliteExecutor::open(database_path.as_path()) + .expect("open shared runtime store db"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate shared runtime store"); let created_at = test_event_created_at(record_id, 1_774_000_020); @@ -20725,8 +20697,8 @@ mod tests { let database_path = paths .shared_runtime_store_database_path() .expect("shared runtime store path"); - let executor = - SqliteExecutor::open(database_path.as_path()).expect("open shared runtime store db"); + let executor = SqlxSqliteExecutor::open(database_path.as_path()) + .expect("open shared runtime store db"); executor .exec( "UPDATE runtime_store_record @@ -20744,8 +20716,8 @@ mod tests { let database_path = paths .shared_runtime_store_database_path() .expect("shared runtime store path"); - let executor = - SqliteExecutor::open(database_path.as_path()).expect("open shared runtime store db"); + let executor = SqlxSqliteExecutor::open(database_path.as_path()) + .expect("open shared runtime store db"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate shared runtime store"); let pubkey = "2222222222222222222222222222222222222222222222222222222222222222"; @@ -20928,8 +20900,8 @@ mod tests { let database_path = paths .shared_runtime_store_database_path() .expect("shared runtime store path"); - let executor = - SqliteExecutor::open(database_path.as_path()).expect("open shared runtime store db"); + let executor = SqlxSqliteExecutor::open(database_path.as_path()) + .expect("open shared runtime store db"); let store = RuntimeStore::new(executor); store .list_records_after_seq(0, 100) @@ -20973,12 +20945,9 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .query_row( - "select status from orders where id = ?1 limit 1", - [order_id.to_string()], - |row| row.get::<_, String>(0), - ) + .query_test_string(&format!( + "select status from orders where id = '{order_id}' limit 1" + )) .expect("order status should load") } @@ -20991,12 +20960,9 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .query_row( - "select workflow_agreement from orders where id = ?1 limit 1", - [order_id.to_string()], - |row| row.get::<_, String>(0), - ) + .query_test_string(&format!( + "select workflow_agreement from orders where id = '{order_id}' limit 1" + )) .expect("order workflow agreement should load") } @@ -21345,7 +21311,7 @@ mod tests { account_id, SelectedSurfaceProjection::new(active_surface), if farmer_active { - FarmerActivationProjection::active(FarmId::new()) + FarmerActivationProjection::active(FarmId::generate()) } else { FarmerActivationProjection::inactive() }, @@ -21364,7 +21330,7 @@ mod tests { account_id: &str, active_surface: ActiveSurface, ) -> FarmId { - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); let activation = AccountSurfaceActivationProjection::new( account_id, SelectedSurfaceProjection::new(active_surface), @@ -21389,7 +21355,7 @@ mod tests { stock_count: Option<u32>, updated_at: &str, ) -> radroots_studio_app_view::ProductId { - let product_id = radroots_studio_app_view::ProductId::new(); + let product_id = radroots_studio_app_view::ProductId::generate(); let stock_count = stock_count .map(|value| value.to_string()) .unwrap_or_else(|| "null".to_owned()); @@ -21432,8 +21398,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&sql) + .execute_test_sql(&sql) .expect("product should seed"); product_id @@ -21446,8 +21411,8 @@ mod tests { farm_display_name: &str, fulfillment_label: &str, ) -> FulfillmentWindowId { - let pickup_location_id = PickupLocationId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); + let pickup_location_id = PickupLocationId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let sql = format!( "insert into pickup_locations ( id, @@ -21530,8 +21495,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&sql) + .execute_test_sql(&sql) .expect("buyer marketplace support should seed"); fulfillment_window_id @@ -21609,9 +21573,9 @@ mod tests { runtime: &DesktopAppRuntime, farm_id: FarmId, ) -> (FulfillmentWindowId, OrderId) { - let pickup_location_id = PickupLocationId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); - let order_id = OrderId::new(); + let pickup_location_id = PickupLocationId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let order_id = OrderId::generate(); let sql = format!( "insert into pickup_locations ( id, @@ -21695,8 +21659,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&sql) + .execute_test_sql(&sql) .expect("orders workspace should seed"); (fulfillment_window_id, order_id) @@ -21707,8 +21670,8 @@ mod tests { farm_id: FarmId, source_fulfillment_window_id: FulfillmentWindowId, ) -> (FulfillmentWindowId, OrderId) { - let fulfillment_window_id = FulfillmentWindowId::new(); - let order_id = OrderId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let order_id = OrderId::generate(); let sql = format!( "insert into fulfillment_windows ( id, @@ -21758,8 +21721,7 @@ mod tests { .sqlite_store .as_ref() .expect("sqlite store") - .connection() - .execute_batch(&sql) + .execute_test_sql(&sql) .expect("second orders workspace should seed"); (fulfillment_window_id, order_id) diff --git a/crates/desktop/src/window.rs b/crates/desktop/src/window.rs @@ -6350,7 +6350,7 @@ impl SettingsFarmPanelState { fn add_pickup_location(&mut self, window: &mut Window, cx: &mut Context<SettingsWindowView>) { let record = PickupLocationRecord { - pickup_location_id: PickupLocationId::new(), + pickup_location_id: PickupLocationId::generate(), farm_id: self.farm_id, label: String::new(), address_line: String::new(), @@ -6400,7 +6400,7 @@ impl SettingsFarmPanelState { .map(|pickup_location| pickup_location.pickup_location_id); let fulfillment_window = SettingsFulfillmentWindowFormState::new( &SettingsFulfillmentWindowDraft { - fulfillment_window_id: FulfillmentWindowId::new(), + fulfillment_window_id: FulfillmentWindowId::generate(), selected_pickup_location_id, label: String::new(), starts_at: String::new(), @@ -6445,7 +6445,7 @@ impl SettingsFarmPanelState { fn add_blackout_period(&mut self, window: &mut Window, cx: &mut Context<SettingsWindowView>) { let blackout_period = SettingsBlackoutPeriodFormState::new( &SettingsBlackoutPeriodDraft { - blackout_period_id: BlackoutPeriodId::new(), + blackout_period_id: BlackoutPeriodId::generate(), label: String::new(), starts_at: String::new(), ends_at: String::new(), @@ -16387,7 +16387,7 @@ mod tests { impl TestDirectory { fn new() -> Self { - let path = std::env::temp_dir().join(FulfillmentWindowId::new().to_string()); + let path = std::env::temp_dir().join(FulfillmentWindowId::generate().to_string()); fs::create_dir_all(&path).unwrap(); Self { path } } @@ -16504,8 +16504,8 @@ mod tests { #[test] fn buyer_order_detail_focus_reopens_same_selected_detail() { - let order_id = OrderId::new(); - let farm_id = FarmId::new(); + let order_id = OrderId::generate(); + let farm_id = FarmId::generate(); let mut runtime = summary( HomeRoute::Personal, TodayAgendaProjection::default(), @@ -16540,15 +16540,15 @@ mod tests { Some(HomeFocusedView::BuyerOrderDetail(order_id)) ); assert_eq!( - buyer_order_detail_focus_after_open(false, &runtime, OrderId::new()), + buyer_order_detail_focus_after_open(false, &runtime, OrderId::generate()), None ); } #[test] fn farmer_order_detail_focus_reopens_same_selected_detail() { - let order_id = OrderId::new(); - let farm_id = FarmId::new(); + let order_id = OrderId::generate(); + let farm_id = FarmId::generate(); let mut runtime = summary( HomeRoute::Today, TodayAgendaProjection::default(), @@ -16581,7 +16581,7 @@ mod tests { Some(HomeFocusedView::FarmerOrderDetail(order_id)) ); assert_eq!( - farmer_order_detail_focus_after_open(false, &runtime, OrderId::new()), + farmer_order_detail_focus_after_open(false, &runtime, OrderId::generate()), None ); } @@ -16620,7 +16620,10 @@ mod tests { block_shared_runtime_store_database(&paths); assert!( - view.open_personal_product_detail_update(PersonalSection::Browse, ProductId::new()) + view.open_personal_product_detail_update( + PersonalSection::Browse, + ProductId::generate() + ) ); assert_eq!( view.buyer_workspace_notice.as_deref(), @@ -16632,8 +16635,8 @@ mod tests { fn sample_pack_day_bundle(bundle_directory: &PathBuf) -> PackDayExportBundle { PackDayExportBundle { - fulfillment_window_id: FulfillmentWindowId::new(), - export_instance_id: radroots_studio_app_view::PackDayExportInstanceId::new(), + fulfillment_window_id: FulfillmentWindowId::generate(), + export_instance_id: radroots_studio_app_view::PackDayExportInstanceId::generate(), generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), bundle_directory: bundle_directory.to_string_lossy().into_owned(), artifacts: vec![ @@ -17079,8 +17082,8 @@ mod tests { Some(HomeAutoFocusTarget::BuyerOrderReviewNameInput) ); - let order_id = OrderId::new(); - let farm_id = FarmId::new(); + let order_id = OrderId::generate(); + let farm_id = FarmId::generate(); let mut buyer_orders = buyer_search.clone(); buyer_orders.shell_projection = AppShellProjection::new( ActiveSurface::Personal, @@ -17145,7 +17148,7 @@ mod tests { Some(HomeAutoFocusTarget::FarmerSetupStart) ); - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); let incomplete_farm = FarmSummary { farm_id, display_name: String::new(), @@ -17172,7 +17175,7 @@ mod tests { ); let saved_farm = FarmSummary { - farm_id: FarmId::new(), + farm_id: FarmId::generate(), display_name: String::new(), readiness: FarmReadiness::Ready, }; @@ -17217,8 +17220,8 @@ mod tests { ActiveSurface::Farmer, ShellSection::Farmer(FarmerSection::Orders), ); - let farmer_order_id = OrderId::new(); - let farmer_order_farm_id = FarmId::new(); + let farmer_order_id = OrderId::generate(); + let farmer_order_farm_id = FarmId::generate(); orders.orders_projection.list.rows = vec![OrdersListRow { order_id: farmer_order_id, farm_id: farmer_order_farm_id, @@ -17322,14 +17325,14 @@ mod tests { #[test] fn farmer_home_farm_state_distinguishes_no_farm_incomplete_and_configured() { - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); let incomplete_farm = FarmSummary { farm_id, display_name: String::new(), readiness: FarmReadiness::Incomplete, }; let configured_farm = FarmSummary { - farm_id: FarmId::new(), + farm_id: FarmId::generate(), display_name: String::new(), readiness: FarmReadiness::Ready, }; @@ -17382,7 +17385,7 @@ mod tests { #[test] fn pack_day_availability_tracks_the_contextual_window_projection() { - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); let mut runtime = summary( HomeRoute::Today, TodayAgendaProjection::default(), @@ -17401,7 +17404,7 @@ mod tests { runtime.pack_day_projection.projection = PackDayProjection { fulfillment_window: Some(FulfillmentWindowSummary { - fulfillment_window_id: FulfillmentWindowId::new(), + fulfillment_window_id: FulfillmentWindowId::generate(), farm_id, starts_at: String::new(), ends_at: String::new(), @@ -17417,8 +17420,8 @@ mod tests { #[test] fn pack_day_export_action_enabled_requires_a_window_and_exportable_rows() { - let farm_id = FarmId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); + let farm_id = FarmId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let mut runtime = summary( HomeRoute::Today, TodayAgendaProjection::default(), @@ -17487,10 +17490,10 @@ mod tests { #[test] fn pack_day_export_detail_rows_surface_bundle_and_failure_details() { - let fulfillment_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let bundle = PackDayExportBundle { fulfillment_window_id, - export_instance_id: radroots_studio_app_view::PackDayExportInstanceId::new(), + export_instance_id: radroots_studio_app_view::PackDayExportInstanceId::generate(), generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(), artifacts: vec![ @@ -18151,7 +18154,7 @@ mod tests { #[test] fn saved_farm_falls_back_to_local_projection_when_today_is_empty() { let saved_farm = FarmSummary { - farm_id: FarmId::new(), + farm_id: FarmId::generate(), display_name: String::new(), readiness: FarmReadiness::Ready, }; @@ -18388,8 +18391,8 @@ mod tests { #[test] fn reminder_action_target_prefers_order_detail_before_pack_day() { - let order_id = radroots_studio_app_view::OrderId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); + let order_id = radroots_studio_app_view::OrderId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); assert_eq!( reminder_action_target(&fixture_reminder( @@ -18448,7 +18451,7 @@ mod tests { fn reminder_deadline_text_uses_the_typed_due_label() { let reminder = fixture_reminder( None, - Some(FulfillmentWindowId::new()), + Some(FulfillmentWindowId::generate()), ReminderKind::FulfillmentWindow, ReminderUrgency::Upcoming, ); @@ -18488,7 +18491,7 @@ mod tests { ); let due_soon = fixture_reminder( None, - Some(FulfillmentWindowId::new()), + Some(FulfillmentWindowId::generate()), ReminderKind::FulfillmentWindow, ReminderUrgency::DueSoon, ); @@ -18558,7 +18561,7 @@ mod tests { let blocking_conflict = DesktopAppSyncConflictSummary { conflict_id: String::new(), conflict: SyncConflict { - aggregate: SyncAggregateRef::Farm(FarmId::new()), + aggregate: SyncAggregateRef::Farm(FarmId::generate()), kind: SyncConflictKind::RevisionMismatch, severity: SyncConflictSeverity::Blocking, resolution: SyncConflictResolutionStatus::Unresolved, @@ -18571,7 +18574,7 @@ mod tests { let review_conflict = DesktopAppSyncConflictSummary { conflict_id: String::new(), conflict: SyncConflict { - aggregate: SyncAggregateRef::Order(radroots_studio_app_view::OrderId::new()), + aggregate: SyncAggregateRef::Order(radroots_studio_app_view::OrderId::generate()), kind: SyncConflictKind::RemoteValidationReject, severity: SyncConflictSeverity::ReviewRequired, resolution: SyncConflictResolutionStatus::Unresolved, @@ -18898,8 +18901,8 @@ mod tests { urgency: ReminderUrgency, ) -> ReminderDeadlineProjection { ReminderDeadlineProjection { - reminder_id: ReminderId::new(), - farm_id: FarmId::new(), + reminder_id: ReminderId::generate(), + farm_id: FarmId::generate(), order_id, fulfillment_window_id, kind, diff --git a/crates/runtime/src/pack_day_export.rs b/crates/runtime/src/pack_day_export.rs @@ -87,7 +87,7 @@ pub fn prepare_pack_day_export_bundle_at_data_root( .collect::<Vec<_>>(); let bundle = PackDayExportBundle { fulfillment_window_id: source.fulfillment_window.fulfillment_window_id, - export_instance_id: PackDayExportInstanceId::new(), + export_instance_id: PackDayExportInstanceId::generate(), generated_at_utc: generated_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true), bundle_directory: bundle_directory.to_string_lossy().into_owned(), artifacts, @@ -413,8 +413,8 @@ mod tests { } fn sample_source() -> PackDayOutputSource { - let farm_id = FarmId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); + let farm_id = FarmId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); PackDayOutputSource { fulfillment_window: PackDayOutputWindow { fulfillment_window_id, @@ -436,7 +436,7 @@ mod tests { ], pack_list: vec![ PackDayOutputPackListEntry { - order_id: OrderId::new(), + order_id: OrderId::generate(), order_number: "R-1001".to_owned(), customer_display_name: "Casey".to_owned(), order_state: PackDayOutputOrderState::Scheduled, @@ -444,7 +444,7 @@ mod tests { quantity: PackDayOutputQuantity::new(2, "bags"), }, PackDayOutputPackListEntry { - order_id: OrderId::new(), + order_id: OrderId::generate(), order_number: "R-1002".to_owned(), customer_display_name: "Taylor".to_owned(), order_state: PackDayOutputOrderState::Packed, @@ -454,13 +454,13 @@ mod tests { ], pickup_roster: vec![ PackDayOutputCustomerOrder { - order_id: OrderId::new(), + order_id: OrderId::generate(), order_number: "R-1001".to_owned(), customer_display_name: "Casey".to_owned(), order_state: PackDayOutputOrderState::Scheduled, }, PackDayOutputCustomerOrder { - order_id: OrderId::new(), + order_id: OrderId::generate(), order_number: "R-1002".to_owned(), customer_display_name: "Taylor".to_owned(), order_state: PackDayOutputOrderState::Packed, diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs @@ -2293,7 +2293,7 @@ mod tests { custody: AccountCustody::LocalManaged, }, SelectedSurfaceProjection::new(surface), - FarmerActivationProjection::active(FarmId::new()), + FarmerActivationProjection::active(FarmId::generate()), ), ) } @@ -2332,7 +2332,7 @@ mod tests { ) -> PackDayExportBundle { PackDayExportBundle { fulfillment_window_id, - export_instance_id: PackDayExportInstanceId::new(), + export_instance_id: PackDayExportInstanceId::generate(), generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(), artifacts: vec![ @@ -2487,9 +2487,9 @@ mod tests { fn orders_and_pack_day_queries_refresh_as_local_app_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let farm_id = FarmId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); - let order_id = OrderId::new(); + let farm_id = FarmId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let order_id = OrderId::generate(); let order_economics = TradeEconomicsProjection { subtotal_minor_units: Some(1300), total_minor_units: Some(1300), @@ -2549,7 +2549,7 @@ mod tests { }; let orders_reminders = ReminderFeedProjection { items: vec![radroots_studio_app_view::ReminderDeadlineProjection { - reminder_id: radroots_studio_app_view::ReminderId::new(), + reminder_id: radroots_studio_app_view::ReminderId::generate(), farm_id, order_id: Some(order_id), fulfillment_window_id: Some(fulfillment_window_id), @@ -2745,7 +2745,7 @@ mod tests { fn pack_day_export_state_is_restart_ephemeral_and_skips_persistence() { let mut store = AppStateStore::load(FailingRepository).expect("failing repository should still load"); - let fulfillment_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_export_request(fulfillment_window_id); let bundle = sample_pack_day_export_bundle(fulfillment_window_id); @@ -2800,7 +2800,7 @@ mod tests { fn pack_day_print_state_is_restart_ephemeral_and_skips_persistence() { let mut store = AppStateStore::load(FailingRepository).expect("failing repository should still load"); - let fulfillment_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_print_request( fulfillment_window_id, PackDayPrintKind::PrintCustomerLabels, @@ -2870,7 +2870,7 @@ mod tests { fn pack_day_batch_print_state_is_restart_ephemeral_and_skips_persistence() { let mut store = AppStateStore::load(FailingRepository).expect("failing repository should still load"); - let fulfillment_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_batch_print_request(fulfillment_window_id); assert_eq!( @@ -2959,7 +2959,7 @@ mod tests { fn pack_day_host_handoff_state_is_restart_ephemeral_and_skips_persistence() { let mut store = AppStateStore::load(FailingRepository).expect("failing repository should still load"); - let fulfillment_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_host_handoff_request( fulfillment_window_id, PackDayHostHandoffKind::RevealBundle, @@ -3017,8 +3017,8 @@ mod tests { fn changing_pack_day_window_clears_stale_export_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let fulfillment_window_id = FulfillmentWindowId::new(); - let next_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let next_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_export_request(fulfillment_window_id); assert_eq!( @@ -3052,8 +3052,8 @@ mod tests { fn changing_pack_day_window_clears_stale_host_handoff_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let fulfillment_window_id = FulfillmentWindowId::new(); - let next_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let next_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_host_handoff_request( fulfillment_window_id, PackDayHostHandoffKind::OpenPickupRoster, @@ -3090,8 +3090,8 @@ mod tests { fn changing_pack_day_window_clears_stale_print_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let fulfillment_window_id = FulfillmentWindowId::new(); - let next_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let next_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_print_request(fulfillment_window_id, PackDayPrintKind::PrintPackSheet); @@ -3126,8 +3126,8 @@ mod tests { fn changing_pack_day_window_clears_stale_batch_print_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let fulfillment_window_id = FulfillmentWindowId::new(); - let next_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let next_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_batch_print_request(fulfillment_window_id); assert_eq!( @@ -3161,7 +3161,7 @@ mod tests { fn changing_pack_day_export_state_clears_stale_host_handoff_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let fulfillment_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let export_request = sample_pack_day_export_request(fulfillment_window_id); let host_handoff_request = sample_pack_day_host_handoff_request( fulfillment_window_id, @@ -3202,7 +3202,7 @@ mod tests { fn changing_pack_day_export_state_clears_stale_print_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let fulfillment_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let export_request = sample_pack_day_export_request(fulfillment_window_id); let print_request = sample_pack_day_print_request( fulfillment_window_id, @@ -3241,7 +3241,7 @@ mod tests { fn changing_pack_day_export_state_clears_stale_batch_print_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let fulfillment_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let export_request = sample_pack_day_export_request(fulfillment_window_id); let batch_request = sample_pack_day_batch_print_request(fulfillment_window_id); @@ -3277,9 +3277,9 @@ mod tests { fn replacing_pack_day_projection_with_new_window_clears_stale_host_handoff_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let farm_id = FarmId::new(); - let current_window_id = FulfillmentWindowId::new(); - let next_window_id = FulfillmentWindowId::new(); + let farm_id = FarmId::generate(); + let current_window_id = FulfillmentWindowId::generate(); + let next_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_host_handoff_request( current_window_id, PackDayHostHandoffKind::OpenCustomerLabels, @@ -3320,9 +3320,9 @@ mod tests { fn replacing_pack_day_projection_with_new_window_clears_stale_print_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let farm_id = FarmId::new(); - let current_window_id = FulfillmentWindowId::new(); - let next_window_id = FulfillmentWindowId::new(); + let farm_id = FarmId::generate(); + let current_window_id = FulfillmentWindowId::generate(); + let next_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_print_request(current_window_id, PackDayPrintKind::PrintCustomerLabels); @@ -3361,9 +3361,9 @@ mod tests { fn replacing_pack_day_projection_with_new_window_clears_stale_batch_print_state() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let farm_id = FarmId::new(); - let current_window_id = FulfillmentWindowId::new(); - let next_window_id = FulfillmentWindowId::new(); + let farm_id = FarmId::generate(); + let current_window_id = FulfillmentWindowId::generate(); + let next_window_id = FulfillmentWindowId::generate(); let request = sample_pack_day_batch_print_request(current_window_id); assert_eq!( @@ -3474,7 +3474,7 @@ mod tests { fn product_editor_state_transitions_are_explicit() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let product_id = ProductId::new(); + let product_id = ProductId::generate(); let ready_draft = ProductEditorDraft { title: "Heirloom tomatoes".to_owned(), subtitle: "Brandywine".to_owned(), @@ -3483,7 +3483,7 @@ mod tests { price_minor_units: Some(450), price_currency: "USD".to_owned(), stock_quantity: Some(12), - availability_window_id: Some(FulfillmentWindowId::new()), + availability_window_id: Some(FulfillmentWindowId::generate()), status: radroots_studio_app_view::ProductStatus::Draft, }; @@ -3558,11 +3558,11 @@ mod tests { fn product_editor_publish_blockers_require_current_fulfillment_window() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let farm_id = FarmId::new(); - let pickup_location_id = PickupLocationId::new(); - let active_window_id = FulfillmentWindowId::new(); - let stale_window_id = FulfillmentWindowId::new(); - let product_id = ProductId::new(); + let farm_id = FarmId::generate(); + let pickup_location_id = PickupLocationId::generate(); + let active_window_id = FulfillmentWindowId::generate(); + let stale_window_id = FulfillmentWindowId::generate(); + let product_id = ProductId::generate(); let publishable_draft = ProductEditorDraft { title: "Salad mix".to_owned(), subtitle: "Spring blend".to_owned(), @@ -4045,7 +4045,7 @@ mod tests { fn replace_today_agenda_updates_in_memory_state_without_touching_repository() { let mut store = AppStateStore::load(FailingRepository).expect("failing repository should still load"); - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); let today = TodayAgendaProjection { farm: Some(radroots_studio_app_view::FarmSummary { farm_id, @@ -4138,7 +4138,7 @@ mod tests { fn saved_farm_in_today_projection_synchronizes_ready_home_route() { let mut store = AppStateStore::load(InMemoryAppStateRepository::default()) .expect("in-memory repository should load"); - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); assert_eq!( store.apply(AppStateCommand::replace_identity_projection( @@ -4268,7 +4268,7 @@ mod tests { ); let conflicts = vec![ SyncConflict { - aggregate: radroots_studio_app_sync::SyncAggregateRef::Farm(FarmId::new()), + aggregate: radroots_studio_app_sync::SyncAggregateRef::Farm(FarmId::generate()), kind: SyncConflictKind::RevisionMismatch, severity: SyncConflictSeverity::Blocking, resolution: SyncConflictResolutionStatus::Unresolved, @@ -4278,7 +4278,7 @@ mod tests { resolved_at: None, }, SyncConflict { - aggregate: radroots_studio_app_sync::SyncAggregateRef::Farm(FarmId::new()), + aggregate: radroots_studio_app_sync::SyncAggregateRef::Farm(FarmId::generate()), kind: SyncConflictKind::RemoteValidationReject, severity: SyncConflictSeverity::ReviewRequired, resolution: SyncConflictResolutionStatus::AcceptedRemote, diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml @@ -17,10 +17,14 @@ radroots_studio_app_view.workspace = true radroots_studio_app_sync.workspace = true radroots_sql_core.workspace = true radroots_trade.workspace = true -rusqlite = { workspace = true } +futures-executor.workspace = true serde_json.workspace = true +sqlx.workspace = true thiserror.workspace = true uuid.workspace = true +[features] +test-support = [] + [lints] workspace = true diff --git a/crates/store/src/db.rs b/crates/store/src/db.rs @@ -0,0 +1,371 @@ +#![forbid(unsafe_code)] + +use std::{ + path::Path, + sync::{Arc, Mutex}, +}; + +use sqlx::{ + Connection, + sqlite::{SqliteArguments, SqliteConnectOptions, SqliteConnection, SqliteRow}, +}; + +#[derive(Clone, Debug)] +pub(crate) enum AppSqliteValue { + Null, + Integer(i64), + Real(f64), + Text(String), +} + +pub(crate) type AppSqliteParams = Vec<AppSqliteValue>; + +pub(crate) trait IntoAppSqliteParams { + fn into_app_sqlite_params(self) -> AppSqliteParams; +} + +pub(crate) trait OptionalSqliteResult<T> { + fn optional(self) -> Result<Option<T>, sqlx::Error>; +} + +pub(crate) trait ToAppSqliteValue { + fn to_app_sqlite_value(&self) -> AppSqliteValue; +} + +impl ToAppSqliteValue for str { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Text(self.to_owned()) + } +} + +impl ToAppSqliteValue for &str { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Text((*self).to_owned()) + } +} + +impl ToAppSqliteValue for String { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Text(self.clone()) + } +} + +impl ToAppSqliteValue for &String { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Text((*self).clone()) + } +} + +impl ToAppSqliteValue for bool { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Integer(i64::from(*self)) + } +} + +impl ToAppSqliteValue for i64 { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Integer(*self) + } +} + +impl ToAppSqliteValue for &i64 { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Integer(**self) + } +} + +impl ToAppSqliteValue for i32 { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Integer(i64::from(*self)) + } +} + +impl ToAppSqliteValue for u32 { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Integer(i64::from(*self)) + } +} + +impl ToAppSqliteValue for usize { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Integer(i64::try_from(*self).unwrap_or(i64::MAX)) + } +} + +impl ToAppSqliteValue for f64 { + fn to_app_sqlite_value(&self) -> AppSqliteValue { + AppSqliteValue::Real(*self) + } +} + +impl<T> ToAppSqliteValue for Option<T> +where + T: ToAppSqliteValue, +{ + fn to_app_sqlite_value(&self) -> AppSqliteValue { + self.as_ref() + .map(ToAppSqliteValue::to_app_sqlite_value) + .unwrap_or(AppSqliteValue::Null) + } +} + +impl IntoAppSqliteParams for AppSqliteParams { + fn into_app_sqlite_params(self) -> AppSqliteParams { + self + } +} + +impl IntoAppSqliteParams for [(); 0] { + fn into_app_sqlite_params(self) -> AppSqliteParams { + Vec::new() + } +} + +impl<T, const N: usize> IntoAppSqliteParams for [T; N] +where + T: ToAppSqliteValue, +{ + fn into_app_sqlite_params(self) -> AppSqliteParams { + self.into_iter() + .map(|value| value.to_app_sqlite_value()) + .collect() + } +} + +impl<T> IntoAppSqliteParams for Vec<T> +where + T: ToAppSqliteValue, +{ + fn into_app_sqlite_params(self) -> AppSqliteParams { + self.into_iter() + .map(|value| value.to_app_sqlite_value()) + .collect() + } +} + +impl<T> OptionalSqliteResult<T> for Result<T, sqlx::Error> { + fn optional(self) -> Result<Option<T>, sqlx::Error> { + match self { + Ok(value) => Ok(Some(value)), + Err(sqlx::Error::RowNotFound) => Ok(None), + Err(error) => Err(error), + } + } +} + +pub(crate) fn app_sqlite_value<T>(value: &T) -> AppSqliteValue +where + T: ToAppSqliteValue + ?Sized, +{ + value.to_app_sqlite_value() +} + +#[derive(Clone)] +pub(crate) struct AppSqliteDatabase { + connection: Arc<Mutex<SqliteConnection>>, +} + +impl AppSqliteDatabase { + pub(crate) fn open_path(path: &Path) -> Result<Self, sqlx::Error> { + let options = SqliteConnectOptions::new() + .filename(path) + .create_if_missing(true); + Self::connect(options) + } + + pub(crate) fn open_in_memory() -> Result<Self, sqlx::Error> { + Self::connect(SqliteConnectOptions::new().in_memory(true)) + } + + pub(crate) fn execute_statement( + &self, + sql: &str, + params: AppSqliteParams, + ) -> Result<u64, sqlx::Error> { + let mut connection = self.lock()?; + let query = bind_params(sqlx::query(sqlx::AssertSqlSafe(sql)), params); + let result = futures_executor::block_on(query.execute(&mut *connection))?; + Ok(result.rows_affected()) + } + + pub(crate) fn execute<P>(&self, sql: &str, params: P) -> Result<u64, sqlx::Error> + where + P: IntoAppSqliteParams, + { + self.execute_statement(sql, params.into_app_sqlite_params()) + } + + pub(crate) fn execute_script(&self, sql: &str) -> Result<(), sqlx::Error> { + let mut connection = self.lock()?; + futures_executor::block_on( + sqlx::raw_sql(sqlx::AssertSqlSafe(sql)).execute(&mut *connection), + )?; + Ok(()) + } + + pub(crate) fn execute_batch(&self, sql: &str) -> Result<(), sqlx::Error> { + self.execute_script(sql) + } + + pub(crate) fn fetch_one<T, F>( + &self, + sql: &str, + params: AppSqliteParams, + map: F, + ) -> Result<T, sqlx::Error> + where + F: FnOnce(&SqliteRow) -> Result<T, sqlx::Error>, + { + let mut connection = self.lock()?; + let query = bind_params(sqlx::query(sqlx::AssertSqlSafe(sql)), params); + let row = futures_executor::block_on(query.fetch_one(&mut *connection))?; + map(&row) + } + + pub(crate) fn query_row<T, P, F>(&self, sql: &str, params: P, map: F) -> Result<T, sqlx::Error> + where + P: IntoAppSqliteParams, + F: FnOnce(&SqliteRow) -> Result<T, sqlx::Error>, + { + self.fetch_one(sql, params.into_app_sqlite_params(), map) + } + + pub(crate) fn fetch_optional<T, F>( + &self, + sql: &str, + params: AppSqliteParams, + map: F, + ) -> Result<Option<T>, sqlx::Error> + where + F: FnOnce(&SqliteRow) -> Result<T, sqlx::Error>, + { + let mut connection = self.lock()?; + let query = bind_params(sqlx::query(sqlx::AssertSqlSafe(sql)), params); + let Some(row) = futures_executor::block_on(query.fetch_optional(&mut *connection))? else { + return Ok(None); + }; + map(&row).map(Some) + } + + pub(crate) fn fetch_mapped<T, F>( + &self, + sql: &str, + params: AppSqliteParams, + map: F, + ) -> Result<std::vec::IntoIter<Result<T, sqlx::Error>>, sqlx::Error> + where + F: FnMut(&SqliteRow) -> Result<T, sqlx::Error>, + { + let rows = { + let mut connection = self.lock()?; + let query = bind_params(sqlx::query(sqlx::AssertSqlSafe(sql)), params); + futures_executor::block_on(query.fetch_all(&mut *connection))? + }; + Ok(rows.iter().map(map).collect::<Vec<_>>().into_iter()) + } + + pub(crate) fn prepare(&self, sql: &str) -> Result<AppSqliteStatement<'_>, sqlx::Error> { + Ok(AppSqliteStatement { + database: self, + sql: sql.to_owned(), + }) + } + + #[cfg(test)] + pub(crate) fn query_rows<P>(&self, sql: &str, params: P) -> Result<AppSqliteRows, sqlx::Error> + where + P: IntoAppSqliteParams, + { + let rows = { + let mut connection = self.lock()?; + let query = bind_params( + sqlx::query(sqlx::AssertSqlSafe(sql)), + params.into_app_sqlite_params(), + ); + futures_executor::block_on(query.fetch_all(&mut *connection))? + }; + Ok(AppSqliteRows { + rows: rows.into_iter(), + }) + } + + fn connect(options: SqliteConnectOptions) -> Result<Self, sqlx::Error> { + let connection = futures_executor::block_on(SqliteConnection::connect_with(&options))?; + Ok(Self { + connection: Arc::new(Mutex::new(connection)), + }) + } + + fn lock(&self) -> Result<std::sync::MutexGuard<'_, SqliteConnection>, sqlx::Error> { + self.connection + .lock() + .map_err(|_| sqlx::Error::Protocol("sqlite connection mutex poisoned".to_owned())) + } +} + +pub(crate) struct AppSqliteStatement<'a> { + database: &'a AppSqliteDatabase, + sql: String, +} + +impl AppSqliteStatement<'_> { + pub(crate) fn execute<P>(&mut self, params: P) -> Result<u64, sqlx::Error> + where + P: IntoAppSqliteParams, + { + self.database.execute(&self.sql, params) + } + + pub(crate) fn query_map<T, P, F>( + &mut self, + params: P, + map: F, + ) -> Result<std::vec::IntoIter<Result<T, sqlx::Error>>, sqlx::Error> + where + P: IntoAppSqliteParams, + F: FnMut(&SqliteRow) -> Result<T, sqlx::Error>, + { + self.database + .fetch_mapped(&self.sql, params.into_app_sqlite_params(), map) + } + + #[cfg(test)] + pub(crate) fn query<P>(&mut self, params: P) -> Result<AppSqliteRows, sqlx::Error> + where + P: IntoAppSqliteParams, + { + self.database.query_rows(&self.sql, params) + } +} + +#[cfg(test)] +pub(crate) struct AppSqliteRows { + rows: std::vec::IntoIter<SqliteRow>, +} + +#[cfg(test)] +impl AppSqliteRows { + pub(crate) fn next(&mut self) -> Result<Option<SqliteRow>, sqlx::Error> { + Ok(self.rows.next()) + } +} + +pub(crate) fn empty_params() -> AppSqliteParams { + Vec::new() +} + +fn bind_params<'q>( + query: sqlx::query::Query<'q, sqlx::Sqlite, SqliteArguments>, + params: AppSqliteParams, +) -> sqlx::query::Query<'q, sqlx::Sqlite, SqliteArguments> { + let mut query = query; + for param in params { + query = match param { + AppSqliteValue::Null => query.bind(Option::<String>::None), + AppSqliteValue::Integer(value) => query.bind(value), + AppSqliteValue::Real(value) => query.bind(value), + AppSqliteValue::Text(value) => query.bind(value), + }; + } + query +} diff --git a/crates/store/src/error.rs b/crates/store/src/error.rs @@ -16,28 +16,28 @@ pub enum AppSqliteError { OpenPath { path: PathBuf, #[source] - source: rusqlite::Error, + source: sqlx::Error, }, #[error("failed to open in-memory sqlite database")] OpenInMemory { #[source] - source: rusqlite::Error, + source: sqlx::Error, }, #[error("failed to configure sqlite busy timeout")] ConfigureBusyTimeout { #[source] - source: rusqlite::Error, + source: sqlx::Error, }, #[error("failed to apply sqlite pragma `{pragma}`")] ApplyPragma { pragma: &'static str, #[source] - source: rusqlite::Error, + source: sqlx::Error, }, #[error("failed to read sqlite schema version")] ReadSchemaVersion { #[source] - source: rusqlite::Error, + source: sqlx::Error, }, #[error( "sqlite schema version {current} is newer than supported version {latest}; manual migration is required" @@ -47,31 +47,31 @@ pub enum AppSqliteError { BeginMigration { version: u32, #[source] - source: rusqlite::Error, + source: sqlx::Error, }, #[error("failed to execute sqlite migration {version}")] ExecuteMigration { version: u32, #[source] - source: rusqlite::Error, + source: sqlx::Error, }, #[error("failed to record sqlite schema version {version}")] RecordSchemaVersion { version: u32, #[source] - source: rusqlite::Error, + source: sqlx::Error, }, #[error("failed to commit sqlite migration {version}")] CommitMigration { version: u32, #[source] - source: rusqlite::Error, + source: sqlx::Error, }, #[error("failed to execute sqlite query for {operation}")] Query { operation: &'static str, #[source] - source: rusqlite::Error, + source: sqlx::Error, }, #[error("failed to encode sqlite JSON column `{field}`")] EncodeJson { diff --git a/crates/store/src/interop.rs b/crates/store/src/interop.rs @@ -27,7 +27,7 @@ use radroots_runtime_store::{ PublishOutboxStatus, RelayDeliveryEvidence, RelayDeliveryState, RuntimeStore, RuntimeStoreRecord, RuntimeStoreRecordFamily, RuntimeStoreRecordStatus, SourceRuntime, }; -use radroots_sql_core::{SqlExecutor, SqliteExecutor}; +use radroots_sql_core::{SqlExecutor, SqlxSqliteExecutor}; use radroots_studio_app_view::{ FarmId, FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary, FulfillmentWindowId, OrderId, OrderStatus, PickupLocationId, ProductId, ProductStatus, @@ -43,7 +43,9 @@ use radroots_trade::order::{ use radroots_trade::validation_receipt::{ RadrootsTradeValidationReceipt, RadrootsValidationReceiptTags, validation_receipt_from_event, }; -use rusqlite::{Connection, OptionalExtension, params}; +use sqlx::Row; + +use crate::{AppSqliteDatabase, OptionalSqliteResult}; use serde_json::Value; use uuid::Uuid; @@ -98,11 +100,11 @@ pub struct StoredLocalInteropRecord { } pub struct AppLocalInteropRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppLocalInteropRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } @@ -116,7 +118,7 @@ impl<'a> AppLocalInteropRepository<'a> { source, })?; } - let executor = SqliteExecutor::open(shared_database_path).map_err(|source| { + let executor = SqlxSqliteExecutor::open(shared_database_path).map_err(|source| { AppSqliteError::RuntimeStoreSql { operation: "open shared runtime store database", source, @@ -212,23 +214,23 @@ impl<'a> AppLocalInteropRepository<'a> { source, })?; let rows = statement - .query_map([], |row| { + .query_map(crate::empty_params(), |row| { Ok(StoredLocalInteropRecord { - record_id: row.get(0)?, - local_seq: row.get(1)?, - record_family: row.get(2)?, - local_status: row.get(3)?, - source_runtime: row.get(4)?, - owner_account_id: row.get(5)?, - owner_pubkey: row.get(6)?, - farm_key: row.get(7)?, - listing_addr: row.get(8)?, - projected_kind: row.get(9)?, - projected_id: row.get(10)?, - event_id: row.get(11)?, - event_kind: row.get(12)?, - outbox_status: row.get(13)?, - relay_delivery_json: row.get(14)?, + record_id: row.try_get(0)?, + local_seq: row.try_get(1)?, + record_family: row.try_get(2)?, + local_status: row.try_get(3)?, + source_runtime: row.try_get(4)?, + owner_account_id: row.try_get(5)?, + owner_pubkey: row.try_get(6)?, + farm_key: row.try_get(7)?, + listing_addr: row.try_get(8)?, + projected_kind: row.try_get(9)?, + projected_id: row.try_get(10)?, + event_id: row.try_get(11)?, + event_kind: row.try_get(12)?, + outbox_status: row.try_get(13)?, + relay_delivery_json: row.try_get(14)?, }) }) .map_err(|source| AppSqliteError::Query { @@ -273,18 +275,18 @@ impl<'a> AppLocalInteropRepository<'a> { source, })?; let rows = statement - .query_map(params![event_kind], |row| { + .query_map(crate::app_sqlite_params![event_kind], |row| { Ok(StoredLocalInteropSignedEventEvidence { - event_id: row.get(0)?, - event_kind: row.get(1)?, - local_status: row.get(2)?, - outbox_status: row.get(3)?, - relay_delivery_json: row.get(4)?, - event_pubkey: row.get(5)?, - event_created_at: row.get(6)?, - event_tags_json: row.get(7)?, - event_content: row.get(8)?, - event_sig: row.get(9)?, + event_id: row.try_get(0)?, + event_kind: row.try_get(1)?, + local_status: row.try_get(2)?, + outbox_status: row.try_get(3)?, + relay_delivery_json: row.try_get(4)?, + event_pubkey: row.try_get(5)?, + event_created_at: row.try_get(6)?, + event_tags_json: row.try_get(7)?, + event_content: row.try_get(8)?, + event_sig: row.try_get(9)?, }) }) .map_err(|source| AppSqliteError::Query { @@ -335,18 +337,18 @@ impl<'a> AppLocalInteropRepository<'a> { source, })?; let rows = statement - .query_map(params![event_id], |row| { + .query_map(crate::app_sqlite_params![event_id], |row| { Ok(StoredLocalInteropSignedEventEvidence { - event_id: row.get(0)?, - event_kind: row.get(1)?, - local_status: row.get(2)?, - outbox_status: row.get(3)?, - relay_delivery_json: row.get(4)?, - event_pubkey: row.get(5)?, - event_created_at: row.get(6)?, - event_tags_json: row.get(7)?, - event_content: row.get(8)?, - event_sig: row.get(9)?, + event_id: row.try_get(0)?, + event_kind: row.try_get(1)?, + local_status: row.try_get(2)?, + outbox_status: row.try_get(3)?, + relay_delivery_json: row.try_get(4)?, + event_pubkey: row.try_get(5)?, + event_created_at: row.try_get(6)?, + event_tags_json: row.try_get(7)?, + event_content: row.try_get(8)?, + event_sig: row.try_get(9)?, }) }) .map_err(|source| AppSqliteError::Query { @@ -377,10 +379,10 @@ impl<'a> AppLocalInteropRepository<'a> { WHERE consumer_id = ?1 LIMIT 1", [APP_LOCAL_INTEROP_CURSOR_ID], - |row| row.get::<_, i64>(0), + |row| row.try_get::<i64, _>(0), ) { Ok(last_change_seq) => Ok(last_change_seq), - Err(rusqlite::Error::QueryReturnedNoRows) => Ok(0), + Err(sqlx::Error::RowNotFound) => Ok(0), Err(source) => Err(AppSqliteError::Query { operation: "read app local interop projection cursor", source, @@ -402,7 +404,7 @@ impl<'a> AppLocalInteropRepository<'a> { excluded.last_change_seq ), updated_at = excluded.updated_at", - params![APP_LOCAL_INTEROP_CURSOR_ID, last_change_seq], + crate::app_sqlite_params![APP_LOCAL_INTEROP_CURSOR_ID, last_change_seq], ) .map_err(|source| AppSqliteError::Query { operation: "advance app local interop projection cursor", @@ -448,13 +450,13 @@ impl<'a> AppLocalInteropRepository<'a> { let projected_kind = projection.kind; let projected_id = projection.projected_id; self.record_import(record, projected_kind, projected_id.clone())?; - if projected_kind == "listing" { - if let Some(projected_id) = projected_id.as_deref() { - self.finish_duplicate_listing_replacement( - &superseded_listing_ids, - projected_id, - )?; - } + if projected_kind == "listing" + && let Some(projected_id) = projected_id.as_deref() + { + self.finish_duplicate_listing_replacement( + &superseded_listing_ids, + projected_id, + )?; } Ok(ImportOutcome::Imported) } @@ -521,14 +523,17 @@ impl<'a> AppLocalInteropRepository<'a> { source, })?; let rows = statement - .query_map(params![event_id, record.record_id.as_str()], |row| { - Ok(StoredSignedEventDuplicate { - source_runtime: row.get(0)?, - owner_account_id: row.get(1)?, - local_status: row.get(2)?, - outbox_status: row.get(3)?, - }) - }) + .query_map( + crate::app_sqlite_params![event_id, record.record_id.as_str()], + |row| { + Ok(StoredSignedEventDuplicate { + source_runtime: row.try_get(0)?, + owner_account_id: row.try_get(1)?, + local_status: row.try_get(2)?, + outbox_status: row.try_get(3)?, + }) + }, + ) .map_err(|source| AppSqliteError::Query { operation: "query duplicate local interop signed events", source, @@ -579,7 +584,7 @@ impl<'a> AppLocalInteropRepository<'a> { WHERE event_id = ?1 AND record_id <> ?2 AND record_family = 'signed_event'", - params![event_id, record_id], + crate::app_sqlite_params![event_id, record_id], ) .map_err(|source| AppSqliteError::Query { operation: "delete superseded duplicate local interop signed event", @@ -622,7 +627,9 @@ impl<'a> AppLocalInteropRepository<'a> { source, })?; let rows = statement - .query_map(params![event_id, record_id], |row| row.get::<_, String>(0)) + .query_map(crate::app_sqlite_params![event_id, record_id], |row| { + row.try_get::<String, _>(0) + }) .map_err(|source| AppSqliteError::Query { operation: "query superseded duplicate listing projections", source, @@ -651,7 +658,7 @@ impl<'a> AppLocalInteropRepository<'a> { WHERE projected_kind = 'listing' AND projected_id = ?1 )", - params![product_id], + crate::app_sqlite_params![product_id], ) .map_err(|source| AppSqliteError::Query { operation: "delete unreferenced superseded listing product", @@ -715,7 +722,7 @@ impl<'a> AppLocalInteropRepository<'a> { seller_pubkey = coalesce(nullif(buyer_cart_lines.seller_pubkey, ''), excluded.seller_pubkey), listing_relays_json = coalesce(nullif(buyer_cart_lines.listing_relays_json, ''), excluded.listing_relays_json), updated_at = excluded.updated_at", - params![product_id, canonical_product_id], + crate::app_sqlite_params![product_id, canonical_product_id], ) .map_err(|source| AppSqliteError::Query { operation: "migrate duplicate listing buyer cart lines", @@ -725,7 +732,7 @@ impl<'a> AppLocalInteropRepository<'a> { .execute( "DELETE FROM buyer_cart_lines WHERE product_id = ?1", - params![product_id], + crate::app_sqlite_params![product_id], ) .map_err(|source| AppSqliteError::Query { operation: "delete migrated duplicate listing buyer cart lines", @@ -1267,7 +1274,7 @@ impl<'a> AppLocalInteropRepository<'a> { status = excluded.status, buyer_context_key = coalesce(orders.buyer_context_key, excluded.buyer_context_key), updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ order_id.to_string(), farm_id.to_string(), order_number.as_str(), @@ -1310,7 +1317,7 @@ impl<'a> AppLocalInteropRepository<'a> { workflow_provenance_last_event_id = ?7, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?1", - params![ + crate::app_sqlite_params![ workflow.order_id.to_string(), status.storage_key(), workflow.revision.storage_key(), @@ -1386,7 +1393,7 @@ impl<'a> AppLocalInteropRepository<'a> { reducer_output_root = excluded.reducer_output_root, public_values_hash = excluded.public_values_hash, event_created_at = excluded.event_created_at", - params![ + crate::app_sqlite_params![ event.id_str(), order_id.map(|order_id| order_id.to_string()), tags.order_id.as_str(), @@ -1449,7 +1456,7 @@ impl<'a> AppLocalInteropRepository<'a> { WHERE root_event_id = ?1 AND raw_order_id = ?2 AND order_id IS NULL", - params![root_event_id, raw_order_id, order_id.to_string()], + crate::app_sqlite_params![root_event_id, raw_order_id, order_id.to_string()], ) .map_err(|source| AppSqliteError::Query { operation: "attach local interop validation receipts to request", @@ -1469,8 +1476,8 @@ impl<'a> AppLocalInteropRepository<'a> { "SELECT status, workflow_agreement, workflow_provenance_last_event_id FROM orders WHERE id = ?1", - params![order_id.to_string()], - |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)), + crate::app_sqlite_params![order_id.to_string()], + |row| Ok((row.try_get(0)?, row.try_get(1)?, row.try_get(2)?)), ) .optional() .map_err(|source| AppSqliteError::Query { @@ -1506,8 +1513,8 @@ impl<'a> AppLocalInteropRepository<'a> { AND target_event_id = ?2 ORDER BY event_created_at DESC, event_id DESC LIMIT 1", - params![order_id.to_string(), target_event_id.as_str()], - |row| row.get(0), + crate::app_sqlite_params![order_id.to_string(), target_event_id.as_str()], + |row| row.try_get(0), ) .optional() .map_err(|source| AppSqliteError::Query { @@ -1545,7 +1552,7 @@ impl<'a> AppLocalInteropRepository<'a> { workflow_provenance_last_event_id = ?6, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') WHERE id = ?1", - params![ + crate::app_sqlite_params![ order_id.to_string(), status, agreement, @@ -1589,7 +1596,7 @@ impl<'a> AppLocalInteropRepository<'a> { self.connection .execute( "DELETE FROM order_lines WHERE order_id = ?1", - params![order_id.to_string()], + crate::app_sqlite_params![order_id.to_string()], ) .map_err(|source| AppSqliteError::Query { operation: "replace local interop order lines", @@ -1633,7 +1640,7 @@ impl<'a> AppLocalInteropRepository<'a> { seller_pubkey, sort_index ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, null, ?13, ?14)", - params![ + crate::app_sqlite_params![ format!( "{}:{}", order_id, @@ -1673,7 +1680,7 @@ impl<'a> AppLocalInteropRepository<'a> { self.connection .execute( "DELETE FROM order_lines WHERE order_id = ?1", - params![order_id.to_string()], + crate::app_sqlite_params![order_id.to_string()], ) .map_err(|source| AppSqliteError::Query { operation: "replace local interop active order agreement lines", @@ -1728,7 +1735,7 @@ impl<'a> AppLocalInteropRepository<'a> { seller_pubkey, sort_index ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", - params![ + crate::app_sqlite_params![ format!( "{}:{}", order_id, @@ -1780,11 +1787,11 @@ impl<'a> AppLocalInteropRepository<'a> { WHERE order_id = ?1 ORDER BY sort_index ASC, id ASC LIMIT 1", - params![order_id.to_string()], + crate::app_sqlite_params![order_id.to_string()], |row| { Ok(ExistingOrderLineMetadata { - listing_event_id: row.get::<_, Option<String>>(0)?, - listing_relays_json: row.get::<_, Option<String>>(1)?, + listing_event_id: row.try_get::<Option<String>, _>(0)?, + listing_relays_json: row.try_get::<Option<String>, _>(1)?, }) }, ) @@ -1804,7 +1811,7 @@ impl<'a> AppLocalInteropRepository<'a> { display_name = excluded.display_name, readiness = excluded.readiness, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ farm.farm_id.to_string(), farm.display_name.as_str(), farm_readiness_storage_key(farm.readiness), @@ -1830,7 +1837,7 @@ impl<'a> AppLocalInteropRepository<'a> { ELSE excluded.readiness END, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ farm.farm_id.to_string(), farm.display_name.as_str(), farm_readiness_storage_key(farm.readiness), @@ -1898,7 +1905,7 @@ impl<'a> AppLocalInteropRepository<'a> { saved_farm_display_name = excluded.saved_farm_display_name, saved_farm_readiness = excluded.saved_farm_readiness, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ account_id, display_name.as_str(), i64::from(method == FarmOrderMethod::Pickup), @@ -1920,7 +1927,7 @@ impl<'a> AppLocalInteropRepository<'a> { .query_row( "SELECT EXISTS(SELECT 1 FROM farms WHERE id = ?1)", [farm_id.to_string()], - |row| row.get::<_, bool>(0), + |row| row.try_get::<bool, _>(0), ) .map_err(|source| AppSqliteError::Query { operation: "check local interop farm existence", @@ -1941,7 +1948,7 @@ impl<'a> AppLocalInteropRepository<'a> { .query_row( "SELECT display_name FROM farms WHERE id = ?1 LIMIT 1", [farm_id.to_string()], - |row| row.get::<_, String>(0), + |row| row.try_get::<String, _>(0), ) .optional() .map_err(|source| AppSqliteError::Query { @@ -1958,7 +1965,7 @@ impl<'a> AppLocalInteropRepository<'a> { .query_row( "SELECT readiness FROM farms WHERE id = ?1 LIMIT 1", [farm_id.to_string()], - |row| row.get::<_, String>(0), + |row| row.try_get::<String, _>(0), ) .optional() .map_err(|source| AppSqliteError::Query { @@ -2022,7 +2029,7 @@ impl<'a> AppLocalInteropRepository<'a> { pickup_location_id = excluded.pickup_location_id, order_cutoff_at = excluded.order_cutoff_at, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ fulfillment_window_id.to_string(), farm_id_string.as_str(), starts_at.as_str(), @@ -2065,7 +2072,7 @@ impl<'a> AppLocalInteropRepository<'a> { label = excluded.label, address_line = excluded.address_line, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ pickup_location_id.to_string(), farm_id_string.as_str(), location_primary, @@ -2091,7 +2098,7 @@ impl<'a> AppLocalInteropRepository<'a> { .query_row( "SELECT strftime('%Y-%m-%dT%H:%M:%SZ', ?1, 'unixepoch')", [seconds], - |row| row.get::<_, Option<String>>(0), + |row| row.try_get::<Option<String>, _>(0), ) .map_err(|source| AppSqliteError::Query { operation, source })?; timestamp.ok_or(AppSqliteError::InvalidProjection { @@ -2138,7 +2145,7 @@ impl<'a> AppLocalInteropRepository<'a> { END, listing_bin_id = coalesce(excluded.listing_bin_id, products.listing_bin_id), updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ projection.product_id.to_string(), projection.farm_id.to_string(), projection.title.as_str(), @@ -2189,12 +2196,12 @@ impl<'a> AppLocalInteropRepository<'a> { [listing_addr], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, Option<String>>(4)?, - row.get::<_, Option<String>>(5)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<Option<String>, _>(4)?, + row.try_get::<Option<String>, _>(5)?, )) }, ) @@ -2289,7 +2296,7 @@ impl<'a> AppLocalInteropRepository<'a> { AND products.id = ?1 AND products.farm_id = ?5 LIMIT 1", - params![ + crate::app_sqlite_params![ product_id.to_string(), farm_key, listing_addr, @@ -2298,12 +2305,12 @@ impl<'a> AppLocalInteropRepository<'a> { ], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, Option<String>>(4)?, - row.get::<_, Option<String>>(5)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<Option<String>, _>(4)?, + row.try_get::<Option<String>, _>(5)?, )) }, ) @@ -2390,7 +2397,10 @@ impl<'a> AppLocalInteropRepository<'a> { source, })?; let rows = statement - .query_map(params![listing_addr.as_str()], listing_currentness_row) + .query_map( + crate::app_sqlite_params![listing_addr.as_str()], + listing_currentness_row, + ) .map_err(|source| AppSqliteError::Query { operation: "query current listing-address evidence", source, @@ -2436,7 +2446,7 @@ impl<'a> AppLocalInteropRepository<'a> { })?; let rows = statement .query_map( - params![event_kind, event_pubkey.as_str()], + crate::app_sqlite_params![event_kind, event_pubkey.as_str()], listing_currentness_identity_row, ) .map_err(|source| AppSqliteError::Query { @@ -2537,7 +2547,7 @@ impl<'a> AppLocalInteropRepository<'a> { outbox_status = excluded.outbox_status, relay_delivery_json = excluded.relay_delivery_json, imported_at = excluded.imported_at", - params![ + crate::app_sqlite_params![ record.record_id.as_str(), record.seq, record.family.as_str(), @@ -2890,35 +2900,35 @@ impl ActiveOrderEvidenceBuckets { } fn listing_currentness_row( - row: &rusqlite::Row<'_>, -) -> rusqlite::Result<StoredListingCurrentnessEvidence> { + row: &sqlx::sqlite::SqliteRow, +) -> Result<StoredListingCurrentnessEvidence, sqlx::Error> { Ok(StoredListingCurrentnessEvidence { - event_id: row.get(0)?, - event_created_at: row.get(1)?, - source_runtime: row.get(2)?, - owner_account_id: row.get(3)?, - local_status: row.get(4)?, - outbox_status: row.get(5)?, - relay_delivery_json: row.get(6)?, + event_id: row.try_get(0)?, + event_created_at: row.try_get(1)?, + source_runtime: row.try_get(2)?, + owner_account_id: row.try_get(3)?, + local_status: row.try_get(4)?, + outbox_status: row.try_get(5)?, + relay_delivery_json: row.try_get(6)?, }) } fn listing_currentness_identity_row( - row: &rusqlite::Row<'_>, -) -> rusqlite::Result<StoredListingCurrentnessIdentityEvidence> { + row: &sqlx::sqlite::SqliteRow, +) -> Result<StoredListingCurrentnessIdentityEvidence, sqlx::Error> { Ok(StoredListingCurrentnessIdentityEvidence { currentness: StoredListingCurrentnessEvidence { - event_id: row.get(0)?, - event_created_at: row.get(1)?, - source_runtime: row.get(2)?, - owner_account_id: row.get(3)?, - local_status: row.get(4)?, - outbox_status: row.get(5)?, - relay_delivery_json: row.get(6)?, + event_id: row.try_get(0)?, + event_created_at: row.try_get(1)?, + source_runtime: row.try_get(2)?, + owner_account_id: row.try_get(3)?, + local_status: row.try_get(4)?, + outbox_status: row.try_get(5)?, + relay_delivery_json: row.try_get(6)?, }, - event_tags_json: row.get(7)?, - event_content: row.get(8)?, - listing_addr: row.get(9)?, + event_tags_json: row.try_get(7)?, + event_content: row.try_get(8)?, + listing_addr: row.try_get(9)?, }) } @@ -3439,14 +3449,14 @@ fn deterministic_order_number(order_id: &str) -> String { } fn existing_order_number( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, ) -> Result<Option<String>, AppSqliteError> { connection .query_row( "SELECT order_number FROM orders WHERE id = ?1 LIMIT 1", - params![order_id.to_string()], - |row| row.get::<_, String>(0), + crate::app_sqlite_params![order_id.to_string()], + |row| row.try_get::<String, _>(0), ) .optional() .map_err(|source| AppSqliteError::Query { @@ -3893,7 +3903,7 @@ mod tests { PublishOutboxStatus, RelayDeliveryEvidence, RuntimeStore, RuntimeStoreRecordFamily, RuntimeStoreRecordInput, RuntimeStoreRecordStatus, RuntimeStoreRecordUpdate, SourceRuntime, }; - use radroots_sql_core::SqliteExecutor; + use radroots_sql_core::SqlxSqliteExecutor; use radroots_studio_app_view::{ BuyerContext, BuyerOrderStatus, FarmId, FarmOrderMethod, OrderId, OrderStatus, OrdersFilter, OrdersScreenQueryState, ProductAvailabilityState, ProductId, @@ -3907,8 +3917,8 @@ mod tests { RadrootsValidationReceiptStatement, RadrootsValidationReceiptType, VALIDATION_RECEIPT_DOMAIN, VALIDATION_RECEIPT_VERSION, validation_receipt_event_build, }; - use rusqlite::params; use serde_json::json; + use sqlx::Row; use uuid::Uuid; use super::{ @@ -3918,8 +3928,8 @@ mod tests { }; use crate::{AppSqliteStore, BuyerRepeatDemandApplyOutcome, DatabaseTarget}; - fn runtime_store_store() -> RuntimeStore<SqliteExecutor> { - let executor = SqliteExecutor::open_memory().expect("open runtime store memory db"); + fn runtime_store_store() -> RuntimeStore<SqlxSqliteExecutor> { + let executor = SqlxSqliteExecutor::open_memory().expect("open runtime store memory db"); let store = RuntimeStore::new(executor); store.migrate_up().expect("migrate runtime store store"); store @@ -4089,6 +4099,7 @@ mod tests { } } + #[expect(clippy::too_many_arguments)] fn signed_market_listing_record( record_id: &str, owner_pubkey: &str, @@ -4331,7 +4342,7 @@ mod tests { .execute( "INSERT INTO farms (id, display_name, readiness, created_at, updated_at) VALUES (?1, 'Origin Farm', 'ready', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')", - params![farm_id.to_string()], + crate::app_sqlite_params![farm_id.to_string()], ) .expect("seed origin farm"); app_store @@ -4362,7 +4373,7 @@ mod tests { NULL, '2026-01-01T00:00:00Z' )", - params![product_id.to_string(), farm_id.to_string()], + crate::app_sqlite_params![product_id.to_string(), farm_id.to_string()], ) .expect("seed origin product"); } @@ -4530,6 +4541,7 @@ mod tests { } } + #[expect(clippy::too_many_arguments)] fn revision_decision_payload( revision_id: &str, order_id: &str, @@ -4569,7 +4581,7 @@ mod tests { struct ValidationReceiptOrderFixture { app_store: AppSqliteStore, - events: RuntimeStore<SqliteExecutor>, + events: RuntimeStore<SqlxSqliteExecutor>, buyer_context: BuyerContext, seller_farm_id: FarmId, order_id: OrderId, @@ -4808,6 +4820,7 @@ mod tests { } } + #[expect(clippy::too_many_arguments)] fn validation_receipt_event( event_id: &str, author: &str, @@ -4958,7 +4971,7 @@ mod tests { .query_row( "SELECT buyer_context_key FROM orders WHERE id = ?1", [order_id.to_string()], - |row| row.get(0), + |row| row.try_get(0), ) .expect("load buyer context key"); @@ -5917,8 +5930,8 @@ mod tests { .connection() .query_row( "SELECT count(*) FROM order_validation_receipts WHERE order_id IS NULL", - [], - |row| row.get(0), + crate::empty_params(), + |row| row.try_get(0), ) .expect("count pending validation receipts"); assert_eq!(pending_count, 1); @@ -6555,7 +6568,11 @@ mod tests { .expect("load imported records"); let order_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM orders", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM orders", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("load order count"); assert_eq!(report.imported_records, 1); @@ -6893,14 +6910,25 @@ mod tests { ); let product_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("product count"); let product: (String, String, Option<i64>, Option<i64>) = app_store .connection() .query_row( "SELECT title, status, price_minor_units, stock_count FROM products", - [], - |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), + crate::empty_params(), + |row| { + Ok(( + row.try_get(0)?, + row.try_get(1)?, + row.try_get(2)?, + row.try_get(3)?, + )) + }, ) .expect("load product"); assert_eq!(product_count, 1); @@ -7109,14 +7137,18 @@ mod tests { .expect("load imported records"); let product_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("product count"); let app_product: (String, Option<i64>) = app_store .connection() .query_row( "SELECT title, stock_count FROM products WHERE id = ?1", [product_uuid.to_string()], - |row| Ok((row.get(0)?, row.get(1)?)), + |row| Ok((row.try_get(0)?, row.try_get(1)?)), ) .expect("load app product"); let network_product_id = @@ -7126,7 +7158,14 @@ mod tests { .query_row( "SELECT id, farm_id, title, stock_count FROM products WHERE id = ?1", [network_product_id.to_string()], - |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), + |row| { + Ok(( + row.try_get(0)?, + row.try_get(1)?, + row.try_get(2)?, + row.try_get(3)?, + )) + }, ) .expect("load network product"); let buyer_listings = app_store @@ -7260,14 +7299,25 @@ mod tests { .expect("import network app-origin listing"); let product_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("product count"); let product: (String, String, String, Option<i64>) = app_store .connection() .query_row( "SELECT id, farm_id, title, stock_count FROM products", - [], - |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)), + crate::empty_params(), + |row| { + Ok(( + row.try_get(0)?, + row.try_get(1)?, + row.try_get(2)?, + row.try_get(3)?, + )) + }, ) .expect("load product"); let imported = app_store @@ -7391,14 +7441,18 @@ mod tests { .expect("import network app-origin listing"); let product_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("product count"); let app_product: (String, Option<i64>) = app_store .connection() .query_row( "SELECT title, stock_count FROM products WHERE id = ?1", [product_uuid.to_string()], - |row| Ok((row.get(0)?, row.get(1)?)), + |row| Ok((row.try_get(0)?, row.try_get(1)?)), ) .expect("load app product"); let foreign_product_id = @@ -7408,7 +7462,7 @@ mod tests { .query_row( "SELECT COUNT(*) FROM products WHERE id = ?1", [foreign_product_id.to_string()], - |row| row.get(0), + |row| row.try_get(0), ) .expect("foreign product count"); @@ -7459,7 +7513,11 @@ mod tests { deterministic_product_id(Some(seller_pubkey), listing_key.as_str()); let network_product_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("network product count"); assert_eq!(network_product_count, 1); assert_ne!(network_product_id.as_uuid(), product_uuid); @@ -7565,14 +7623,18 @@ mod tests { .expect("load imported records"); let product_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("product count"); let stale_product_count: i64 = app_store .connection() .query_row( "SELECT COUNT(*) FROM products WHERE id = ?1", [network_product_id.to_string()], - |row| row.get(0), + |row| row.try_get(0), ) .expect("stale product count"); let listing_import = imported @@ -7587,7 +7649,7 @@ mod tests { .query_row( "SELECT id FROM order_lines WHERE order_id = ?1", [order_id.to_string()], - |row| row.get(0), + |row| row.try_get(0), ) .expect("order line id should load"); @@ -7734,14 +7796,18 @@ mod tests { .expect("load imported records"); let product_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("product count"); let stale_cart_quantity: i64 = app_store .connection() .query_row( "SELECT quantity FROM buyer_cart_lines WHERE product_id = ?1", [network_product_id.to_string()], - |row| row.get(0), + |row| row.try_get(0), ) .expect("stale cart quantity"); let canonical_cart_count: i64 = app_store @@ -7749,7 +7815,7 @@ mod tests { .query_row( "SELECT COUNT(*) FROM buyer_cart_lines WHERE product_id = ?1", [product_uuid.to_string()], - |row| row.get(0), + |row| row.try_get(0), ) .expect("canonical cart count"); let network_product_title: String = app_store @@ -7757,7 +7823,7 @@ mod tests { .query_row( "SELECT title FROM products WHERE id = ?1", [network_product_id.to_string()], - |row| row.get(0), + |row| row.try_get(0), ) .expect("network product title"); @@ -8005,9 +8071,11 @@ mod tests { .expect("import older listing"); let product: (String, Option<i64>) = app_store .connection() - .query_row("SELECT title, stock_count FROM products", [], |row| { - Ok((row.get(0)?, row.get(1)?)) - }) + .query_row( + "SELECT title, stock_count FROM products", + crate::empty_params(), + |row| Ok((row.try_get(0)?, row.try_get(1)?)), + ) .expect("load product"); let imported = app_store .load_local_interop_records() @@ -8086,9 +8154,11 @@ mod tests { .expect("import losing listing"); let product: (String, Option<i64>) = app_store .connection() - .query_row("SELECT title, stock_count FROM products", [], |row| { - Ok((row.get(0)?, row.get(1)?)) - }) + .query_row( + "SELECT title, stock_count FROM products", + crate::empty_params(), + |row| Ok((row.try_get(0)?, row.try_get(1)?)), + ) .expect("load product"); assert_eq!(product.0, "Tie Winner Eggs"); @@ -8150,9 +8220,11 @@ mod tests { .expect("load imported records"); let stored_farm: (String, String) = app_store .connection() - .query_row("SELECT id, display_name FROM farms", [], |row| { - Ok((row.get(0)?, row.get(1)?)) - }) + .query_row( + "SELECT id, display_name FROM farms", + crate::empty_params(), + |row| Ok((row.try_get(0)?, row.try_get(1)?)), + ) .expect("load farm"); assert_eq!(report.imported_records, 1); @@ -8237,9 +8309,11 @@ mod tests { .expect("load imported records"); let product: (String, String) = app_store .connection() - .query_row("SELECT id, farm_id FROM products", [], |row| { - Ok((row.get(0)?, row.get(1)?)) - }) + .query_row( + "SELECT id, farm_id FROM products", + crate::empty_params(), + |row| Ok((row.try_get(0)?, row.try_get(1)?)), + ) .expect("load product"); assert_eq!(report.imported_records, 1); @@ -8522,9 +8596,11 @@ mod tests { .expect("import shared local work after relay"); let stored_farm: (String, String, String) = app_store .connection() - .query_row("SELECT id, display_name, readiness FROM farms", [], |row| { - Ok((row.get(0)?, row.get(1)?, row.get(2)?)) - }) + .query_row( + "SELECT id, display_name, readiness FROM farms", + crate::empty_params(), + |row| Ok((row.try_get(0)?, row.try_get(1)?, row.try_get(2)?)), + ) .expect("load farm"); assert_eq!(direct_report.imported_records, 1); @@ -8605,9 +8681,11 @@ mod tests { .expect("import listing and farm"); let stored_farm: (String, String, String) = app_store .connection() - .query_row("SELECT id, display_name, readiness FROM farms", [], |row| { - Ok((row.get(0)?, row.get(1)?, row.get(2)?)) - }) + .query_row( + "SELECT id, display_name, readiness FROM farms", + crate::empty_params(), + |row| Ok((row.try_get(0)?, row.try_get(1)?, row.try_get(2)?)), + ) .expect("load farm"); assert_eq!(report.imported_records, 2); @@ -8644,7 +8722,11 @@ mod tests { .expect("import signed listing"); let product_status: String = app_store .connection() - .query_row("SELECT status FROM products", [], |row| row.get(0)) + .query_row( + "SELECT status FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("load product status"); assert_eq!(report.imported_records, 1); @@ -8685,7 +8767,11 @@ mod tests { .expect("import observed signed listing"); let product_status: String = app_store .connection() - .query_row("SELECT status FROM products", [], |row| row.get(0)) + .query_row( + "SELECT status FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("load product status"); assert_eq!(report.imported_records, 1); @@ -8717,7 +8803,11 @@ mod tests { .expect("load imported records"); let product_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("product count"); assert_eq!(report.imported_records, 0); @@ -8770,7 +8860,11 @@ mod tests { .expect("import unconfirmed signed listing"); let product_status: String = app_store .connection() - .query_row("SELECT status FROM products", [], |row| row.get(0)) + .query_row( + "SELECT status FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("load product status"); let imported = app_store .load_local_interop_records() @@ -8830,7 +8924,11 @@ mod tests { .expect("import updated listing"); let product_status: String = app_store .connection() - .query_row("SELECT status FROM products", [], |row| row.get(0)) + .query_row( + "SELECT status FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("load product status"); let imported = app_store .load_local_interop_records() @@ -8929,20 +9027,24 @@ mod tests { .expect("import unchanged shared runtime store into origin store"); let origin_product_count: i64 = origin_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("origin product count"); let origin_product: (String, String, String, Option<i64>, Option<i64>) = origin_store .connection() .query_row( "SELECT id, farm_id, title, price_minor_units, stock_count FROM products", - [], + crate::empty_params(), |row| { Ok(( - row.get(0)?, - row.get(1)?, - row.get(2)?, - row.get(3)?, - row.get(4)?, + row.try_get(0)?, + row.try_get(1)?, + row.try_get(2)?, + row.try_get(3)?, + row.try_get(4)?, )) }, ) @@ -8971,13 +9073,19 @@ mod tests { .expect("import shared runtime store into fresh store"); let fresh_product_count: i64 = fresh_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("fresh product count"); let fresh_product: (String, String, String) = fresh_store .connection() - .query_row("SELECT id, farm_id, title FROM products", [], |row| { - Ok((row.get(0)?, row.get(1)?, row.get(2)?)) - }) + .query_row( + "SELECT id, farm_id, title FROM products", + crate::empty_params(), + |row| Ok((row.try_get(0)?, row.try_get(1)?, row.try_get(2)?)), + ) .expect("load fresh product"); let fresh_imports = fresh_store .load_local_interop_records() @@ -9031,7 +9139,9 @@ mod tests { .expect("load imported records"); let farm_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM farms", [], |row| row.get(0)) + .query_row("SELECT COUNT(*) FROM farms", crate::empty_params(), |row| { + row.try_get(0) + }) .expect("farm count"); assert_eq!(report.scanned_records, 1); @@ -9169,20 +9279,24 @@ mod tests { .collect::<Vec<_>>(); let product_count: i64 = app_store .connection() - .query_row("SELECT COUNT(*) FROM products", [], |row| row.get(0)) + .query_row( + "SELECT COUNT(*) FROM products", + crate::empty_params(), + |row| row.try_get(0), + ) .expect("product count"); let product: (String, String, String, Option<i64>, Option<i64>) = app_store .connection() .query_row( "SELECT id, farm_id, status, price_minor_units, stock_count FROM products", - [], + crate::empty_params(), |row| { Ok(( - row.get(0)?, - row.get(1)?, - row.get(2)?, - row.get(3)?, - row.get(4)?, + row.try_get(0)?, + row.try_get(1)?, + row.try_get(2)?, + row.try_get(3)?, + row.try_get(4)?, )) }, ) diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs @@ -1,14 +1,20 @@ #![forbid(unsafe_code)] +mod db; mod error; mod interop; mod migrations; mod repo; mod sdk_workflow_receipts; +#[cfg(test)] +mod source_guards; mod sync; -use std::{collections::BTreeSet, fs, path::PathBuf, time::Duration}; +use std::{collections::BTreeSet, fs, path::PathBuf}; +pub(crate) use db::{ + AppSqliteDatabase, IntoAppSqliteParams, OptionalSqliteResult, app_sqlite_value, empty_params, +}; use radroots_studio_app_sync::{ AppRelayIngestScopeFreshness, PendingSyncOperation, SyncCheckpointStatus, SyncConflict, SyncConflictResolutionStatus, @@ -24,7 +30,7 @@ use radroots_studio_app_view::{ ProductsListProjection, ProductsSort, ReminderFeedProjection, ReminderLogEntryProjection, ReminderLogProjection, TodayAgendaProjection, }; -use rusqlite::Connection; +use sqlx::Row; pub use error::AppSqliteError; pub use interop::{ @@ -46,11 +52,23 @@ pub use sdk_workflow_receipts::{ AppSdkWorkflowReceiptSourceKind, AppSdkWorkflowReceiptState, }; pub use sync::{ - AppSyncRepository, StoredPendingSyncOperation, StoredRelayIngestCursor, StoredSyncConflict, + AppRelayIngestFailureInput, AppRelayIngestSuccessInput, AppSyncRepository, + StoredPendingSyncOperation, StoredRelayIngestCursor, StoredSyncConflict, }; const SQLITE_BUSY_TIMEOUT_MS: u64 = 5_000; +macro_rules! app_sqlite_params { + () => { + $crate::empty_params() + }; + ($($value:expr),+ $(,)?) => { + vec![$($crate::app_sqlite_value(&$value)),+] + }; +} + +pub(crate) use app_sqlite_params; + #[derive(Clone, Debug, Eq, PartialEq)] pub enum DatabaseTarget { InMemory, @@ -58,23 +76,50 @@ pub enum DatabaseTarget { } pub struct AppSqliteStore { - connection: Connection, + connection: AppSqliteDatabase, } impl AppSqliteStore { pub fn open(target: DatabaseTarget) -> Result<Self, AppSqliteError> { - let mut connection = open_connection(&target)?; - bootstrap_connection(&mut connection, &target)?; + let connection = open_connection(&target)?; + bootstrap_connection(&connection, &target)?; Ok(Self { connection }) } - pub fn connection(&self) -> &Connection { + #[cfg(test)] + pub(crate) fn connection(&self) -> &AppSqliteDatabase { &self.connection } - pub fn into_connection(self) -> Connection { + #[cfg(any(test, feature = "test-support"))] + pub fn execute_test_sql(&self, sql: &str) -> Result<(), AppSqliteError> { self.connection + .execute_script(sql) + .map_err(|source| AppSqliteError::Query { + operation: "execute test sqlite script", + source, + }) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn query_test_i64(&self, sql: &str) -> Result<i64, AppSqliteError> { + self.connection + .fetch_one(sql, empty_params(), |row| row.try_get::<i64, _>(0)) + .map_err(|source| AppSqliteError::Query { + operation: "query test sqlite i64", + source, + }) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn query_test_string(&self, sql: &str) -> Result<String, AppSqliteError> { + self.connection + .fetch_one(sql, empty_params(), |row| row.try_get::<String, _>(0)) + .map_err(|source| AppSqliteError::Query { + operation: "query test sqlite string", + source, + }) } pub fn schema_version(&self) -> Result<u32, AppSqliteError> { @@ -612,46 +657,16 @@ impl AppSqliteStore { pub fn record_relay_ingest_success( &self, - scope_key: &str, - relay_url: &str, - cursor_since_unix_seconds: i64, - last_event_created_at_unix_seconds: Option<i64>, - started_at: &str, - started_unix_seconds: i64, - completed_at: &str, - completed_unix_seconds: i64, + input: AppRelayIngestSuccessInput<'_>, ) -> Result<(), AppSqliteError> { - self.sync_repository().record_relay_ingest_success( - scope_key, - relay_url, - cursor_since_unix_seconds, - last_event_created_at_unix_seconds, - started_at, - started_unix_seconds, - completed_at, - completed_unix_seconds, - ) + self.sync_repository().record_relay_ingest_success(input) } pub fn record_relay_ingest_failure( &self, - scope_key: &str, - relay_url: &str, - started_at: &str, - started_unix_seconds: i64, - completed_at: &str, - completed_unix_seconds: i64, - error_message: &str, + input: AppRelayIngestFailureInput<'_>, ) -> Result<(), AppSqliteError> { - self.sync_repository().record_relay_ingest_failure( - scope_key, - relay_url, - started_at, - started_unix_seconds, - completed_at, - completed_unix_seconds, - error_message, - ) + self.sync_repository().record_relay_ingest_failure(input) } pub fn record_sync_conflict( @@ -690,24 +705,23 @@ impl AppSqliteStore { } } -fn open_connection(target: &DatabaseTarget) -> Result<Connection, AppSqliteError> { +fn open_connection(target: &DatabaseTarget) -> Result<AppSqliteDatabase, AppSqliteError> { match target { - DatabaseTarget::InMemory => { - Connection::open_in_memory().map_err(|source| AppSqliteError::OpenInMemory { source }) - } + DatabaseTarget::InMemory => AppSqliteDatabase::open_in_memory() + .map_err(|source| AppSqliteError::OpenInMemory { source }), DatabaseTarget::Path(path) => { - if let Some(parent) = path.parent() { - if !parent.as_os_str().is_empty() { - fs::create_dir_all(parent).map_err(|source| { - AppSqliteError::CreateParentDirectory { - path: parent.to_path_buf(), - source, - } - })?; - } + if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + { + fs::create_dir_all(parent).map_err(|source| { + AppSqliteError::CreateParentDirectory { + path: parent.to_path_buf(), + source, + } + })?; } - Connection::open(path).map_err(|source| AppSqliteError::OpenPath { + AppSqliteDatabase::open_path(path).map_err(|source| AppSqliteError::OpenPath { path: path.clone(), source, }) @@ -716,20 +730,19 @@ fn open_connection(target: &DatabaseTarget) -> Result<Connection, AppSqliteError } fn bootstrap_connection( - connection: &mut Connection, + connection: &AppSqliteDatabase, target: &DatabaseTarget, ) -> Result<(), AppSqliteError> { connection - .busy_timeout(Duration::from_millis(SQLITE_BUSY_TIMEOUT_MS)) + .execute_script(&format!("PRAGMA busy_timeout = {SQLITE_BUSY_TIMEOUT_MS}")) .map_err(|source| AppSqliteError::ConfigureBusyTimeout { source })?; - apply_pragma(connection, "foreign_keys", "ON")?; apply_pragma(connection, "synchronous", "NORMAL")?; if matches!(target, DatabaseTarget::Path(_)) { connection - .query_row("PRAGMA journal_mode = WAL", [], |row| { - row.get::<_, String>(0) + .fetch_one("PRAGMA journal_mode = WAL", empty_params(), |row| { + row.try_get::<String, _>(0) }) .map_err(|source| AppSqliteError::ApplyPragma { pragma: "journal_mode", @@ -741,23 +754,25 @@ fn bootstrap_connection( } fn apply_pragma( - connection: &Connection, + connection: &AppSqliteDatabase, pragma: &'static str, value: &str, ) -> Result<(), AppSqliteError> { let sql = format!("PRAGMA {pragma} = {value}"); connection - .execute_batch(&sql) + .execute_script(&sql) .map_err(|source| AppSqliteError::ApplyPragma { pragma, source }) } -fn schema_version(connection: &Connection) -> Result<u32, AppSqliteError> { +fn schema_version(connection: &AppSqliteDatabase) -> Result<u32, AppSqliteError> { connection - .query_row("PRAGMA user_version", [], |row| row.get(0)) + .fetch_one("PRAGMA user_version", empty_params(), |row| { + row.try_get::<u32, _>(0) + }) .map_err(|source| AppSqliteError::ReadSchemaVersion { source }) } -fn apply_migrations(connection: &mut Connection) -> Result<(), AppSqliteError> { +fn apply_migrations(connection: &AppSqliteDatabase) -> Result<(), AppSqliteError> { let current_version = schema_version(connection)?; let latest_version = migrations::latest_schema_version(); @@ -769,18 +784,23 @@ fn apply_migrations(connection: &mut Connection) -> Result<(), AppSqliteError> { } for (version, sql) in migrations::pending_migrations(current_version) { - let transaction = connection - .transaction() + connection + .execute_script("BEGIN IMMEDIATE") .map_err(|source| AppSqliteError::BeginMigration { version, source })?; - transaction - .execute_batch(sql) - .map_err(|source| AppSqliteError::ExecuteMigration { version, source })?; - transaction - .pragma_update(None, "user_version", version) - .map_err(|source| AppSqliteError::RecordSchemaVersion { version, source })?; - transaction - .commit() + if let Err(source) = connection.execute_script(sql) { + let _ = connection.execute_script("ROLLBACK"); + return Err(AppSqliteError::ExecuteMigration { version, source }); + } + + let record_version_sql = format!("PRAGMA user_version = {version}"); + if let Err(source) = connection.execute_script(&record_version_sql) { + let _ = connection.execute_script("ROLLBACK"); + return Err(AppSqliteError::RecordSchemaVersion { version, source }); + } + + connection + .execute_script("COMMIT") .map_err(|source| AppSqliteError::CommitMigration { version, source })?; } @@ -790,7 +810,9 @@ fn apply_migrations(connection: &mut Connection) -> Result<(), AppSqliteError> { #[cfg(test)] mod tests { use super::{AppSqliteStore, DatabaseTarget, latest_schema_version}; - use rusqlite::{Connection, params}; + use sqlx::Row; + + use crate::AppSqliteDatabase; use std::{ env, fs, path::PathBuf, @@ -1072,7 +1094,7 @@ mod tests { 'acknowledged', '2026-01-01T00:00:00Z' )", - [], + crate::empty_params(), ) .expect("local interop imports should accept validation receipt projections"); assert_eq!(row_count(connection, "sync_checkpoints"), 0); @@ -1132,7 +1154,7 @@ mod tests { .execute( "INSERT INTO farms (id, display_name, readiness, created_at, updated_at) VALUES (?1, 'Schema Farm', 'ready', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')", - params!["farm_schema"], + crate::app_sqlite_params!["farm_schema"], ) .expect("farm should insert"); connection @@ -1156,7 +1178,7 @@ mod tests { 'invalid', 'needs_review' )", - [], + crate::empty_params(), ) .expect("agreement-only workflow projection should insert"); connection @@ -1180,7 +1202,7 @@ mod tests { 'agreed_pending_rhi', 'reserved' )", - [], + crate::empty_params(), ) .expect("pending rhi workflow projection should insert"); @@ -1194,7 +1216,7 @@ mod tests { updated_at, workflow_agreement ) VALUES ('order_agreement_invalid', 'farm_schema', 'invalid', 'Buyer', 'scheduled', '2026-01-01T00:00:00Z', 'complete')", - [], + crate::empty_params(), ); assert!(invalid_result.is_err()); } @@ -1230,7 +1252,7 @@ mod tests { .execute( "INSERT INTO farms (id, display_name, readiness, created_at, updated_at) VALUES (?1, 'Schema Farm', 'ready', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')", - params!["farm_status"], + crate::app_sqlite_params!["farm_status"], ) .expect("farm should insert"); connection @@ -1260,7 +1282,7 @@ mod tests { '', '' )", - [], + crate::empty_params(), ) .expect("order should insert"); connection @@ -1278,7 +1300,7 @@ mod tests { 2, '2 each' )", - [], + crate::empty_params(), ) .expect("order line should insert"); connection @@ -1296,7 +1318,7 @@ mod tests { '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z' )", - [], + crate::empty_params(), ) .expect("buyer coordination should insert"); @@ -1312,62 +1334,62 @@ mod tests { connection .execute( "UPDATE orders SET status = 'declined' WHERE id = 'order_status'", - [], + crate::empty_params(), ) .expect("declined status should satisfy current check"); connection .execute( "UPDATE orders SET status = 'needs_review' WHERE id = 'order_status'", - [], + crate::empty_params(), ) .expect("needs review status should satisfy current check"); let status: String = connection .query_row( "SELECT status FROM orders WHERE id = 'order_status'", - [], - |row| row.get(0), + crate::empty_params(), + |row| row.try_get(0), ) .expect("status should load"); assert_eq!(status, "needs_review"); connection .execute( "UPDATE orders SET workflow_agreement = 'agreed_pending_rhi' WHERE id = 'order_status'", - [], + crate::empty_params(), ) .expect("agreed pending rhi agreement should satisfy current check"); } - fn table_exists(connection: &Connection, table_name: &str) -> bool { + fn table_exists(connection: &AppSqliteDatabase, table_name: &str) -> bool { connection .query_row( "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?1)", [table_name], - |row| row.get::<_, i64>(0), + |row| row.try_get::<i64, _>(0), ) .expect("table existence query should succeed") == 1 } - fn row_count(connection: &Connection, table_name: &str) -> i64 { + fn row_count(connection: &AppSqliteDatabase, table_name: &str) -> i64 { let sql = format!("SELECT COUNT(*) FROM {table_name}"); connection - .query_row(&sql, [], |row| row.get(0)) + .query_row(&sql, crate::empty_params(), |row| row.try_get(0)) .expect("row count query should succeed") } - fn column_exists(connection: &Connection, table_name: &str, column_name: &str) -> bool { + fn column_exists(connection: &AppSqliteDatabase, table_name: &str, column_name: &str) -> bool { let sql = format!("PRAGMA table_info({table_name})"); let mut statement = connection .prepare(&sql) .expect("table info statement should prepare"); let mut rows = statement - .query([]) + .query(crate::empty_params()) .expect("table info query should succeed"); while let Some(row) = rows.next().expect("table info row should load") { if row - .get::<_, String>(1) + .try_get::<String, _>(1) .expect("table info name should load") == column_name { @@ -1378,11 +1400,13 @@ mod tests { false } - fn foreign_key_violation_count(connection: &Connection) -> usize { + fn foreign_key_violation_count(connection: &AppSqliteDatabase) -> usize { let mut statement = connection .prepare("PRAGMA foreign_key_check") .expect("foreign key check should prepare"); - let mut rows = statement.query([]).expect("foreign key check should run"); + let mut rows = statement + .query(crate::empty_params()) + .expect("foreign key check should run"); let mut count = 0; while rows .next() @@ -1394,17 +1418,17 @@ mod tests { count } - fn pragma_i64(connection: &Connection, pragma_name: &str) -> i64 { + fn pragma_i64(connection: &AppSqliteDatabase, pragma_name: &str) -> i64 { let sql = format!("PRAGMA {pragma_name}"); connection - .query_row(&sql, [], |row| row.get(0)) + .query_row(&sql, crate::empty_params(), |row| row.try_get(0)) .expect("pragma query should succeed") } - fn pragma_text(connection: &Connection, pragma_name: &str) -> String { + fn pragma_text(connection: &AppSqliteDatabase, pragma_name: &str) -> String { let sql = format!("PRAGMA {pragma_name}"); connection - .query_row(&sql, [], |row| row.get(0)) + .query_row(&sql, crate::empty_params(), |row| row.try_get(0)) .expect("pragma query should succeed") } diff --git a/crates/store/src/repo/activation.rs b/crates/store/src/repo/activation.rs @@ -2,16 +2,16 @@ use radroots_studio_app_view::{ AccountSurfaceActivationProjection, ActiveSurface, FarmId, FarmerActivationProjection, SelectedSurfaceProjection, }; -use rusqlite::{Connection, OptionalExtension, params}; +use sqlx::Row; -use crate::AppSqliteError; +use crate::{AppSqliteDatabase, AppSqliteError}; pub struct AppActivationRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppActivationRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } @@ -21,21 +21,20 @@ impl<'a> AppActivationRepository<'a> { ) -> Result<Option<AccountSurfaceActivationProjection>, AppSqliteError> { let row = self .connection - .query_row( + .fetch_optional( "SELECT account_id, selected_surface, farmer_farm_id FROM account_surface_activations WHERE account_id = ?1 LIMIT 1", - [account_id], + crate::app_sqlite_params![account_id], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, Option<String>>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<Option<String>, _>(2)?, )) }, ) - .optional() .map_err(|source| AppSqliteError::Query { operation: "load account surface activation", source, @@ -64,7 +63,7 @@ impl<'a> AppActivationRepository<'a> { projection: &AccountSurfaceActivationProjection, ) -> Result<(), AppSqliteError> { self.connection - .execute( + .execute_statement( "INSERT INTO account_surface_activations ( account_id, selected_surface, @@ -75,7 +74,7 @@ impl<'a> AppActivationRepository<'a> { selected_surface = excluded.selected_surface, farmer_farm_id = excluded.farmer_farm_id, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ projection.account_id, projection.active_surface().storage_key(), projection @@ -94,9 +93,9 @@ impl<'a> AppActivationRepository<'a> { pub fn clear_surface_activation(&self, account_id: &str) -> Result<(), AppSqliteError> { self.connection - .execute( + .execute_statement( "DELETE FROM account_surface_activations WHERE account_id = ?1", - [account_id], + crate::app_sqlite_params![account_id], ) .map_err(|source| AppSqliteError::Query { operation: "clear account surface activation", @@ -157,7 +156,7 @@ mod tests { let projection = AccountSurfaceActivationProjection::new( "acct_farmer", SelectedSurfaceProjection::new(ActiveSurface::Farmer), - FarmerActivationProjection::active(FarmId::new()), + FarmerActivationProjection::active(FarmId::generate()), ); store @@ -178,7 +177,7 @@ mod tests { let first = AccountSurfaceActivationProjection::new( "acct_surface", SelectedSurfaceProjection::new(ActiveSurface::Farmer), - FarmerActivationProjection::active(FarmId::new()), + FarmerActivationProjection::active(FarmId::generate()), ); let second = AccountSurfaceActivationProjection::new( "acct_surface", diff --git a/crates/store/src/repo/activity.rs b/crates/store/src/repo/activity.rs @@ -2,31 +2,31 @@ use radroots_studio_app_view::{ ActivityEventId, AppActivityContext, AppActivityEvent, AppActivityKind, SettingsPreference, SettingsSection, }; -use rusqlite::{Connection, params}; +use sqlx::Row; -use crate::AppSqliteError; +use crate::{AppSqliteDatabase, AppSqliteError}; pub const APP_ACTIVITY_CONTEXT_LIMIT: usize = 64; pub const APP_ACTIVITY_RETENTION_LIMIT: i64 = 5_000; pub struct AppActivityRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppActivityRepository<'a> { - pub fn new(connection: &'a Connection) -> Self { + pub(crate) fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } pub fn record(&self, kind: &AppActivityKind) -> Result<(), AppSqliteError> { - let activity_event_id = ActivityEventId::new().to_string(); + let activity_event_id = ActivityEventId::generate().to_string(); let event_kind = kind.storage_key(); let settings_section = settings_section_value(kind); let settings_preference = settings_preference_value(kind); let preference_enabled = preference_enabled_value(kind); self.connection - .execute( + .execute_statement( "INSERT INTO activity_events ( activity_event_id, event_kind, @@ -34,7 +34,7 @@ impl<'a> AppActivityRepository<'a> { settings_preference, preference_enabled ) VALUES (?1, ?2, ?3, ?4, ?5)", - params![ + crate::app_sqlite_params![ activity_event_id, event_kind, settings_section, @@ -53,9 +53,12 @@ impl<'a> AppActivityRepository<'a> { } pub fn load_recent(&self, limit: usize) -> Result<Vec<AppActivityEvent>, AppSqliteError> { - let mut statement = self + let query_limit = i64::try_from(limit).map_err(|_| AppSqliteError::InvalidProjection { + reason: "activity query limit exceeds sqlite integer range", + })?; + let rows = self .connection - .prepare( + .fetch_mapped( "SELECT activity_event_id, recorded_at, @@ -66,30 +69,26 @@ impl<'a> AppActivityRepository<'a> { FROM activity_events ORDER BY recorded_at DESC, activity_event_id DESC LIMIT ?1", + crate::app_sqlite_params![query_limit], + |row| { + let activity_event_id = row.try_get::<String, _>(0)?; + let recorded_at = row.try_get::<String, _>(1)?; + let event_kind = row.try_get::<String, _>(2)?; + let settings_section = row.try_get::<Option<String>, _>(3)?; + let settings_preference = row.try_get::<Option<String>, _>(4)?; + let preference_enabled = row.try_get::<Option<i64>, _>(5)?; + + Ok(( + activity_event_id, + recorded_at, + event_kind, + settings_section, + settings_preference, + preference_enabled, + )) + }, ) .map_err(|source| AppSqliteError::Query { - operation: "prepare recent activity query", - source, - })?; - let rows = statement - .query_map([limit as i64], |row| { - let activity_event_id = row.get::<_, String>(0)?; - let recorded_at = row.get::<_, String>(1)?; - let event_kind = row.get::<_, String>(2)?; - let settings_section = row.get::<_, Option<String>>(3)?; - let settings_preference = row.get::<_, Option<String>>(4)?; - let preference_enabled = row.get::<_, Option<i64>>(5)?; - - Ok(( - activity_event_id, - recorded_at, - event_kind, - settings_section, - settings_preference, - preference_enabled, - )) - }) - .map_err(|source| AppSqliteError::Query { operation: "query recent activity events", source, })?; @@ -127,7 +126,7 @@ impl<'a> AppActivityRepository<'a> { fn trim_retained_events(&self, retention_limit: i64) -> Result<(), AppSqliteError> { self.connection - .execute( + .execute_statement( "DELETE FROM activity_events WHERE activity_event_id IN ( SELECT activity_event_id @@ -135,7 +134,7 @@ impl<'a> AppActivityRepository<'a> { ORDER BY recorded_at DESC, activity_event_id DESC LIMIT -1 OFFSET ?1 )", - [retention_limit], + crate::app_sqlite_params![retention_limit], ) .map_err(|source| AppSqliteError::Query { operation: "trim retained activity events", @@ -266,9 +265,9 @@ fn preference_enabled_value(kind: &AppActivityKind) -> Option<i64> { #[cfg(test)] mod tests { use radroots_studio_app_view::{AppActivityKind, SettingsPreference, SettingsSection}; - use rusqlite::Connection; + use sqlx::Row; - use crate::{AppSqliteStore, DatabaseTarget}; + use crate::{AppSqliteDatabase, AppSqliteStore, DatabaseTarget, empty_params}; use super::{APP_ACTIVITY_CONTEXT_LIMIT, APP_ACTIVITY_RETENTION_LIMIT, AppActivityRepository}; @@ -330,12 +329,12 @@ mod tests { #[test] fn activity_repository_trims_events_to_retention_limit() { - let connection = Connection::open_in_memory().expect("open in-memory connection"); + let connection = AppSqliteDatabase::open_in_memory().expect("open in-memory connection"); connection - .execute_batch(include_str!("../../migrations/0001_init.sql")) + .execute_script(include_str!("../../migrations/0001_init.sql")) .expect("apply init migration"); connection - .execute_batch(include_str!("../../migrations/0002_activity_journal.sql")) + .execute_script(include_str!("../../migrations/0002_activity_journal.sql")) .expect("apply activity migration"); let repository = AppActivityRepository::new(&connection); @@ -350,10 +349,10 @@ mod tests { assert_eq!(retained, APP_ACTIVITY_RETENTION_LIMIT); } - fn count_rows(connection: &Connection, table_name: &str) -> i64 { + fn count_rows(connection: &AppSqliteDatabase, table_name: &str) -> i64 { let sql = format!("SELECT COUNT(*) FROM {table_name}"); connection - .query_row(&sql, [], |row| row.get(0)) + .fetch_one(&sql, empty_params(), |row| row.try_get(0)) .expect("row count query should succeed") } } diff --git a/crates/store/src/repo/buyer.rs b/crates/store/src/repo/buyer.rs @@ -10,7 +10,9 @@ use radroots_studio_app_view::{ ProductPricePresentation, ProductStatus, ProductStockState, ProductStockSummary, RepeatDemandEligibility, RepeatDemandHandoffProjection, }; -use rusqlite::{Connection, OptionalExtension, params, params_from_iter}; +use sqlx::Row; + +use crate::{AppSqliteDatabase, OptionalSqliteResult}; use serde_json::Value; use super::{ @@ -147,11 +149,11 @@ pub struct BuyerOrderCoordinationRecord { } pub struct AppBuyerRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppBuyerRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } @@ -231,7 +233,7 @@ impl<'a> AppBuyerRepository<'a> { on conflict(buyer_context_key) do update set farm_id = excluded.farm_id, updated_at = excluded.updated_at", - params![context_key.as_str(), farm_id.map(|id| id.to_string())], + crate::app_sqlite_params![context_key.as_str(), farm_id.map(|id| id.to_string())], ) .map_err(|source| AppSqliteError::Query { operation: "save buyer cart header", @@ -240,7 +242,7 @@ impl<'a> AppBuyerRepository<'a> { self.connection .execute( "delete from buyer_cart_lines where buyer_context_key = ?1", - params![context_key.as_str()], + crate::app_sqlite_params![context_key.as_str()], ) .map_err(|source| AppSqliteError::Query { operation: "clear buyer cart lines", @@ -267,7 +269,7 @@ impl<'a> AppBuyerRepository<'a> { seller_pubkey, updated_at ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))", - params![ + crate::app_sqlite_params![ context_key.as_str(), line.product_id.to_string(), i64::from(line.quantity), @@ -297,7 +299,7 @@ impl<'a> AppBuyerRepository<'a> { self.connection .execute( "delete from buyer_cart_lines where buyer_context_key = ?1", - params![context_key.as_str()], + crate::app_sqlite_params![context_key.as_str()], ) .map_err(|source| AppSqliteError::Query { operation: "delete buyer cart lines", @@ -311,7 +313,7 @@ impl<'a> AppBuyerRepository<'a> { buyer_order_note = '', updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') where buyer_context_key = ?1", - params![context_key.as_str()], + crate::app_sqlite_params![context_key.as_str()], ) .map_err(|source| AppSqliteError::Query { operation: "clear buyer cart header", @@ -369,7 +371,7 @@ impl<'a> AppBuyerRepository<'a> { updated_at ) values (?1, null, strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) on conflict(buyer_context_key) do nothing", - params![context_key.as_str()], + crate::app_sqlite_params![context_key.as_str()], ) .map_err(|source| AppSqliteError::Query { operation: "ensure buyer order review header", @@ -386,7 +388,7 @@ impl<'a> AppBuyerRepository<'a> { buyer_order_note_public_confirmed = ?6, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') where buyer_context_key = ?1", - params![ + crate::app_sqlite_params![ context_key.as_str(), draft.name.trim(), draft.email.trim(), @@ -424,7 +426,7 @@ impl<'a> AppBuyerRepository<'a> { reason: "buyer cart farm is missing", })?; let fulfillment_window_id = shared_fulfillment_window_id(&line_records)?; - let order_id = OrderId::new(); + let order_id = OrderId::generate(); let order_number = self.next_order_number(farm_id)?; self.connection @@ -464,7 +466,7 @@ impl<'a> AppBuyerRepository<'a> { ?9, ?10 )", - params![ + crate::app_sqlite_params![ order_id.to_string(), farm_id.to_string(), fulfillment_window_id.map(|id| id.to_string()), @@ -503,7 +505,7 @@ impl<'a> AppBuyerRepository<'a> { seller_pubkey, sort_index ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", - params![ + crate::app_sqlite_params![ format!("{}:{}", order_id, line.listing.product_id), order_id.to_string(), line.listing.title, @@ -530,7 +532,7 @@ impl<'a> AppBuyerRepository<'a> { self.connection .execute( "delete from buyer_cart_lines where buyer_context_key = ?1", - params![context_key.as_str()], + crate::app_sqlite_params![context_key.as_str()], ) .map_err(|source| AppSqliteError::Query { operation: "clear buyer cart lines after order review", @@ -545,7 +547,7 @@ impl<'a> AppBuyerRepository<'a> { buyer_order_note_public_confirmed = 0, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') where buyer_context_key = ?1", - params![context_key.as_str()], + crate::app_sqlite_params![context_key.as_str()], ) .map_err(|source| AppSqliteError::Query { operation: "reset buyer cart header after order review", @@ -593,16 +595,16 @@ impl<'a> AppBuyerRepository<'a> { from buyer_order_coordination_records where buyer_context_key = ?1 and order_id = ?2 limit 1", - params![context_key.as_str(), order_id.to_string()], + crate::app_sqlite_params![context_key.as_str(), order_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, Option<String>>(2)?, - row.get::<_, String>(3)?, - row.get::<_, Option<String>>(4)?, - row.get::<_, i64>(5)?, - row.get::<_, Option<String>>(6)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<Option<String>, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<Option<String>, _>(4)?, + row.try_get::<i64, _>(5)?, + row.try_get::<Option<String>, _>(6)?, )) }, ) @@ -640,15 +642,15 @@ impl<'a> AppBuyerRepository<'a> { source, })?; let rows = statement - .query_map(params![context_key.as_str()], |row| { + .query_map(crate::app_sqlite_params![context_key.as_str()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, Option<String>>(2)?, - row.get::<_, String>(3)?, - row.get::<_, Option<String>>(4)?, - row.get::<_, i64>(5)?, - row.get::<_, Option<String>>(6)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<Option<String>, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<Option<String>, _>(4)?, + row.try_get::<i64, _>(5)?, + row.try_get::<Option<String>, _>(6)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -721,7 +723,7 @@ impl<'a> AppBuyerRepository<'a> { synced_at = null where buyer_order_coordination_records.buyer_context_key = excluded.buyer_context_key and buyer_order_coordination_records.state <> 'synced'", - params![ + crate::app_sqlite_params![ order_id.to_string(), context_key.as_str(), record_id, @@ -752,7 +754,7 @@ impl<'a> AppBuyerRepository<'a> { updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), synced_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') where buyer_context_key = ?1 and order_id = ?2", - params![context_key.as_str(), order_id.to_string()], + crate::app_sqlite_params![context_key.as_str(), order_id.to_string()], ) .map_err(|source| AppSqliteError::Query { operation: "mark buyer order coordination synced", @@ -779,7 +781,11 @@ impl<'a> AppBuyerRepository<'a> { updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), synced_at = null where buyer_context_key = ?1 and order_id = ?2", - params![context_key.as_str(), order_id.to_string(), error_message], + crate::app_sqlite_params![ + context_key.as_str(), + order_id.to_string(), + error_message + ], ) .map_err(|source| AppSqliteError::Query { operation: "mark buyer order coordination failed", @@ -836,21 +842,21 @@ impl<'a> AppBuyerRepository<'a> { source, })?; let rows = statement - .query_map(params_from_iter(context_keys.iter()), |row| { + .query_map(context_keys.to_vec(), |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, String>(7)?, - row.get::<_, Option<String>>(8)?, - row.get::<_, String>(9)?, - row.get::<_, Option<String>>(10)?, - row.get::<_, Option<String>>(11)?, - row.get::<_, Option<String>>(12)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<String, _>(7)?, + row.try_get::<Option<String>, _>(8)?, + row.try_get::<String, _>(9)?, + row.try_get::<Option<String>, _>(10)?, + row.try_get::<Option<String>, _>(11)?, + row.try_get::<Option<String>, _>(12)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -968,22 +974,22 @@ impl<'a> AppBuyerRepository<'a> { params.push(order_id.to_string()); let record = self .connection - .query_row(query.as_str(), params_from_iter(params.iter()), |row| { + .query_row(query.as_str(), params, |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, String>(7)?, - row.get::<_, String>(8)?, - row.get::<_, Option<String>>(9)?, - row.get::<_, String>(10)?, - row.get::<_, Option<String>>(11)?, - row.get::<_, Option<String>>(12)?, - row.get::<_, Option<String>>(13)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<String, _>(7)?, + row.try_get::<String, _>(8)?, + row.try_get::<Option<String>, _>(9)?, + row.try_get::<String, _>(10)?, + row.try_get::<Option<String>, _>(11)?, + row.try_get::<Option<String>, _>(12)?, + row.try_get::<Option<String>, _>(13)?, )) }) .optional() @@ -1088,25 +1094,25 @@ impl<'a> AppBuyerRepository<'a> { left join fulfillment_windows fw on fw.id = o.fulfillment_window_id where o.buyer_context_key = ?1 and o.id = ?2 limit 1", - params![context_key.as_str(), order_id.to_string()], + crate::app_sqlite_params![context_key.as_str(), order_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, Option<String>>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, String>(7)?, - row.get::<_, String>(8)?, - row.get::<_, bool>(9)?, - row.get::<_, String>(10)?, - row.get::<_, String>(11)?, - row.get::<_, Option<String>>(12)?, - row.get::<_, Option<String>>(13)?, - row.get::<_, Option<String>>(14)?, - row.get::<_, Option<String>>(15)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<Option<String>, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<String, _>(7)?, + row.try_get::<String, _>(8)?, + row.try_get::<bool, _>(9)?, + row.try_get::<String, _>(10)?, + row.try_get::<String, _>(11)?, + row.try_get::<Option<String>, _>(12)?, + row.try_get::<Option<String>, _>(13)?, + row.try_get::<Option<String>, _>(14)?, + row.try_get::<Option<String>, _>(15)?, )) }, ) @@ -1293,9 +1299,11 @@ impl<'a> AppBuyerRepository<'a> { fn current_utc_timestamp(&self) -> Result<String, AppSqliteError> { self.connection - .query_row("select strftime('%Y-%m-%dT%H:%M:%SZ', 'now')", [], |row| { - row.get(0) - }) + .query_row( + "select strftime('%Y-%m-%dT%H:%M:%SZ', 'now')", + crate::empty_params(), + |row| row.try_get(0), + ) .map_err(|source| AppSqliteError::Query { operation: "load buyer current utc timestamp", source, @@ -1324,7 +1332,7 @@ impl<'a> AppBuyerRepository<'a> { strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), strftime('%Y-%m-%dT%H:%M:%SZ', 'now') )", - params![order_id.to_string(), context_key], + crate::app_sqlite_params![order_id.to_string(), context_key], ) .map_err(|source| AppSqliteError::Query { operation: "insert pending buyer order coordination record", @@ -1428,33 +1436,33 @@ impl<'a> AppBuyerRepository<'a> { source, })?; let rows = statement - .query_map([], |row| { + .query_map(crate::empty_params(), |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, String>(7)?, - row.get::<_, Option<u32>>(8)?, - row.get::<_, String>(9)?, - row.get::<_, Option<String>>(10)?, - row.get::<_, Option<String>>(11)?, - row.get::<_, Option<String>>(12)?, - row.get::<_, Option<String>>(13)?, - row.get::<_, Option<String>>(14)?, - row.get::<_, Option<String>>(15)?, - row.get::<_, Option<u32>>(16)?, - row.get::<_, Option<String>>(17)?, - row.get::<_, Option<String>>(18)?, - row.get::<_, Option<String>>(19)?, - row.get::<_, Option<String>>(20)?, - row.get::<_, Option<String>>(21)?, - row.get::<_, i64>(22)?, - row.get::<_, i64>(23)?, - row.get::<_, i64>(24)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<String, _>(7)?, + row.try_get::<Option<u32>, _>(8)?, + row.try_get::<String, _>(9)?, + row.try_get::<Option<String>, _>(10)?, + row.try_get::<Option<String>, _>(11)?, + row.try_get::<Option<String>, _>(12)?, + row.try_get::<Option<String>, _>(13)?, + row.try_get::<Option<String>, _>(14)?, + row.try_get::<Option<String>, _>(15)?, + row.try_get::<Option<u32>, _>(16)?, + row.try_get::<Option<String>, _>(17)?, + row.try_get::<Option<String>, _>(18)?, + row.try_get::<Option<String>, _>(19)?, + row.try_get::<Option<String>, _>(20)?, + row.try_get::<Option<String>, _>(21)?, + row.try_get::<i64, _>(22)?, + row.try_get::<i64, _>(23)?, + row.try_get::<i64, _>(24)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -1582,15 +1590,15 @@ impl<'a> AppBuyerRepository<'a> { from buyer_carts where buyer_context_key = ?1 limit 1", - params![context_key], + crate::app_sqlite_params![context_key], |row| { Ok(( - row.get::<_, Option<String>>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, bool>(5)?, + row.try_get::<Option<String>, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<bool, _>(5)?, )) }, ) @@ -1723,34 +1731,34 @@ impl<'a> AppBuyerRepository<'a> { source, })?; let rows = statement - .query_map(params![context_key], |row| { + .query_map(crate::app_sqlite_params![context_key], |row| { Ok(( - row.get::<_, u32>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, String>(7)?, - row.get::<_, String>(8)?, - row.get::<_, Option<u32>>(9)?, - row.get::<_, String>(10)?, - row.get::<_, Option<String>>(11)?, - row.get::<_, Option<String>>(12)?, - row.get::<_, Option<String>>(13)?, - row.get::<_, Option<String>>(14)?, - row.get::<_, Option<String>>(15)?, - row.get::<_, Option<String>>(16)?, - row.get::<_, Option<u32>>(17)?, - row.get::<_, Option<String>>(18)?, - row.get::<_, Option<String>>(19)?, - row.get::<_, Option<String>>(20)?, - row.get::<_, Option<String>>(21)?, - row.get::<_, Option<String>>(22)?, - row.get::<_, i64>(23)?, - row.get::<_, i64>(24)?, - row.get::<_, i64>(25)?, + row.try_get::<u32, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<String, _>(7)?, + row.try_get::<String, _>(8)?, + row.try_get::<Option<u32>, _>(9)?, + row.try_get::<String, _>(10)?, + row.try_get::<Option<String>, _>(11)?, + row.try_get::<Option<String>, _>(12)?, + row.try_get::<Option<String>, _>(13)?, + row.try_get::<Option<String>, _>(14)?, + row.try_get::<Option<String>, _>(15)?, + row.try_get::<Option<String>, _>(16)?, + row.try_get::<Option<u32>, _>(17)?, + row.try_get::<Option<String>, _>(18)?, + row.try_get::<Option<String>, _>(19)?, + row.try_get::<Option<String>, _>(20)?, + row.try_get::<Option<String>, _>(21)?, + row.try_get::<Option<String>, _>(22)?, + row.try_get::<i64, _>(23)?, + row.try_get::<i64, _>(24)?, + row.try_get::<i64, _>(25)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -1862,14 +1870,14 @@ impl<'a> AppBuyerRepository<'a> { source, })?; let rows = statement - .query_map(params![order_id], |row| { + .query_map(crate::app_sqlite_params![order_id], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, i64>(2)?, - row.get::<_, String>(3)?, - row.get::<_, Option<u32>>(4)?, - row.get::<_, Option<String>>(5)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<i64, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<Option<u32>, _>(4)?, + row.try_get::<Option<String>, _>(5)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -1934,21 +1942,21 @@ impl<'a> AppBuyerRepository<'a> { source, })?; let rows = statement - .query_map(params![order_id_string.as_str()], |row| { + .query_map(crate::app_sqlite_params![order_id_string.as_str()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, i64>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, Option<u32>>(5)?, - row.get::<_, Option<String>>(6)?, - row.get::<_, Option<String>>(7)?, - row.get::<_, Option<String>>(8)?, - row.get::<_, Option<String>>(9)?, - row.get::<_, Option<String>>(10)?, - row.get::<_, Option<String>>(11)?, - row.get::<_, Option<String>>(12)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<i64, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<Option<u32>, _>(5)?, + row.try_get::<Option<String>, _>(6)?, + row.try_get::<Option<String>, _>(7)?, + row.try_get::<Option<String>, _>(8)?, + row.try_get::<Option<String>, _>(9)?, + row.try_get::<Option<String>, _>(10)?, + row.try_get::<Option<String>, _>(11)?, + row.try_get::<Option<String>, _>(12)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -2024,11 +2032,11 @@ impl<'a> AppBuyerRepository<'a> { source, })?; let rows = statement - .query_map(params![order_id.to_string()], |row| { + .query_map(crate::app_sqlite_params![order_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, i64>(1)?, - row.get::<_, Option<String>>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<i64, _>(1)?, + row.try_get::<Option<String>, _>(2)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -2084,8 +2092,8 @@ impl<'a> AppBuyerRepository<'a> { let mut params = context_keys.to_vec(); params.push(order_id.to_string()); self.connection - .query_row(query.as_str(), params_from_iter(params.iter()), |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) + .query_row(query.as_str(), params, |row| { + Ok((row.try_get::<String, _>(0)?, row.try_get::<String, _>(1)?)) }) .optional() .map_err(|source| AppSqliteError::Query { @@ -2187,8 +2195,8 @@ impl<'a> AppBuyerRepository<'a> { self.connection .query_row( "select display_name from farms where id = ?1 limit 1", - params![farm_id.to_string()], - |row| row.get::<_, String>(0), + crate::app_sqlite_params![farm_id.to_string()], + |row| row.try_get::<String, _>(0), ) .optional() .map_err(|source| AppSqliteError::Query { @@ -2206,8 +2214,8 @@ impl<'a> AppBuyerRepository<'a> { where farm_id = ?1 and order_number like 'R-%' and substr(order_number, 3) glob '[0-9]*'", - params![farm_id.to_string()], - |row| row.get::<_, i64>(0), + crate::app_sqlite_params![farm_id.to_string()], + |row| row.try_get::<i64, _>(0), ) .map_err(|source| AppSqliteError::Query { operation: "load next buyer order number", @@ -2978,7 +2986,9 @@ mod tests { OrderId, PickupLocationId, ProductId, ProductPricePresentation, TradeAgreementStatus, TradeInventoryStatus, TradeRevisionStatus, TradeWorkflowSource, }; - use rusqlite::{Connection, params}; + use sqlx::Row; + + use crate::AppSqliteDatabase; use serde_json::json; use crate::{ @@ -2994,11 +3004,11 @@ mod tests { #[test] fn buyer_order_review_requires_public_note_confirmation() { let cart = BuyerCartProjection { - farm_id: Some(FarmId::new()), + farm_id: Some(FarmId::generate()), farm_display_name: Some("Willow Farm".to_owned()), lines: vec![BuyerCartLineProjection { - product_id: ProductId::new(), - farm_id: FarmId::new(), + product_id: ProductId::generate(), + farm_id: FarmId::generate(), farm_display_name: "Willow Farm".to_owned(), title: "Salad mix".to_owned(), quantity: 1, @@ -3219,8 +3229,8 @@ mod tests { let connection = store.connection(); let repository = AppBuyerRepository::new(connection); let farm_id = insert_farm(connection, "Willow Farm", "ready"); - let account_order_id = OrderId::new(); - let relay_order_id = OrderId::new(); + let account_order_id = OrderId::generate(); + let relay_order_id = OrderId::generate(); let linked_context_key = format!("nostr:{LINKED_BUYER_PUBKEY}"); insert_order( connection, @@ -3491,7 +3501,7 @@ mod tests { connection .execute( "update products set status = 'archived' where id = ?1", - params![unavailable_product_id.to_string()], + crate::app_sqlite_params![unavailable_product_id.to_string()], ) .expect("product should archive"); @@ -3618,7 +3628,10 @@ mod tests { connection .execute( "update orders set buyer_context_key = ?1 where id = ?2", - params![format!("nostr:{LINKED_BUYER_PUBKEY}"), order_id.to_string()], + crate::app_sqlite_params![ + format!("nostr:{LINKED_BUYER_PUBKEY}"), + order_id.to_string() + ], ) .expect("linked order context should mutate"); @@ -3640,8 +3653,8 @@ mod tests { let nostr_cart_line_count: u32 = connection .query_row( "select count(*) from buyer_cart_lines where buyer_context_key = ?1", - params![format!("nostr:{LINKED_BUYER_PUBKEY}")], - |row| row.get(0), + crate::app_sqlite_params![format!("nostr:{LINKED_BUYER_PUBKEY}")], + |row| row.try_get(0), ) .expect("nostr cart line count should load"); @@ -3733,7 +3746,7 @@ mod tests { connection .execute( "update products set stock_count = 1 where id = ?1", - params![product_id.to_string()], + crate::app_sqlite_params![product_id.to_string()], ) .expect("product stock should lower"); @@ -3774,7 +3787,7 @@ mod tests { insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, "R-100", "needs_action", @@ -3785,7 +3798,7 @@ mod tests { ); insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, "R-101", "scheduled", @@ -3796,7 +3809,7 @@ mod tests { ); insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, "R-102", "packed", @@ -3826,7 +3839,7 @@ mod tests { let repository = AppBuyerRepository::new(connection); let context = BuyerContext::Guest; let farm_id = insert_farm(connection, "Willow Farm", "ready"); - let order_id = OrderId::new(); + let order_id = OrderId::generate(); insert_order( connection, @@ -3879,8 +3892,8 @@ mod tests { let repository = AppBuyerRepository::new(connection); let context = BuyerContext::account("acct_buyer"); let farm_id = insert_farm(connection, "Willow Farm", "ready"); - let order_id = OrderId::new(); - let product_id = ProductId::new(); + let order_id = OrderId::generate(); + let product_id = ProductId::generate(); insert_order( connection, @@ -3949,7 +3962,7 @@ mod tests { let repository = AppBuyerRepository::new(connection); let context = BuyerContext::account("acct_buyer"); let farm_id = insert_farm(connection, "Willow Farm", "ready"); - let order_id = OrderId::new(); + let order_id = OrderId::generate(); insert_order( connection, @@ -4004,8 +4017,8 @@ mod tests { #[test] fn buyer_cart_rejects_cross_farm_lines() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); - let farm_id = FarmId::new(); - let other_farm_id = FarmId::new(); + let farm_id = FarmId::generate(); + let other_farm_id = FarmId::generate(); let error = repository_error(&store, farm_id, other_farm_id); @@ -4024,7 +4037,7 @@ mod tests { farm_id: Some(farm_id), farm_display_name: Some("Willow Farm".to_owned()), lines: vec![radroots_studio_app_view::BuyerCartLineProjection { - product_id: ProductId::new(), + product_id: ProductId::generate(), farm_id: other_farm_id, farm_display_name: "Other Farm".to_owned(), title: "Mismatch".to_owned(), @@ -4045,8 +4058,8 @@ mod tests { .expect_err("cross-farm cart should fail") } - fn insert_farm(connection: &Connection, display_name: &str, readiness: &str) -> FarmId { - let farm_id = FarmId::new(); + fn insert_farm(connection: &AppSqliteDatabase, display_name: &str, readiness: &str) -> FarmId { + let farm_id = FarmId::generate(); connection .execute( @@ -4059,7 +4072,7 @@ mod tests { created_at, updated_at ) values (?1, ?2, ?3, 'UTC', 'USD', '2026-04-20T08:00:00Z', '2026-04-20T08:00:00Z')", - params![farm_id.to_string(), display_name, readiness], + crate::app_sqlite_params![farm_id.to_string(), display_name, readiness], ) .expect("farm insert should succeed"); @@ -4067,11 +4080,11 @@ mod tests { } fn insert_pickup_location( - connection: &Connection, + connection: &AppSqliteDatabase, farm_id: FarmId, label: &str, ) -> PickupLocationId { - let pickup_location_id = PickupLocationId::new(); + let pickup_location_id = PickupLocationId::generate(); connection .execute( @@ -4085,7 +4098,7 @@ mod tests { created_at, updated_at ) values (?1, ?2, ?3, '14 County Road', null, 1, '2026-04-20T08:00:00Z', '2026-04-20T08:00:00Z')", - params![pickup_location_id.to_string(), farm_id.to_string(), label], + crate::app_sqlite_params![pickup_location_id.to_string(), farm_id.to_string(), label], ) .expect("pickup location insert should succeed"); @@ -4093,14 +4106,14 @@ mod tests { } fn insert_window( - connection: &Connection, + connection: &AppSqliteDatabase, farm_id: FarmId, pickup_location_id: Option<PickupLocationId>, label: &str, starts_at: &str, ends_at: &str, ) -> FulfillmentWindowId { - let fulfillment_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); connection .execute( @@ -4116,7 +4129,7 @@ mod tests { label, order_cutoff_at ) values (?1, ?2, ?3, ?4, null, ?3, ?3, ?5, ?6, ?3)", - params![ + crate::app_sqlite_params![ fulfillment_window_id.to_string(), farm_id.to_string(), starts_at, @@ -4131,7 +4144,7 @@ mod tests { } fn insert_farm_setup_binding( - connection: &Connection, + connection: &AppSqliteDatabase, account_id: &str, farm_id: FarmId, pickup_enabled: bool, @@ -4152,7 +4165,7 @@ mod tests { saved_farm_readiness, updated_at ) values (?1, 'Willow Farm', 'County Road', ?2, ?3, ?4, ?5, 'Willow Farm', 'ready', '2026-04-20T08:00:00Z')", - params![ + crate::app_sqlite_params![ account_id, i64::from(pickup_enabled), i64::from(delivery_enabled), @@ -4175,11 +4188,11 @@ mod tests { } fn insert_product( - connection: &Connection, + connection: &AppSqliteDatabase, farm_id: FarmId, product: SeedProduct<'_>, ) -> ProductId { - let product_id = ProductId::new(); + let product_id = ProductId::generate(); connection .execute( @@ -4196,7 +4209,7 @@ mod tests { availability_window_id, updated_at ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, '2026-04-20T09:00:00Z')", - params![ + crate::app_sqlite_params![ product_id.to_string(), farm_id.to_string(), product.title, @@ -4214,8 +4227,9 @@ mod tests { product_id } + #[expect(clippy::too_many_arguments)] fn insert_order( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, farm_id: FarmId, order_number: &str, @@ -4240,7 +4254,7 @@ mod tests { buyer_phone, buyer_order_note ) values (?1, ?2, null, ?3, 'Casey', ?4, '2026-04-20T10:00:00Z', ?5, ?6, ?7, ?8)", - params![ + crate::app_sqlite_params![ order_id.to_string(), farm_id.to_string(), order_number, @@ -4255,20 +4269,20 @@ mod tests { } fn set_order_workflow_revision( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, workflow_revision: &str, ) { connection .execute( "update orders set workflow_revision = ?1 where id = ?2", - params![workflow_revision, order_id.to_string()], + crate::app_sqlite_params![workflow_revision, order_id.to_string()], ) .expect("order workflow revision update should succeed"); } fn set_order_workflow_display_projection( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, agreement: &str, inventory: &str, @@ -4283,7 +4297,7 @@ mod tests { workflow_provenance_source = ?3, workflow_provenance_last_event_id = ?4 where id = ?5", - params![ + crate::app_sqlite_params![ agreement, inventory, provenance_source, @@ -4294,8 +4308,9 @@ mod tests { .expect("order workflow display projection update should succeed"); } + #[expect(clippy::too_many_arguments)] fn insert_order_line( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, product_id: ProductId, title: &str, @@ -4317,7 +4332,7 @@ mod tests { price_currency, sort_index ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 0)", - params![ + crate::app_sqlite_params![ format!("{order_id}:{product_id}"), order_id.to_string(), title, @@ -4332,7 +4347,7 @@ mod tests { } fn corrupt_order_workflow_revision( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, workflow_revision: &str, ) { @@ -4346,7 +4361,7 @@ mod tests { } fn corrupt_order_workflow_display_projection( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, column: &str, value: &str, @@ -4363,7 +4378,10 @@ mod tests { _ => panic!("unsupported workflow display projection column {column}"), }; connection - .execute(statement, params![value, order_id.to_string()]) + .execute( + statement, + crate::app_sqlite_params![value, order_id.to_string()], + ) .expect("order workflow display projection corruption should succeed"); connection .execute_batch("pragma ignore_check_constraints = off") @@ -4380,11 +4398,11 @@ mod tests { } } - fn row_count(connection: &Connection, table_name: &str) -> i64 { + fn row_count(connection: &AppSqliteDatabase, table_name: &str) -> i64 { let sql = format!("SELECT COUNT(*) FROM {table_name}"); connection - .query_row(&sql, [], |row| row.get(0)) + .query_row(&sql, crate::empty_params(), |row| row.try_get(0)) .expect("row count query should succeed") } } diff --git a/crates/store/src/repo/farm_rules.rs b/crates/store/src/repo/farm_rules.rs @@ -5,16 +5,18 @@ use radroots_studio_app_view::{ FarmReadinessBlocker, FarmRulesProjection, FarmRulesReadiness, FarmTimingConflict, FarmTimingConflictKind, FulfillmentWindowRecord, PickupLocationRecord, }; -use rusqlite::{Connection, OptionalExtension, params, params_from_iter}; +use sqlx::Row; + +use crate::{AppSqliteDatabase, OptionalSqliteResult}; use crate::AppSqliteError; pub struct AppFarmRulesRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppFarmRulesRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } @@ -123,10 +125,10 @@ impl<'a> AppFarmRulesRepository<'a> { [farm_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, )) }, ) @@ -166,12 +168,12 @@ impl<'a> AppFarmRulesRepository<'a> { let rows = statement .query_map([farm_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, Option<String>>(4)?, - row.get::<_, i64>(5)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<Option<String>, _>(4)?, + row.try_get::<i64, _>(5)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -209,9 +211,9 @@ impl<'a> AppFarmRulesRepository<'a> { [farm_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, i64>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<i64, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -265,13 +267,13 @@ impl<'a> AppFarmRulesRepository<'a> { let rows = statement .query_map([farm_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -330,11 +332,11 @@ impl<'a> AppFarmRulesRepository<'a> { let rows = statement .query_map([farm_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -387,7 +389,7 @@ impl<'a> AppFarmRulesRepository<'a> { timezone = excluded.timezone, currency_code = excluded.currency_code, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ farm_profile.farm_id.to_string(), farm_profile.display_name, farm_readiness_storage_key(ready), @@ -426,7 +428,7 @@ impl<'a> AppFarmRulesRepository<'a> { promise_lead_hours = excluded.promise_lead_hours, substitution_policy = excluded.substitution_policy, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ operating_rules.farm_id.to_string(), i64::from(operating_rules.promise_lead_hours), operating_rules.substitution_policy, @@ -486,7 +488,7 @@ impl<'a> AppFarmRulesRepository<'a> { directions = excluded.directions, is_default = excluded.is_default, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ pickup_location.pickup_location_id.to_string(), pickup_location.farm_id.to_string(), pickup_location.label, @@ -540,7 +542,7 @@ impl<'a> AppFarmRulesRepository<'a> { label = excluded.label, order_cutoff_at = excluded.order_cutoff_at, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ fulfillment_window.fulfillment_window_id.to_string(), fulfillment_window.farm_id.to_string(), fulfillment_window.starts_at, @@ -587,7 +589,7 @@ impl<'a> AppFarmRulesRepository<'a> { starts_at = excluded.starts_at, ends_at = excluded.ends_at, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ blackout_period.blackout_period_id.to_string(), blackout_period.farm_id.to_string(), blackout_period.label, @@ -760,10 +762,7 @@ fn derive_farm_rules_readiness_parts( blockers.push(FarmReadinessBlocker::MissingProfileBasics); } - if !pickup_locations - .iter() - .any(|pickup_location| pickup_location_is_present(pickup_location)) - { + if !pickup_locations.iter().any(pickup_location_is_present) { blockers.push(FarmReadinessBlocker::MissingPickupLocation); } @@ -837,7 +836,7 @@ fn pickup_location_is_present(pickup_location: &PickupLocationRecord) -> bool { } fn delete_missing_rows<T>( - connection: &Connection, + connection: &AppSqliteDatabase, table_name: &str, id_column: &str, farm_id: FarmId, @@ -866,19 +865,16 @@ where values.extend(keep_ids.iter().map(ToString::to_string)); connection - .execute(&sql, params_from_iter(values.iter())) + .execute(&sql, values) .map_err(|source| AppSqliteError::Query { operation, source })?; Ok(()) } -fn collect_rows<T, F>( +fn collect_rows<T>( operation: &'static str, - rows: rusqlite::MappedRows<'_, F>, -) -> Result<Vec<T>, AppSqliteError> -where - F: FnMut(&rusqlite::Row<'_>) -> rusqlite::Result<T>, -{ + rows: std::vec::IntoIter<Result<T, sqlx::Error>>, +) -> Result<Vec<T>, AppSqliteError> { let mut values = Vec::new(); for row in rows { @@ -947,7 +943,7 @@ mod tests { let repository = AppFarmRulesRepository::new(store.connection()); let projection = repository - .load_farm_rules(FarmId::new()) + .load_farm_rules(FarmId::generate()) .expect("missing farm rules should load"); assert_eq!(projection, FarmRulesProjection::default()); @@ -956,10 +952,10 @@ mod tests { #[test] fn save_farm_rules_round_trips_across_restart() { let path = temp_database_path("farm-rules-roundtrip"); - let farm_id = FarmId::new(); - let pickup_location_id = PickupLocationId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); - let blackout_period_id = BlackoutPeriodId::new(); + let farm_id = FarmId::generate(); + let pickup_location_id = PickupLocationId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let blackout_period_id = BlackoutPeriodId::generate(); let projection = FarmRulesProjection { farm_profile: Some(FarmProfileRecord { farm_id, @@ -1024,10 +1020,10 @@ mod tests { fn load_farm_rules_derives_missing_and_conflict_readiness() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let repository = AppFarmRulesRepository::new(store.connection()); - let farm_id = FarmId::new(); - let pickup_location_id = PickupLocationId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); - let blackout_period_id = BlackoutPeriodId::new(); + let farm_id = FarmId::generate(); + let pickup_location_id = PickupLocationId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let blackout_period_id = BlackoutPeriodId::generate(); repository .save_farm_rules(&FarmRulesProjection { @@ -1091,7 +1087,7 @@ mod tests { #[test] fn blank_pickup_location_rows_do_not_count_as_present_for_readiness() { - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); let readiness = derive_farm_rules_readiness(&FarmRulesProjection { farm_profile: Some(FarmProfileRecord { farm_id, @@ -1100,7 +1096,7 @@ mod tests { currency_code: "USD".to_owned(), }), pickup_locations: vec![PickupLocationRecord { - pickup_location_id: PickupLocationId::new(), + pickup_location_id: PickupLocationId::generate(), farm_id, label: " ".to_owned(), address_line: String::new(), @@ -1131,8 +1127,8 @@ mod tests { #[test] fn zero_promise_lead_hours_keep_operating_rules_incomplete() { - let farm_id = FarmId::new(); - let pickup_location_id = PickupLocationId::new(); + let farm_id = FarmId::generate(); + let pickup_location_id = PickupLocationId::generate(); let readiness = derive_farm_rules_readiness(&FarmRulesProjection { farm_profile: Some(FarmProfileRecord { farm_id, @@ -1154,7 +1150,7 @@ mod tests { substitution_policy: "ask_customer".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { - fulfillment_window_id: FulfillmentWindowId::new(), + fulfillment_window_id: FulfillmentWindowId::generate(), farm_id, pickup_location_id, label: "Friday pickup".to_owned(), @@ -1175,8 +1171,8 @@ mod tests { #[test] fn complete_pickup_location_row_counts_as_present_for_readiness() { - let farm_id = FarmId::new(); - let pickup_location_id = PickupLocationId::new(); + let farm_id = FarmId::generate(); + let pickup_location_id = PickupLocationId::generate(); let readiness = derive_farm_rules_readiness(&FarmRulesProjection { farm_profile: Some(FarmProfileRecord { farm_id, @@ -1198,7 +1194,7 @@ mod tests { substitution_policy: "ask_customer".to_owned(), }), fulfillment_windows: vec![FulfillmentWindowRecord { - fulfillment_window_id: FulfillmentWindowId::new(), + fulfillment_window_id: FulfillmentWindowId::generate(), farm_id, pickup_location_id, label: "Friday pickup".to_owned(), diff --git a/crates/store/src/repo/farm_setup.rs b/crates/store/src/repo/farm_setup.rs @@ -3,23 +3,23 @@ use std::collections::BTreeSet; use radroots_studio_app_view::{ FarmOrderMethod, FarmReadiness, FarmSetupDraft, FarmSetupProjection, FarmSummary, }; -use rusqlite::{Connection, OptionalExtension, params}; +use sqlx::Row; -use crate::AppSqliteError; +use crate::{AppSqliteDatabase, AppSqliteError}; pub struct AppFarmSetupRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppFarmSetupRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } pub fn load_farm_setup(&self, account_id: &str) -> Result<FarmSetupProjection, AppSqliteError> { let row = self .connection - .query_row( + .fetch_optional( "SELECT farm_name, location_or_service_area, @@ -32,21 +32,20 @@ impl<'a> AppFarmSetupRepository<'a> { FROM account_farm_setups WHERE account_id = ?1 LIMIT 1", - [account_id], + crate::app_sqlite_params![account_id], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, i64>(2)?, - row.get::<_, i64>(3)?, - row.get::<_, i64>(4)?, - row.get::<_, Option<String>>(5)?, - row.get::<_, Option<String>>(6)?, - row.get::<_, Option<String>>(7)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<i64, _>(2)?, + row.try_get::<i64, _>(3)?, + row.try_get::<i64, _>(4)?, + row.try_get::<Option<String>, _>(5)?, + row.try_get::<Option<String>, _>(6)?, + row.try_get::<Option<String>, _>(7)?, )) }, ) - .optional() .map_err(|source| AppSqliteError::Query { operation: "load account farm setup", source, @@ -96,7 +95,7 @@ impl<'a> AppFarmSetupRepository<'a> { } self.connection - .execute( + .execute_statement( "INSERT INTO account_farm_setups ( account_id, farm_name, @@ -119,7 +118,7 @@ impl<'a> AppFarmSetupRepository<'a> { saved_farm_display_name = excluded.saved_farm_display_name, saved_farm_readiness = excluded.saved_farm_readiness, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ account_id, projection.draft.farm_name, projection.draft.location_or_service_area, @@ -165,9 +164,9 @@ impl<'a> AppFarmSetupRepository<'a> { pub fn clear_farm_setup(&self, account_id: &str) -> Result<(), AppSqliteError> { self.connection - .execute( + .execute_statement( "DELETE FROM account_farm_setups WHERE account_id = ?1", - [account_id], + crate::app_sqlite_params![account_id], ) .map_err(|source| AppSqliteError::Query { operation: "clear account farm setup", @@ -286,7 +285,7 @@ mod tests { fn farm_setup_round_trips_saved_farm_state() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let saved_farm = FarmSummary { - farm_id: FarmId::new(), + farm_id: FarmId::generate(), display_name: "North field farm".to_owned(), readiness: FarmReadiness::Ready, }; diff --git a/crates/store/src/repo/order_detail.rs b/crates/store/src/repo/order_detail.rs @@ -3,7 +3,9 @@ use radroots_studio_app_view::{ TradeValidationReceiptProjection, TradeValidationReceiptProofSystem, TradeValidationReceiptResult, TradeValidationReceiptType, }; -use rusqlite::{Connection, params}; +use sqlx::Row; + +use crate::AppSqliteDatabase; use crate::AppSqliteError; @@ -86,7 +88,7 @@ pub(super) fn order_detail_economics( } pub(super) fn order_validation_receipts( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, ) -> Result<Vec<TradeValidationReceiptProjection>, AppSqliteError> { let mut statement = connection @@ -110,17 +112,17 @@ pub(super) fn order_validation_receipts( source, })?; let rows = statement - .query_map(params![order_id.to_string()], |row| { + .query_map(crate::app_sqlite_params![order_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, String>(7)?, - row.get::<_, i64>(8)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<String, _>(7)?, + row.try_get::<i64, _>(8)?, )) }) .map_err(|source| AppSqliteError::Query { diff --git a/crates/store/src/repo/orders.rs b/crates/store/src/repo/orders.rs @@ -9,7 +9,9 @@ use radroots_studio_app_view::{ PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, PackDayScreenQueryState, ProductId, TradeAgreementStatus, TradeWorkflowProjection, }; -use rusqlite::{Connection, OptionalExtension, params}; +use sqlx::Row; + +use crate::{AppSqliteDatabase, OptionalSqliteResult}; use super::{ order_detail::{order_detail_economics, order_detail_item_row, order_validation_receipts}, @@ -19,7 +21,7 @@ use super::{ use crate::AppSqliteError; pub struct AppOrdersRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -40,7 +42,7 @@ pub struct SellerOrderDecisionLineExport { } impl<'a> AppOrdersRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } @@ -90,22 +92,22 @@ impl<'a> AppOrdersRepository<'a> { left join pickup_locations pl on pl.id = fw.pickup_location_id where o.farm_id = ?1 and o.id = ?2 limit 1", - params![farm_id.to_string(), order_id.to_string()], + crate::app_sqlite_params![farm_id.to_string(), order_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, Option<String>>(5)?, - row.get::<_, String>(6)?, - row.get::<_, String>(7)?, - row.get::<_, String>(8)?, - row.get::<_, String>(9)?, - row.get::<_, Option<String>>(10)?, - row.get::<_, Option<String>>(11)?, - row.get::<_, Option<String>>(12)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<Option<String>, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<String, _>(7)?, + row.try_get::<String, _>(8)?, + row.try_get::<String, _>(9)?, + row.try_get::<Option<String>, _>(10)?, + row.try_get::<Option<String>, _>(11)?, + row.try_get::<Option<String>, _>(12)?, )) }, ) @@ -185,12 +187,12 @@ impl<'a> AppOrdersRepository<'a> { from orders where farm_id = ?1 and id = ?2 limit 1", - params![farm_id.to_string(), order_id.to_string()], + crate::app_sqlite_params![farm_id.to_string(), order_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -305,25 +307,25 @@ impl<'a> AppOrdersRepository<'a> { })?; let rows = statement .query_map( - params![ + crate::app_sqlite_params![ farm_id.to_string(), fulfillment_window_id.map(|id| id.to_string()) ], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, Option<String>>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, String>(7)?, - row.get::<_, String>(8)?, - row.get::<_, String>(9)?, - row.get::<_, Option<String>>(10)?, - row.get::<_, Option<String>>(11)?, - row.get::<_, Option<String>>(12)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<Option<String>, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<String, _>(7)?, + row.try_get::<String, _>(8)?, + row.try_get::<String, _>(9)?, + row.try_get::<Option<String>, _>(10)?, + row.try_get::<Option<String>, _>(11)?, + row.try_get::<Option<String>, _>(12)?, )) }, ) @@ -411,14 +413,14 @@ impl<'a> AppOrdersRepository<'a> { source, })?; let rows = statement - .query_map(params![order_id], |row| { + .query_map(crate::app_sqlite_params![order_id], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, i64>(2)?, - row.get::<_, String>(3)?, - row.get::<_, Option<u32>>(4)?, - row.get::<_, Option<String>>(5)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<i64, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<Option<u32>, _>(4)?, + row.try_get::<Option<String>, _>(5)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -469,11 +471,11 @@ impl<'a> AppOrdersRepository<'a> { source, })?; let rows = statement - .query_map(params![order_id.to_string()], |row| { + .query_map(crate::app_sqlite_params![order_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, i64>(1)?, - row.get::<_, Option<String>>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<i64, _>(1)?, + row.try_get::<Option<String>, _>(2)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -518,8 +520,8 @@ impl<'a> AppOrdersRepository<'a> { .connection .query_row( "select stock_count from products where id = ?1 limit 1", - params![product_id.to_string()], - |row| row.get::<_, Option<i64>>(0), + crate::app_sqlite_params![product_id.to_string()], + |row| row.try_get::<Option<i64>, _>(0), ) .optional() .map_err(|source| AppSqliteError::Query { @@ -556,9 +558,10 @@ impl<'a> AppOrdersRepository<'a> { source, })?; let rows = statement - .query_map(params![excluding_order_id.to_string()], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)) - }) + .query_map( + crate::app_sqlite_params![excluding_order_id.to_string()], + |row| Ok((row.try_get::<String, _>(0)?, row.try_get::<i64, _>(1)?)), + ) .map_err(|source| AppSqliteError::Query { operation: "query seller order decision reservations", source, @@ -602,12 +605,12 @@ impl<'a> AppOrdersRepository<'a> { from fulfillment_windows where farm_id = ?1 and id = ?2 limit 1", - params![farm_id.to_string(), fulfillment_window_id.to_string()], + crate::app_sqlite_params![farm_id.to_string(), fulfillment_window_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -641,12 +644,12 @@ impl<'a> AppOrdersRepository<'a> { where farm_id = ?1 and starts_at >= strftime('%Y-%m-%dT%H:%M:%SZ', 'now') order by starts_at asc, id asc limit 1", - params![farm_id.to_string()], + crate::app_sqlite_params![farm_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -682,12 +685,12 @@ impl<'a> AppOrdersRepository<'a> { and o.status in ('needs_action', 'scheduled', 'packed') order by fw.starts_at asc, fw.id asc limit 1", - params![farm_id.to_string()], + crate::app_sqlite_params![farm_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -729,15 +732,15 @@ impl<'a> AppOrdersRepository<'a> { left join pickup_locations pl on pl.id = fw.pickup_location_id where fw.farm_id = ?1 and fw.id = ?2 limit 1", - params![farm_id.to_string(), fulfillment_window_id.to_string()], + crate::app_sqlite_params![farm_id.to_string(), fulfillment_window_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, Option<String>>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<Option<String>, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, )) }, ) @@ -790,12 +793,12 @@ impl<'a> AppOrdersRepository<'a> { })?; let rows = statement .query_map( - params![farm_id.to_string(), fulfillment_window_id.to_string()], + crate::app_sqlite_params![farm_id.to_string(), fulfillment_window_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, u32>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<u32, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -847,14 +850,14 @@ impl<'a> AppOrdersRepository<'a> { })?; let rows = statement .query_map( - params![farm_id.to_string(), fulfillment_window_id.to_string()], + crate::app_sqlite_params![farm_id.to_string(), fulfillment_window_id.to_string()], |row| { Ok(PackDayPackListRow { - title: row.get(1)?, + title: row.try_get(1)?, quantity_display: format!( "{}: {}", - row.get::<_, String>(0)?, - row.get::<_, String>(2)? + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(2)? ), }) }, @@ -893,12 +896,12 @@ impl<'a> AppOrdersRepository<'a> { })?; let rows = statement .query_map( - params![farm_id.to_string(), fulfillment_window_id.to_string()], + crate::app_sqlite_params![farm_id.to_string(), fulfillment_window_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, u32>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<u32, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -957,16 +960,16 @@ impl<'a> AppOrdersRepository<'a> { })?; let rows = statement .query_map( - params![farm_id.to_string(), fulfillment_window_id.to_string()], + crate::app_sqlite_params![farm_id.to_string(), fulfillment_window_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, u32>(5)?, - row.get::<_, String>(6)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<u32, _>(5)?, + row.try_get::<String, _>(6)?, )) }, ) @@ -1023,12 +1026,12 @@ impl<'a> AppOrdersRepository<'a> { })?; let rows = statement .query_map( - params![farm_id.to_string(), fulfillment_window_id.to_string()], + crate::app_sqlite_params![farm_id.to_string(), fulfillment_window_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -1075,13 +1078,13 @@ impl<'a> AppOrdersRepository<'a> { })?; let rows = statement .query_map( - params![farm_id.to_string(), fulfillment_window_id.to_string()], + crate::app_sqlite_params![farm_id.to_string(), fulfillment_window_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, )) }, ) @@ -1273,24 +1276,22 @@ fn empty_string_to_none(value: Option<String>) -> Option<String> { #[cfg(test)] mod tests { + use crate::{AppSqliteDatabase, AppSqliteError, AppSqliteStore, DatabaseTarget}; use radroots_studio_app_view::{ FarmId, FulfillmentWindowId, OrderId, OrderPrimaryAction, OrderStatus, OrdersFilter, OrdersScreenQueryState, PackDayOutputOrderState, PackDayProductTotalRow, PackDayScreenQueryState, PickupLocationId, TradeAgreementStatus, TradeInventoryStatus, TradeRevisionStatus, TradeWorkflowSource, }; - use rusqlite::{Connection, params}; - - use crate::{AppSqliteError, AppSqliteStore, DatabaseTarget}; #[test] fn orders_list_loads_summary_rows_and_window_filter_truthfully() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); - let farm_id = FarmId::new(); - let other_farm_id = FarmId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); - let other_window_id = FulfillmentWindowId::new(); + let farm_id = FarmId::generate(); + let other_farm_id = FarmId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let other_window_id = FulfillmentWindowId::generate(); insert_farm( connection, @@ -1330,7 +1331,7 @@ mod tests { insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, Some(fulfillment_window_id), "R-100", @@ -1340,7 +1341,7 @@ mod tests { ); insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, Some(fulfillment_window_id), "R-101", @@ -1350,7 +1351,7 @@ mod tests { ); insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, Some(fulfillment_window_id), "R-102", @@ -1360,7 +1361,7 @@ mod tests { ); insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, Some(other_window_id), "R-103", @@ -1370,7 +1371,7 @@ mod tests { ); insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, None, "R-104", @@ -1380,7 +1381,7 @@ mod tests { ); insert_order( connection, - OrderId::new(), + OrderId::generate(), other_farm_id, Some(fulfillment_window_id), "R-999", @@ -1423,9 +1424,9 @@ mod tests { fn order_detail_loads_items_and_context() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); - let farm_id = FarmId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); - let order_id = OrderId::new(); + let farm_id = FarmId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let order_id = OrderId::generate(); insert_farm( connection, @@ -1502,8 +1503,8 @@ mod tests { fn seller_order_projections_fail_closed_for_invalid_workflow_revision() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); - let farm_id = FarmId::new(); - let order_id = OrderId::new(); + let farm_id = FarmId::generate(); + let order_id = OrderId::generate(); insert_farm( connection, @@ -1568,8 +1569,8 @@ mod tests { fn seller_order_projections_read_workflow_display_snapshot() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); - let farm_id = FarmId::new(); - let order_id = OrderId::new(); + let farm_id = FarmId::generate(); + let order_id = OrderId::generate(); insert_farm( connection, @@ -1649,8 +1650,8 @@ mod tests { fn seller_order_projections_fail_closed_for_invalid_workflow_snapshot_keys() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); - let farm_id = FarmId::new(); - let order_id = OrderId::new(); + let farm_id = FarmId::generate(); + let order_id = OrderId::generate(); insert_farm( connection, @@ -1718,11 +1719,11 @@ mod tests { fn pack_day_defaults_to_next_window_and_projects_totals_pack_list_and_roster() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); - let farm_id = FarmId::new(); - let next_window_id = FulfillmentWindowId::new(); - let later_window_id = FulfillmentWindowId::new(); - let scheduled_order_id = OrderId::new(); - let packed_order_id = OrderId::new(); + let farm_id = FarmId::generate(); + let next_window_id = FulfillmentWindowId::generate(); + let later_window_id = FulfillmentWindowId::generate(); + let scheduled_order_id = OrderId::generate(); + let packed_order_id = OrderId::generate(); insert_farm( connection, @@ -1774,7 +1775,7 @@ mod tests { ); insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, Some(next_window_id), "R-102", @@ -1784,7 +1785,7 @@ mod tests { ); insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, Some(later_window_id), "R-200", @@ -1862,10 +1863,10 @@ mod tests { fn pack_day_output_source_projects_canonical_records_without_screen_strings() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); - let farm_id = FarmId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); - let scheduled_order_id = OrderId::new(); - let packed_order_id = OrderId::new(); + let farm_id = FarmId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let scheduled_order_id = OrderId::generate(); + let packed_order_id = OrderId::generate(); insert_farm( connection, @@ -1907,7 +1908,7 @@ mod tests { ); insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, Some(fulfillment_window_id), "R-102", @@ -1996,8 +1997,8 @@ mod tests { fn orders_list_stays_aligned_with_today_needs_action_order_boundary() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); - let farm_id = FarmId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); + let farm_id = FarmId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); insert_farm( connection, @@ -2019,7 +2020,7 @@ mod tests { for index in 0..5 { insert_order( connection, - OrderId::new(), + OrderId::generate(), farm_id, Some(fulfillment_window_id), &format!("R-10{index}"), @@ -2059,7 +2060,7 @@ mod tests { } fn insert_farm( - connection: &Connection, + connection: &AppSqliteDatabase, farm_id: FarmId, display_name: &str, readiness: &str, @@ -2076,18 +2077,18 @@ mod tests { created_at, updated_at ) values (?1, ?2, ?3, 'UTC', 'USD', ?4, ?4)", - params![farm_id.to_string(), display_name, readiness, created_at], + crate::app_sqlite_params![farm_id.to_string(), display_name, readiness, created_at], ) .expect("farm insert should succeed"); } fn insert_pickup_location( - connection: &Connection, + connection: &AppSqliteDatabase, farm_id: FarmId, label: &str, is_default: bool, ) -> PickupLocationId { - let pickup_location_id = PickupLocationId::new(); + let pickup_location_id = PickupLocationId::generate(); connection .execute( @@ -2101,7 +2102,7 @@ mod tests { created_at, updated_at ) values (?1, ?2, ?3, '14 County Road', null, ?4, '2026-04-17T08:00:00Z', '2026-04-17T08:00:00Z')", - params![ + crate::app_sqlite_params![ pickup_location_id.to_string(), farm_id.to_string(), label, @@ -2113,8 +2114,9 @@ mod tests { pickup_location_id } + #[expect(clippy::too_many_arguments)] fn insert_window( - connection: &Connection, + connection: &AppSqliteDatabase, fulfillment_window_id: FulfillmentWindowId, farm_id: FarmId, pickup_location_id: Option<PickupLocationId>, @@ -2137,7 +2139,7 @@ mod tests { label, order_cutoff_at ) values (?1, ?2, ?3, ?4, null, ?3, ?3, ?5, ?6, ?7)", - params![ + crate::app_sqlite_params![ fulfillment_window_id.to_string(), farm_id.to_string(), starts_at, @@ -2150,8 +2152,9 @@ mod tests { .expect("fulfillment window insert should succeed"); } + #[expect(clippy::too_many_arguments)] fn insert_order( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, farm_id: FarmId, fulfillment_window_id: Option<FulfillmentWindowId>, @@ -2171,7 +2174,7 @@ mod tests { status, updated_at ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![ + crate::app_sqlite_params![ order_id.to_string(), farm_id.to_string(), fulfillment_window_id.map(|id| id.to_string()), @@ -2185,20 +2188,20 @@ mod tests { } fn set_order_workflow_revision( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, workflow_revision: &str, ) { connection .execute( "update orders set workflow_revision = ?1 where id = ?2", - params![workflow_revision, order_id.to_string()], + crate::app_sqlite_params![workflow_revision, order_id.to_string()], ) .expect("order workflow revision update should succeed"); } fn corrupt_order_workflow_revision( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, workflow_revision: &str, ) { @@ -2212,7 +2215,7 @@ mod tests { } fn set_order_workflow_display_projection( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, agreement: &str, inventory: &str, @@ -2227,7 +2230,7 @@ mod tests { workflow_provenance_source = ?3, workflow_provenance_last_event_id = ?4 where id = ?5", - params![ + crate::app_sqlite_params![ agreement, inventory, provenance_source, @@ -2239,7 +2242,7 @@ mod tests { } fn corrupt_order_workflow_display_projection( - connection: &Connection, + connection: &AppSqliteDatabase, order_id: OrderId, column: &str, value: &str, @@ -2256,7 +2259,10 @@ mod tests { _ => panic!("unsupported workflow display projection column {column}"), }; connection - .execute(statement, params![value, order_id.to_string()]) + .execute( + statement, + crate::app_sqlite_params![value, order_id.to_string()], + ) .expect("order workflow display projection corruption should succeed"); connection .execute_batch("pragma ignore_check_constraints = off") @@ -2273,8 +2279,9 @@ mod tests { } } + #[expect(clippy::too_many_arguments)] fn insert_order_line( - connection: &Connection, + connection: &AppSqliteDatabase, line_id: &str, order_id: OrderId, title: &str, @@ -2296,7 +2303,7 @@ mod tests { price_currency, sort_index ) values (?1, ?2, ?3, ?4, ?5, ?6, 650, 'USD', ?7)", - params![ + crate::app_sqlite_params![ line_id, order_id.to_string(), title, diff --git a/crates/store/src/repo/products.rs b/crates/store/src/repo/products.rs @@ -6,18 +6,20 @@ use radroots_studio_app_view::{ ProductPublishBlocker, ProductStatus, ProductStockState, ProductStockSummary, ProductsFilter, ProductsListProjection, ProductsListRow, ProductsListSummary, ProductsSort, }; -use rusqlite::{Connection, OptionalExtension, params}; +use sqlx::Row; + +use crate::{AppSqliteDatabase, OptionalSqliteResult}; use crate::AppSqliteError; const PRODUCTS_LOW_STOCK_THRESHOLD: u32 = 3; pub struct AppProductsRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppProductsRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } @@ -58,7 +60,7 @@ impl<'a> AppProductsRepository<'a> { } pub fn create_product_draft(&self, farm_id: FarmId) -> Result<ProductId, AppSqliteError> { - let product_id = ProductId::new(); + let product_id = ProductId::generate(); self.connection .execute( @@ -89,7 +91,7 @@ impl<'a> AppProductsRepository<'a> { null, strftime('%Y-%m-%dT%H:%M:%SZ', 'now') )", - params![product_id.to_string(), farm_id.to_string()], + crate::app_sqlite_params![product_id.to_string(), farm_id.to_string()], ) .map_err(|source| AppSqliteError::Query { operation: "create product draft", @@ -120,7 +122,7 @@ impl<'a> AppProductsRepository<'a> { availability_window_id = ?10, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') where id = ?1", - params![ + crate::app_sqlite_params![ product_id.to_string(), draft.title.as_str(), draft.subtitle.as_str(), @@ -154,7 +156,7 @@ impl<'a> AppProductsRepository<'a> { stock_count = ?2, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now') where id = ?1", - params![product_id.to_string(), stock_quantity], + crate::app_sqlite_params![product_id.to_string(), stock_quantity], ) .map_err(|source| AppSqliteError::Query { operation: "update product stock", @@ -175,9 +177,11 @@ impl<'a> AppProductsRepository<'a> { fn current_utc_timestamp(&self) -> Result<String, AppSqliteError> { self.connection - .query_row("select strftime('%Y-%m-%dT%H:%M:%SZ', 'now')", [], |row| { - row.get(0) - }) + .query_row( + "select strftime('%Y-%m-%dT%H:%M:%SZ', 'now')", + crate::empty_params(), + |row| row.try_get(0), + ) .map_err(|source| AppSqliteError::Query { operation: "load current utc timestamp", source, @@ -212,22 +216,22 @@ impl<'a> AppProductsRepository<'a> { source, })?; let rows = statement - .query_map(params![farm_id.to_string()], |row| { + .query_map(crate::app_sqlite_params![farm_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, Option<u32>>(7)?, - row.get::<_, String>(8)?, - row.get::<_, Option<u32>>(9)?, - row.get::<_, Option<String>>(10)?, - row.get::<_, Option<String>>(11)?, - row.get::<_, Option<String>>(12)?, - row.get::<_, String>(13)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<Option<u32>, _>(7)?, + row.try_get::<String, _>(8)?, + row.try_get::<Option<u32>, _>(9)?, + row.try_get::<Option<String>, _>(10)?, + row.try_get::<Option<String>, _>(11)?, + row.try_get::<Option<String>, _>(12)?, + row.try_get::<String, _>(13)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -307,23 +311,23 @@ impl<'a> AppProductsRepository<'a> { left join fulfillment_windows fw on fw.id = p.availability_window_id where p.id = ?1 limit 1", - params![product_id.to_string()], + crate::app_sqlite_params![product_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, Option<u32>>(7)?, - row.get::<_, String>(8)?, - row.get::<_, Option<u32>>(9)?, - row.get::<_, Option<String>>(10)?, - row.get::<_, Option<String>>(11)?, - row.get::<_, Option<String>>(12)?, - row.get::<_, String>(13)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<Option<u32>, _>(7)?, + row.try_get::<String, _>(8)?, + row.try_get::<Option<u32>, _>(9)?, + row.try_get::<Option<String>, _>(10)?, + row.try_get::<Option<String>, _>(11)?, + row.try_get::<Option<String>, _>(12)?, + row.try_get::<String, _>(13)?, )) }, ) @@ -728,14 +732,12 @@ fn parse_product_status( #[cfg(test)] mod tests { + use crate::{AppSqliteDatabase, AppSqliteStore, DatabaseTarget}; use radroots_studio_app_view::{ FarmId, FulfillmentWindowId, ProductAttentionState, ProductAvailabilityState, ProductEditorDraft, ProductId, ProductPublishBlocker, ProductStatus, ProductStockState, ProductsFilter, ProductsSort, }; - use rusqlite::{Connection, params}; - - use crate::{AppSqliteStore, DatabaseTarget}; use super::AppProductsRepository; @@ -984,14 +986,14 @@ mod tests { .expect("ready blockers should load"), Some(Vec::new()) ); - let stale_window_id = FulfillmentWindowId::new(); + let stale_window_id = FulfillmentWindowId::generate(); connection .execute_batch("PRAGMA foreign_keys = OFF;") .expect("foreign keys should disable for stale fixture"); connection .execute( "update products set availability_window_id = ?2 where id = ?1", - params![product_id.to_string(), stale_window_id.to_string()], + crate::app_sqlite_params![product_id.to_string(), stale_window_id.to_string()], ) .expect("stale availability id should write"); connection @@ -1009,7 +1011,7 @@ mod tests { connection .execute( "update products set availability_window_id = ?2 where id = ?1", - params![product_id.to_string(), window_id.to_string()], + crate::app_sqlite_params![product_id.to_string(), window_id.to_string()], ) .expect("ready availability id should restore"); connection @@ -1054,14 +1056,14 @@ mod tests { ); } - fn insert_farm(connection: &Connection, display_name: &str) -> FarmId { - let farm_id = FarmId::new(); + fn insert_farm(connection: &AppSqliteDatabase, display_name: &str) -> FarmId { + let farm_id = FarmId::generate(); connection .execute( "insert into farms (id, display_name, readiness, created_at, updated_at) values (?1, ?2, 'ready', '2026-04-18T08:00:00Z', '2026-04-18T08:00:00Z')", - params![farm_id.to_string(), display_name], + crate::app_sqlite_params![farm_id.to_string(), display_name], ) .expect("farm insert should succeed"); @@ -1069,18 +1071,18 @@ mod tests { } fn insert_window( - connection: &Connection, + connection: &AppSqliteDatabase, farm_id: FarmId, starts_at: &str, ends_at: &str, ) -> FulfillmentWindowId { - let window_id = FulfillmentWindowId::new(); + let window_id = FulfillmentWindowId::generate(); connection .execute( "insert into fulfillment_windows (id, farm_id, starts_at, ends_at, capacity_limit, created_at, updated_at) values (?1, ?2, ?3, ?4, null, ?3, ?3)", - params![window_id.to_string(), farm_id.to_string(), starts_at, ends_at], + crate::app_sqlite_params![window_id.to_string(), farm_id.to_string(), starts_at, ends_at], ) .expect("fulfillment window insert should succeed"); @@ -1088,11 +1090,11 @@ mod tests { } fn insert_product( - connection: &Connection, + connection: &AppSqliteDatabase, farm_id: FarmId, seed: SeedProduct<'_>, ) -> ProductId { - let product_id = ProductId::new(); + let product_id = ProductId::generate(); connection .execute( @@ -1110,7 +1112,7 @@ mod tests { availability_window_id, updated_at ) values (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, 'USD', ?9, ?10, ?11)", - params![ + crate::app_sqlite_params![ product_id.to_string(), farm_id.to_string(), seed.title, diff --git a/crates/store/src/repo/reminders.rs b/crates/store/src/repo/reminders.rs @@ -3,18 +3,20 @@ use radroots_studio_app_view::{ ReminderKind, ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, ReminderUrgency, }; -use rusqlite::{Connection, params}; +use sqlx::Row; + +use crate::AppSqliteDatabase; use std::str::FromStr; use uuid::Uuid; use crate::AppSqliteError; pub struct AppRemindersRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppRemindersRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } @@ -47,21 +49,24 @@ impl<'a> AppRemindersRepository<'a> { source, })?; let rows = statement - .query_map(params![account_id, farm_id.to_string()], |row| { - Ok(( - row.get::<_, String>(0)?, - row.get::<_, Option<String>>(1)?, - row.get::<_, Option<String>>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, String>(7)?, - row.get::<_, String>(8)?, - row.get::<_, Option<String>>(9)?, - row.get::<_, String>(10)?, - )) - }) + .query_map( + crate::app_sqlite_params![account_id, farm_id.to_string()], + |row| { + Ok(( + row.try_get::<String, _>(0)?, + row.try_get::<Option<String>, _>(1)?, + row.try_get::<Option<String>, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<String, _>(7)?, + row.try_get::<String, _>(8)?, + row.try_get::<Option<String>, _>(9)?, + row.try_get::<String, _>(10)?, + )) + }, + ) .map_err(|source| AppSqliteError::Query { operation: "query reminder schedule", source, @@ -125,26 +130,26 @@ impl<'a> AppRemindersRepository<'a> { projection: &ReminderFeedProjection, log_entries: &[ReminderLogEntryProjection], ) -> Result<(), AppSqliteError> { - let transaction = + self.connection + .execute_batch("BEGIN IMMEDIATE") + .map_err(|source| AppSqliteError::Query { + operation: "begin reminder schedule replacement", + source, + })?; + + let result = (|| -> Result<(), AppSqliteError> { self.connection - .unchecked_transaction() + .execute( + "DELETE FROM reminder_schedules WHERE account_id = ?1 AND farm_id = ?2", + crate::app_sqlite_params![account_id, farm_id.to_string()], + ) .map_err(|source| AppSqliteError::Query { - operation: "begin reminder schedule replacement", + operation: "clear reminder schedule", source, })?; - transaction - .execute( - "DELETE FROM reminder_schedules WHERE account_id = ?1 AND farm_id = ?2", - params![account_id, farm_id.to_string()], - ) - .map_err(|source| AppSqliteError::Query { - operation: "clear reminder schedule", - source, - })?; - - { - let mut statement = transaction + let mut statement = self + .connection .prepare( "INSERT INTO reminder_schedules ( reminder_id, @@ -169,7 +174,7 @@ impl<'a> AppRemindersRepository<'a> { for reminder in &projection.items { statement - .execute(params![ + .execute(crate::app_sqlite_params![ reminder.reminder_id.to_string(), account_id, reminder.farm_id.to_string(), @@ -191,14 +196,13 @@ impl<'a> AppRemindersRepository<'a> { source, })?; } - } - for entry in log_entries { - let log_entry_id = Uuid::now_v7().to_string(); + for entry in log_entries { + let log_entry_id = Uuid::now_v7().to_string(); - transaction - .execute( - "INSERT INTO reminder_log_entries ( + self.connection + .execute( + "INSERT INTO reminder_log_entries ( log_entry_id, account_id, farm_id, @@ -209,26 +213,34 @@ impl<'a> AppRemindersRepository<'a> { delivery_state, detail ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - params![ - log_entry_id, - account_id, - farm_id.to_string(), - entry.reminder_id.to_string(), - entry.kind.storage_key(), - entry.title, - entry.recorded_at, - entry.delivery_state.storage_key(), - entry.detail, - ], - ) - .map_err(|source| AppSqliteError::Query { - operation: "record reminder log entry", - source, - })?; + crate::app_sqlite_params![ + log_entry_id, + account_id, + farm_id.to_string(), + entry.reminder_id.to_string(), + entry.kind.storage_key(), + entry.title, + entry.recorded_at, + entry.delivery_state.storage_key(), + entry.detail, + ], + ) + .map_err(|source| AppSqliteError::Query { + operation: "record reminder log entry", + source, + })?; + } + + Ok(()) + })(); + + if let Err(error) = result { + let _ = self.connection.execute_batch("ROLLBACK"); + return Err(error); } - transaction - .commit() + self.connection + .execute_batch("COMMIT") .map_err(|source| AppSqliteError::Query { operation: "commit reminder schedule replacement", source, @@ -258,7 +270,7 @@ impl<'a> AppRemindersRepository<'a> { delivery_state, detail ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", - params![ + crate::app_sqlite_params![ log_entry_id, account_id, farm_id.to_string(), @@ -305,15 +317,15 @@ impl<'a> AppRemindersRepository<'a> { })?; let rows = statement .query_map( - params![account_id, farm_id.to_string(), limit as i64], + crate::app_sqlite_params![account_id, farm_id.to_string(), limit as i64], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, Option<String>>(5)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<Option<String>, _>(5)?, )) }, ) @@ -425,11 +437,11 @@ mod tests { fn reminder_schedule_round_trips_and_is_account_scoped() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let repository = AppRemindersRepository::new(store.connection()); - let farm_id = FarmId::new(); - let other_farm_id = FarmId::new(); - let order_id = OrderId::new(); + let farm_id = FarmId::generate(); + let other_farm_id = FarmId::generate(); + let order_id = OrderId::generate(); let reminder = ReminderDeadlineProjection { - reminder_id: ReminderId::new(), + reminder_id: ReminderId::generate(), farm_id, order_id: Some(order_id), fulfillment_window_id: None, @@ -482,9 +494,9 @@ mod tests { fn reminder_log_records_and_loads_recent_entries() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let repository = AppRemindersRepository::new(store.connection()); - let farm_id = FarmId::new(); - let first_reminder_id = ReminderId::new(); - let second_reminder_id = ReminderId::new(); + let farm_id = FarmId::generate(); + let first_reminder_id = ReminderId::generate(); + let second_reminder_id = ReminderId::generate(); repository .record_reminder_log_entry( diff --git a/crates/store/src/repo/today.rs b/crates/store/src/repo/today.rs @@ -3,7 +3,9 @@ use radroots_studio_app_view::{ ProductListRow, ProductStatus, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind, TodaySummary, }; -use rusqlite::{Connection, OptionalExtension, Params, params}; +use sqlx::Row; + +use crate::{AppSqliteDatabase, IntoAppSqliteParams, OptionalSqliteResult}; use crate::AppSqliteError; @@ -11,11 +13,11 @@ pub const TODAY_AGENDA_LIST_LIMIT: i64 = 4; pub const TODAY_AGENDA_LOW_STOCK_THRESHOLD: u32 = 3; pub struct AppTodayAgendaRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppTodayAgendaRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } @@ -51,7 +53,7 @@ impl<'a> AppTodayAgendaRepository<'a> { display_name = excluded.display_name, readiness = excluded.readiness, updated_at = excluded.updated_at", - params![ + crate::app_sqlite_params![ farm.farm_id.to_string(), farm.display_name, farm_readiness_storage_key(farm.readiness), @@ -73,12 +75,12 @@ impl<'a> AppTodayAgendaRepository<'a> { self.connection .query_row( "select id, display_name, readiness from farms where id = ?1 limit 1", - params![farm_id.to_string()], + crate::app_sqlite_params![farm_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -91,12 +93,12 @@ impl<'a> AppTodayAgendaRepository<'a> { self.connection .query_row( "select id, display_name, readiness from farms order by created_at asc, id asc limit 1", - [], + crate::empty_params(), |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -124,17 +126,17 @@ impl<'a> AppTodayAgendaRepository<'a> { orders_needing_action: self.count_u32( "count today orders needing action", "select count(*) from orders where farm_id = ?1 and status = 'needs_action'", - params![farm_id.to_string()], + crate::app_sqlite_params![farm_id.to_string()], )?, low_stock_products: self.count_u32( "count today low-stock products", "select count(*) from products where farm_id = ?1 and status = 'published' and stock_count <= ?2", - params![farm_id.to_string(), TODAY_AGENDA_LOW_STOCK_THRESHOLD], + crate::app_sqlite_params![farm_id.to_string(), TODAY_AGENDA_LOW_STOCK_THRESHOLD], )?, draft_products: self.count_u32( "count today draft products", "select count(*) from products where farm_id = ?1 and status = 'draft'", - params![farm_id.to_string()], + crate::app_sqlite_params![farm_id.to_string()], )?, reminders_due_soon: 0, }) @@ -159,13 +161,13 @@ impl<'a> AppTodayAgendaRepository<'a> { })?; let rows = statement .query_map( - params![farm_id.to_string(), TODAY_AGENDA_LIST_LIMIT], + crate::app_sqlite_params![farm_id.to_string(), TODAY_AGENDA_LIST_LIMIT], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, Option<String>>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, + row.try_get::<String, _>(0)?, + row.try_get::<Option<String>, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, )) }, ) @@ -217,16 +219,16 @@ impl<'a> AppTodayAgendaRepository<'a> { })?; let rows = statement .query_map( - params![ + crate::app_sqlite_params![ farm_id.to_string(), TODAY_AGENDA_LOW_STOCK_THRESHOLD, TODAY_AGENDA_LIST_LIMIT ], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, u32>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<u32, _>(2)?, )) }, ) @@ -270,12 +272,12 @@ impl<'a> AppTodayAgendaRepository<'a> { })?; let rows = statement .query_map( - params![farm_id.to_string(), TODAY_AGENDA_LIST_LIMIT], + crate::app_sqlite_params![farm_id.to_string(), TODAY_AGENDA_LIST_LIMIT], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, u32>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<u32, _>(2)?, )) }, ) @@ -314,12 +316,12 @@ impl<'a> AppTodayAgendaRepository<'a> { where farm_id = ?1 and starts_at >= strftime('%Y-%m-%dT%H:%M:%SZ', 'now') \ order by starts_at asc, id asc \ limit 1", - params![farm_id.to_string()], + crate::app_sqlite_params![farm_id.to_string()], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, )) }, ) @@ -356,7 +358,7 @@ impl<'a> AppTodayAgendaRepository<'a> { is_complete: self.exists( "check today fulfillment window setup", "select exists(select 1 from fulfillment_windows where farm_id = ?1)", - params![farm.farm_id.to_string()], + crate::app_sqlite_params![farm.farm_id.to_string()], )?, }, TodaySetupTask { @@ -364,31 +366,31 @@ impl<'a> AppTodayAgendaRepository<'a> { is_complete: self.exists( "check today published product setup", "select exists(select 1 from products where farm_id = ?1 and status = 'published')", - params![farm.farm_id.to_string()], + crate::app_sqlite_params![farm.farm_id.to_string()], )?, }, ]) } - fn count_u32<P: Params>( + fn count_u32<P: IntoAppSqliteParams>( &self, operation: &'static str, sql: &'static str, params: P, ) -> Result<u32, AppSqliteError> { self.connection - .query_row(sql, params, |row| row.get::<_, u32>(0)) + .query_row(sql, params, |row| row.try_get::<u32, _>(0)) .map_err(|source| AppSqliteError::Query { operation, source }) } - fn exists<P: Params>( + fn exists<P: IntoAppSqliteParams>( &self, operation: &'static str, sql: &'static str, params: P, ) -> Result<bool, AppSqliteError> { self.connection - .query_row(sql, params, |row| row.get::<_, i64>(0)) + .query_row(sql, params, |row| row.try_get::<i64, _>(0)) .map(|value| value == 1) .map_err(|source| AppSqliteError::Query { operation, source }) } @@ -433,10 +435,8 @@ fn farm_readiness_storage_key(readiness: FarmReadiness) -> &'static str { #[cfg(test)] mod tests { + use crate::{AppSqliteDatabase, AppSqliteStore, DatabaseTarget}; use radroots_studio_app_view::{FarmId, FulfillmentWindowId, ProductId, TodaySetupTaskKind}; - use rusqlite::{Connection, params}; - - use crate::{AppSqliteStore, DatabaseTarget}; use super::{TODAY_AGENDA_LIST_LIMIT, TODAY_AGENDA_LOW_STOCK_THRESHOLD}; @@ -458,10 +458,10 @@ mod tests { fn today_agenda_loads_truthful_projection_for_selected_farm() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); - let farm_id = FarmId::new(); - let other_farm_id = FarmId::new(); - let earliest_window_id = FulfillmentWindowId::new(); - let later_window_id = FulfillmentWindowId::new(); + let farm_id = FarmId::generate(); + let other_farm_id = FarmId::generate(); + let earliest_window_id = FulfillmentWindowId::generate(); + let later_window_id = FulfillmentWindowId::generate(); insert_farm( connection, @@ -493,7 +493,7 @@ mod tests { ); insert_window( connection, - FulfillmentWindowId::new(), + FulfillmentWindowId::generate(), other_farm_id, "2099-04-17T10:00:00Z", "2099-04-17T12:00:00Z", @@ -608,8 +608,8 @@ mod tests { fn today_agenda_uses_primary_farm_and_builds_setup_checklist_for_incomplete_farm() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); - let primary_farm_id = FarmId::new(); - let secondary_farm_id = FarmId::new(); + let primary_farm_id = FarmId::generate(); + let secondary_farm_id = FarmId::generate(); insert_farm( connection, @@ -643,7 +643,7 @@ mod tests { ); insert_window( connection, - FulfillmentWindowId::new(), + FulfillmentWindowId::generate(), secondary_farm_id, "2099-04-20T16:00:00Z", "2099-04-20T18:00:00Z", @@ -674,7 +674,7 @@ mod tests { fn saved_farm_summary_round_trips_into_today_projection() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let farm = radroots_studio_app_view::FarmSummary { - farm_id: FarmId::new(), + farm_id: FarmId::generate(), display_name: "North field farm".to_owned(), readiness: radroots_studio_app_view::FarmReadiness::Incomplete, }; @@ -696,7 +696,7 @@ mod tests { } fn insert_farm( - connection: &Connection, + connection: &AppSqliteDatabase, farm_id: FarmId, display_name: &str, readiness: &str, @@ -706,13 +706,13 @@ mod tests { .execute( "insert into farms (id, display_name, readiness, created_at, updated_at) \ values (?1, ?2, ?3, ?4, ?4)", - params![farm_id.to_string(), display_name, readiness, created_at], + crate::app_sqlite_params![farm_id.to_string(), display_name, readiness, created_at], ) .expect("farm insert should succeed"); } fn insert_window( - connection: &Connection, + connection: &AppSqliteDatabase, fulfillment_window_id: FulfillmentWindowId, farm_id: FarmId, starts_at: &str, @@ -722,7 +722,7 @@ mod tests { .execute( "insert into fulfillment_windows (id, farm_id, starts_at, ends_at, capacity_limit, created_at, updated_at) \ values (?1, ?2, ?3, ?4, null, ?3, ?3)", - params![ + crate::app_sqlite_params![ fulfillment_window_id.to_string(), farm_id.to_string(), starts_at, @@ -733,20 +733,20 @@ mod tests { } fn insert_product( - connection: &Connection, + connection: &AppSqliteDatabase, farm_id: FarmId, title: &str, status: &str, stock_count: u32, updated_at: &str, ) -> ProductId { - let product_id = ProductId::new(); + let product_id = ProductId::generate(); connection .execute( "insert into products (id, farm_id, title, status, stock_count, updated_at) \ values (?1, ?2, ?3, ?4, ?5, ?6)", - params![ + crate::app_sqlite_params![ product_id.to_string(), farm_id.to_string(), title, @@ -761,7 +761,7 @@ mod tests { } fn insert_order( - connection: &Connection, + connection: &AppSqliteDatabase, farm_id: FarmId, fulfillment_window_id: Option<FulfillmentWindowId>, order_number: &str, @@ -773,8 +773,8 @@ mod tests { .execute( "insert into orders (id, farm_id, fulfillment_window_id, order_number, customer_display_name, status, updated_at) \ values (?1, ?2, ?3, ?4, ?5, ?6, ?7)", - params![ - radroots_studio_app_view::OrderId::new().to_string(), + crate::app_sqlite_params![ + radroots_studio_app_view::OrderId::generate().to_string(), farm_id.to_string(), fulfillment_window_id.map(|id| id.to_string()), order_number, diff --git a/crates/store/src/sdk_workflow_receipts.rs b/crates/store/src/sdk_workflow_receipts.rs @@ -1,4 +1,6 @@ -use rusqlite::{Connection, OptionalExtension, params}; +use sqlx::Row; + +use crate::{AppSqliteDatabase, OptionalSqliteResult}; use serde_json::Value; use uuid::Uuid; @@ -111,11 +113,11 @@ pub struct AppSdkStoredWorkflowReceipt { } pub struct AppSdkWorkflowReceiptRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppSdkWorkflowReceiptRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } @@ -164,7 +166,7 @@ impl<'a> AppSdkWorkflowReceiptRepository<'a> { workflow_state = excluded.workflow_state, updated_at = excluded.updated_at, detail_json = excluded.detail_json", - params![ + crate::app_sqlite_params![ receipt_id, input.source_kind.storage_key(), input.source_record_id.as_str(), @@ -213,7 +215,7 @@ impl<'a> AppSdkWorkflowReceiptRepository<'a> { WHERE source_kind = ?1 AND source_record_id = ?2 LIMIT 1", - params![source_kind.storage_key(), source_record_id], + crate::app_sqlite_params![source_kind.storage_key(), source_record_id], decode_receipt_row, ) .optional() @@ -224,30 +226,32 @@ impl<'a> AppSdkWorkflowReceiptRepository<'a> { } } -fn decode_receipt_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<AppSdkStoredWorkflowReceipt> { - let source_kind: String = row.get(1)?; - let outbox_ids_json: String = row.get(4)?; - let workflow_state: String = row.get(8)?; - let detail_json: String = row.get(11)?; +fn decode_receipt_row( + row: &sqlx::sqlite::SqliteRow, +) -> Result<AppSdkStoredWorkflowReceipt, sqlx::Error> { + let source_kind: String = row.try_get(1)?; + let outbox_ids_json: String = row.try_get(4)?; + let workflow_state: String = row.try_get(8)?; + let detail_json: String = row.try_get(11)?; Ok(AppSdkStoredWorkflowReceipt { - id: row.get(0)?, + id: row.try_get(0)?, source_kind: AppSdkWorkflowReceiptSourceKind::parse(source_kind.as_str()) .map_err(decode_app_error)?, - source_record_id: row.get(2)?, - sdk_operation_kind: row.get(3)?, + source_record_id: row.try_get(2)?, + sdk_operation_kind: row.try_get(3)?, sdk_outbox_event_ids: serde_json::from_str(outbox_ids_json.as_str()).map_err(|source| { decode_app_error(AppSqliteError::DecodeJson { field: "app_sdk_workflow_receipts.sdk_outbox_event_ids_json", source, }) })?, - expected_event_id: row.get(5)?, - actor_pubkey: row.get(6)?, - idempotency_digest_prefix: row.get(7)?, + expected_event_id: row.try_get(5)?, + actor_pubkey: row.try_get(6)?, + idempotency_digest_prefix: row.try_get(7)?, workflow_state: AppSdkWorkflowReceiptState::parse(workflow_state.as_str()) .map_err(decode_app_error)?, - created_at: row.get(9)?, - updated_at: row.get(10)?, + created_at: row.try_get(9)?, + updated_at: row.try_get(10)?, detail_json: serde_json::from_str(detail_json.as_str()).map_err(|source| { decode_app_error(AppSqliteError::DecodeJson { field: "app_sdk_workflow_receipts.detail_json", @@ -257,8 +261,8 @@ fn decode_receipt_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<AppSdkStoredW }) } -fn decode_app_error(error: AppSqliteError) -> rusqlite::Error { - rusqlite::Error::FromSqlConversionFailure(0, rusqlite::types::Type::Text, Box::new(error)) +fn decode_app_error(error: AppSqliteError) -> sqlx::Error { + sqlx::Error::Decode(Box::new(error)) } #[cfg(test)] diff --git a/crates/store/src/source_guards.rs b/crates/store/src/source_guards.rs @@ -0,0 +1,170 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +const WORKSPACE_SQLX_DEPENDENCY: &str = r#"sqlx = { version = "0.9.0", default-features = false, features = ["derive", "sqlite-bundled"] }"#; +const WORKSPACE_LIBSQLITE3_PATCH: &str = + r#"libsqlite3-sys = { path = "../lib/crates/libsqlite3_sys_3_53_3" }"#; + +#[test] +fn app_sqlite_runtime_uses_sqlx_bundled_sqlite_only() { + let app_root = app_root(); + let workspace_manifest = read_source(app_root.join("Cargo.toml").as_path()); + + assert!( + workspace_manifest.contains(WORKSPACE_SQLX_DEPENDENCY), + "workspace SQLx dependency must stay pinned to SQLx 0.9.0 with sqlite-bundled" + ); + assert!( + workspace_manifest.contains(WORKSPACE_LIBSQLITE3_PATCH), + "workspace must keep the approved SQLite 3.53.3 libsqlite3-sys patch" + ); + + let findings = sqlite_runtime_drift_findings(app_root.as_path()); + assert!( + findings.is_empty(), + "app SQLite runtime drift findings:\n{}", + findings.join("\n") + ); +} + +fn sqlite_runtime_drift_findings(app_root: &Path) -> Vec<String> { + sqlite_guard_paths(app_root) + .into_iter() + .flat_map(|path| { + let source = read_source(path.as_path()); + let relative_path = path + .strip_prefix(app_root) + .expect("guard path should be app-relative") + .to_string_lossy() + .replace('\\', "/"); + forbidden_sqlite_findings(relative_path.as_str(), source.as_str()) + }) + .collect() +} + +fn forbidden_sqlite_findings(path: &str, source: &str) -> Vec<String> { + let mut findings = Vec::new(); + + for pattern in ["rusqlite", "SqliteExecutor"] { + for line in token_match_lines(source, pattern) { + findings.push(format!( + "{path}:{line} contains forbidden SQLite runtime token `{pattern}`" + )); + } + } + + for pattern in [ + "sqlite-wasm-rs", + "rsqlite-vfs", + "bundled-sqlcipher", + "features = [\"bundled\"]", + "features = [\"bundled-full\"]", + "features = [\"bundled-sqlcipher\"]", + ] { + for line in literal_match_lines(source, pattern) { + findings.push(format!( + "{path}:{line} contains forbidden SQLite runtime literal `{pattern}`" + )); + } + } + + let lowercase_source = source.to_lowercase(); + for line in literal_match_lines(lowercase_source.as_str(), "sqlcipher") { + findings.push(format!( + "{path}:{line} contains forbidden SQLCipher runtime literal" + )); + } + + findings +} + +fn sqlite_guard_paths(app_root: &Path) -> Vec<PathBuf> { + let mut paths = vec![app_root.join("Cargo.toml"), app_root.join("Cargo.lock")]; + collect_guard_paths(app_root.join("crates").as_path(), &mut paths); + paths.sort(); + paths +} + +fn collect_guard_paths(root: &Path, paths: &mut Vec<PathBuf>) { + for entry in fs::read_dir(root).unwrap_or_else(|error| { + panic!("failed to read guard directory {}: {error}", root.display()) + }) { + let entry = entry.unwrap_or_else(|error| { + panic!( + "failed to inspect guard directory {}: {error}", + root.display() + ) + }); + let path = entry.path(); + + if path.is_dir() { + if path.file_name().and_then(|name| name.to_str()) != Some("target") { + collect_guard_paths(path.as_path(), paths); + } + continue; + } + + if path.file_name().and_then(|name| name.to_str()) == Some("source_guards.rs") { + continue; + } + + if matches!( + path.extension().and_then(|extension| extension.to_str()), + Some("rs" | "toml" | "lock") + ) { + paths.push(path); + } + } +} + +fn token_match_lines(source: &str, pattern: &str) -> Vec<usize> { + source + .match_indices(pattern) + .filter_map(|(index, _)| { + let before = source[..index].chars().next_back(); + let after = source[index + pattern.len()..].chars().next(); + + if before.is_some_and(is_rust_identifier_character) + || after.is_some_and(is_rust_identifier_character) + { + None + } else { + Some(line_number(source, index)) + } + }) + .collect() +} + +fn literal_match_lines(source: &str, pattern: &str) -> Vec<usize> { + source + .match_indices(pattern) + .map(|(index, _)| line_number(source, index)) + .collect() +} + +fn read_source(path: &Path) -> String { + fs::read_to_string(path) + .unwrap_or_else(|error| panic!("failed to read source {}: {error}", path.display())) +} + +fn app_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .and_then(Path::parent) + .expect("store crate should live under app crates directory") + .to_path_buf() +} + +fn is_rust_identifier_character(character: char) -> bool { + character == '_' || character.is_ascii_alphanumeric() +} + +fn line_number(source: &str, index: usize) -> usize { + source[..index] + .bytes() + .filter(|byte| *byte == b'\n') + .count() + + 1 +} diff --git a/crates/store/src/sync.rs b/crates/store/src/sync.rs @@ -5,7 +5,9 @@ use radroots_studio_app_sync::{ SyncConflictResolutionStatus, SyncConflictSeverity, SyncOperationKind, }; use radroots_studio_app_view::{FarmId, FulfillmentWindowId, OrderId, ProductId}; -use rusqlite::{Connection, OptionalExtension, params}; +use sqlx::Row; + +use crate::{AppSqliteDatabase, OptionalSqliteResult}; use uuid::Uuid; use crate::AppSqliteError; @@ -28,12 +30,35 @@ pub struct StoredRelayIngestCursor { pub cursor_since_unix_seconds: Option<i64>, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AppRelayIngestSuccessInput<'a> { + pub scope_key: &'a str, + pub relay_url: &'a str, + pub cursor_since_unix_seconds: i64, + pub last_event_created_at_unix_seconds: Option<i64>, + pub started_at: &'a str, + pub started_unix_seconds: i64, + pub completed_at: &'a str, + pub completed_unix_seconds: i64, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AppRelayIngestFailureInput<'a> { + pub scope_key: &'a str, + pub relay_url: &'a str, + pub started_at: &'a str, + pub started_unix_seconds: i64, + pub completed_at: &'a str, + pub completed_unix_seconds: i64, + pub error_message: &'a str, +} + pub struct AppSyncRepository<'a> { - connection: &'a Connection, + connection: &'a AppSqliteDatabase, } impl<'a> AppSyncRepository<'a> { - pub const fn new(connection: &'a Connection) -> Self { + pub(crate) const fn new(connection: &'a AppSqliteDatabase) -> Self { Self { connection } } @@ -72,7 +97,7 @@ impl<'a> AppSyncRepository<'a> { attempt_count = 0, state = 'pending', last_error_message = NULL", - params![ + crate::app_sqlite_params![ operation_id, account_id, operation.operation_key.as_str(), @@ -100,8 +125,8 @@ impl<'a> AppSyncRepository<'a> { AND operation_key = ?2 AND state IN ('pending', 'in_progress', 'failed', 'blocked', 'retryable') LIMIT 1", - params![account_id, operation.operation_key.as_str()], - |row| row.get::<_, String>(0), + crate::app_sqlite_params![account_id, operation.operation_key.as_str()], + |row| row.try_get::<String, _>(0), ) .map_err(|source| AppSqliteError::Query { operation: "load pending sync operation id after enqueue", @@ -140,17 +165,17 @@ impl<'a> AppSyncRepository<'a> { let rows = statement .query_map([account_id], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, String>(7)?, - row.get::<_, u32>(8)?, - row.get::<_, String>(9)?, - row.get::<_, Option<String>>(10)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<String, _>(7)?, + row.try_get::<u32, _>(8)?, + row.try_get::<String, _>(9)?, + row.try_get::<Option<String>, _>(10)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -216,7 +241,7 @@ impl<'a> AppSyncRepository<'a> { state = 'retryable', last_error_message = ?5 WHERE account_id = ?1 AND id = ?2", - params![ + crate::app_sqlite_params![ account_id, operation_id, available_at, @@ -241,7 +266,7 @@ impl<'a> AppSyncRepository<'a> { .connection .execute( "DELETE FROM local_outbox WHERE account_id = ?1 AND id = ?2", - params![account_id, operation_id], + crate::app_sqlite_params![account_id, operation_id], ) .map_err(|source| AppSqliteError::Query { operation: "dequeue pending sync operation", @@ -270,11 +295,11 @@ impl<'a> AppSyncRepository<'a> { [account_id], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, Option<String>>(1)?, - row.get::<_, Option<String>>(2)?, - row.get::<_, Option<String>>(3)?, - row.get::<_, Option<String>>(4)?, + row.try_get::<String, _>(0)?, + row.try_get::<Option<String>, _>(1)?, + row.try_get::<Option<String>, _>(2)?, + row.try_get::<Option<String>, _>(3)?, + row.try_get::<Option<String>, _>(4)?, )) }, ) @@ -325,7 +350,7 @@ impl<'a> AppSyncRepository<'a> { last_sync_completed_at = excluded.last_sync_completed_at, last_remote_cursor = excluded.last_remote_cursor, last_error_message = excluded.last_error_message", - params![ + crate::app_sqlite_params![ account_id, sync_checkpoint_state_value(checkpoint.state), checkpoint.last_sync_started_at, @@ -357,8 +382,8 @@ impl<'a> AppSyncRepository<'a> { FROM app_relay_ingest_freshness WHERE scope_key = ?1 AND relay_url = ?2 LIMIT 1", - params![scope_key, relay_url.as_str()], - |row| row.get::<_, Option<i64>>(0), + crate::app_sqlite_params![scope_key, relay_url.as_str()], + |row| row.try_get::<Option<i64>, _>(0), ) .optional() .map_err(|source| AppSqliteError::Query { @@ -404,14 +429,7 @@ impl<'a> AppSyncRepository<'a> { pub fn record_relay_ingest_success( &self, - scope_key: &str, - relay_url: &str, - cursor_since_unix_seconds: i64, - last_event_created_at_unix_seconds: Option<i64>, - started_at: &str, - started_unix_seconds: i64, - completed_at: &str, - completed_unix_seconds: i64, + input: AppRelayIngestSuccessInput<'_>, ) -> Result<(), AppSqliteError> { self.connection .execute( @@ -442,15 +460,15 @@ impl<'a> AppSyncRepository<'a> { last_success_unix_seconds = excluded.last_success_unix_seconds, last_error_message = NULL, updated_at = excluded.updated_at", - params![ - scope_key, - relay_url, - cursor_since_unix_seconds, - last_event_created_at_unix_seconds, - started_at, - started_unix_seconds, - completed_at, - completed_unix_seconds, + crate::app_sqlite_params![ + input.scope_key, + input.relay_url, + input.cursor_since_unix_seconds, + input.last_event_created_at_unix_seconds, + input.started_at, + input.started_unix_seconds, + input.completed_at, + input.completed_unix_seconds, ], ) .map_err(|source| AppSqliteError::Query { @@ -463,13 +481,7 @@ impl<'a> AppSyncRepository<'a> { pub fn record_relay_ingest_failure( &self, - scope_key: &str, - relay_url: &str, - started_at: &str, - started_unix_seconds: i64, - completed_at: &str, - completed_unix_seconds: i64, - error_message: &str, + input: AppRelayIngestFailureInput<'_>, ) -> Result<(), AppSqliteError> { self.connection .execute( @@ -496,14 +508,14 @@ impl<'a> AppSyncRepository<'a> { last_fetch_completed_unix_seconds = excluded.last_fetch_completed_unix_seconds, last_error_message = excluded.last_error_message, updated_at = excluded.updated_at", - params![ - scope_key, - relay_url, - started_at, - started_unix_seconds, - completed_at, - completed_unix_seconds, - error_message, + crate::app_sqlite_params![ + input.scope_key, + input.relay_url, + input.started_at, + input.started_unix_seconds, + input.completed_at, + input.completed_unix_seconds, + input.error_message, ], ) .map_err(|source| AppSqliteError::Query { @@ -536,17 +548,17 @@ impl<'a> AppSyncRepository<'a> { FROM app_relay_ingest_freshness WHERE scope_key = ?1 AND relay_url = ?2 LIMIT 1", - params![scope_key, relay_url], + crate::app_sqlite_params![scope_key, relay_url], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, Option<i64>>(1)?, - row.get::<_, Option<i64>>(2)?, - row.get::<_, Option<String>>(3)?, - row.get::<_, Option<String>>(4)?, - row.get::<_, Option<i64>>(5)?, - row.get::<_, Option<String>>(6)?, - row.get::<_, Option<String>>(7)?, + row.try_get::<String, _>(0)?, + row.try_get::<Option<i64>, _>(1)?, + row.try_get::<Option<i64>, _>(2)?, + row.try_get::<Option<String>, _>(3)?, + row.try_get::<Option<String>, _>(4)?, + row.try_get::<Option<i64>, _>(5)?, + row.try_get::<Option<String>, _>(6)?, + row.try_get::<Option<String>, _>(7)?, )) }, ) @@ -625,7 +637,7 @@ impl<'a> AppSyncRepository<'a> { detected_at, resolved_at ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", - params![ + crate::app_sqlite_params![ conflict_id, account_id, conflict.aggregate.aggregate_kind(), @@ -698,16 +710,16 @@ impl<'a> AppSyncRepository<'a> { let rows = statement .query_map([account_id], |row| { Ok(( - row.get::<_, String>(0)?, - row.get::<_, String>(1)?, - row.get::<_, String>(2)?, - row.get::<_, String>(3)?, - row.get::<_, String>(4)?, - row.get::<_, String>(5)?, - row.get::<_, String>(6)?, - row.get::<_, Option<String>>(7)?, - row.get::<_, String>(8)?, - row.get::<_, Option<String>>(9)?, + row.try_get::<String, _>(0)?, + row.try_get::<String, _>(1)?, + row.try_get::<String, _>(2)?, + row.try_get::<String, _>(3)?, + row.try_get::<String, _>(4)?, + row.try_get::<String, _>(5)?, + row.try_get::<String, _>(6)?, + row.try_get::<Option<String>, _>(7)?, + row.try_get::<String, _>(8)?, + row.try_get::<Option<String>, _>(9)?, )) }) .map_err(|source| AppSqliteError::Query { @@ -773,7 +785,7 @@ impl<'a> AppSyncRepository<'a> { "UPDATE local_conflicts SET resolution_status = ?3, resolved_at = ?4 WHERE account_id = ?1 AND id = ?2", - params![ + crate::app_sqlite_params![ account_id, conflict_id, sync_conflict_resolution_status_value(resolution), @@ -1008,7 +1020,9 @@ mod tests { }; use radroots_studio_app_view::{FarmId, ProductId}; - use crate::{AppSqliteStore, DatabaseTarget}; + use crate::{ + AppRelayIngestFailureInput, AppRelayIngestSuccessInput, AppSqliteStore, DatabaseTarget, + }; #[test] fn checkpoints_are_selected_account_scoped() { @@ -1064,27 +1078,27 @@ mod tests { ); repository - .record_relay_ingest_success( - "direct_relay_ingest", - "wss://relay-a.example", - 1_010, - Some(1_009), - "2026-05-25T20:00:00Z", - 1_000, - "2026-05-25T20:00:02Z", - 1_002, - ) + .record_relay_ingest_success(AppRelayIngestSuccessInput { + scope_key: "direct_relay_ingest", + relay_url: "wss://relay-a.example", + cursor_since_unix_seconds: 1_010, + last_event_created_at_unix_seconds: Some(1_009), + started_at: "2026-05-25T20:00:00Z", + started_unix_seconds: 1_000, + completed_at: "2026-05-25T20:00:02Z", + completed_unix_seconds: 1_002, + }) .expect("success should record"); repository - .record_relay_ingest_failure( - "direct_relay_ingest", - "wss://relay-b.example", - "2026-05-25T20:00:00Z", - 1_000, - "2026-05-25T20:00:02Z", - 1_002, - "relay timeout", - ) + .record_relay_ingest_failure(AppRelayIngestFailureInput { + scope_key: "direct_relay_ingest", + relay_url: "wss://relay-b.example", + started_at: "2026-05-25T20:00:00Z", + started_unix_seconds: 1_000, + completed_at: "2026-05-25T20:00:02Z", + completed_unix_seconds: 1_002, + error_message: "relay timeout", + }) .expect("failure should record"); let cursors = repository @@ -1125,13 +1139,13 @@ mod tests { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let repository = store.sync_repository(); let first = PendingSyncOperation::new( - SyncAggregateRef::Farm(FarmId::new()), + SyncAggregateRef::Farm(FarmId::generate()), SyncOperationKind::Upsert, "{\"farm\":\"a\"}", "2026-04-20T18:00:00Z", ); let second = PendingSyncOperation::new( - SyncAggregateRef::Product(ProductId::new()), + SyncAggregateRef::Product(ProductId::generate()), SyncOperationKind::Delete, "{\"product\":\"b\"}", "2026-04-20T18:05:00Z", @@ -1211,7 +1225,7 @@ mod tests { fn outbox_enqueue_upserts_active_operation_by_deterministic_key() { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let repository = store.sync_repository(); - let product_id = ProductId::new(); + let product_id = ProductId::generate(); let first = PendingSyncOperation::new( SyncAggregateRef::Product(product_id), SyncOperationKind::Upsert, @@ -1260,7 +1274,7 @@ mod tests { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let repository = store.sync_repository(); let first = SyncConflict { - aggregate: SyncAggregateRef::Farm(FarmId::new()), + aggregate: SyncAggregateRef::Farm(FarmId::generate()), kind: SyncConflictKind::RevisionMismatch, severity: SyncConflictSeverity::Blocking, resolution: SyncConflictResolutionStatus::Unresolved, @@ -1270,7 +1284,7 @@ mod tests { resolved_at: None, }; let second = SyncConflict { - aggregate: SyncAggregateRef::Product(ProductId::new()), + aggregate: SyncAggregateRef::Product(ProductId::generate()), kind: SyncConflictKind::RemoteValidationReject, severity: SyncConflictSeverity::ReviewRequired, resolution: SyncConflictResolutionStatus::Unresolved, @@ -1334,7 +1348,7 @@ mod tests { let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let repository = store.sync_repository(); let first = SyncConflict { - aggregate: SyncAggregateRef::Farm(FarmId::new()), + aggregate: SyncAggregateRef::Farm(FarmId::generate()), kind: SyncConflictKind::RevisionMismatch, severity: SyncConflictSeverity::Blocking, resolution: SyncConflictResolutionStatus::Unresolved, @@ -1344,7 +1358,7 @@ mod tests { resolved_at: None, }; let second = SyncConflict { - aggregate: SyncAggregateRef::Product(ProductId::new()), + aggregate: SyncAggregateRef::Product(ProductId::generate()), kind: SyncConflictKind::RemoteValidationReject, severity: SyncConflictSeverity::ReviewRequired, resolution: SyncConflictResolutionStatus::Unresolved, diff --git a/crates/sync/src/lib.rs b/crates/sync/src/lib.rs @@ -586,7 +586,7 @@ mod tests { fn conflict_status_counts_only_unresolved_conflicts() { let conflicts = vec![ SyncConflict { - aggregate: SyncAggregateRef::Product(ProductId::new()), + aggregate: SyncAggregateRef::Product(ProductId::generate()), kind: SyncConflictKind::RevisionMismatch, severity: SyncConflictSeverity::Blocking, resolution: SyncConflictResolutionStatus::Unresolved, @@ -596,7 +596,7 @@ mod tests { resolved_at: None, }, SyncConflict { - aggregate: SyncAggregateRef::Farm(FarmId::new()), + aggregate: SyncAggregateRef::Farm(FarmId::generate()), kind: SyncConflictKind::RemoteValidationReject, severity: SyncConflictSeverity::ReviewRequired, resolution: SyncConflictResolutionStatus::AcceptedRemote, @@ -618,14 +618,14 @@ mod tests { #[test] fn request_and_result_surface_conflict_status_through_typed_contracts() { let mut pending_operation = PendingSyncOperation::new( - SyncAggregateRef::Product(ProductId::new()), + SyncAggregateRef::Product(ProductId::generate()), SyncOperationKind::Upsert, "{\"title\":\"greens\"}", "2026-04-17T19:32:00Z", ); pending_operation.attempt_count = 1; let conflict = SyncConflict { - aggregate: SyncAggregateRef::Product(ProductId::new()), + aggregate: SyncAggregateRef::Product(ProductId::generate()), kind: SyncConflictKind::RevisionMismatch, severity: SyncConflictSeverity::ReviewRequired, resolution: SyncConflictResolutionStatus::Unresolved, diff --git a/crates/sync/src/publish.rs b/crates/sync/src/publish.rs @@ -681,7 +681,7 @@ mod tests { #[test] fn publish_payload_serializes_with_stable_kind_and_sdk_target() { - let farm_id = FarmId::new(); + let farm_id = FarmId::generate(); let payload = AppPublishPayload::FarmProfile(AppFarmProfilePublishPayload { context: AppPublishContext::new("acct_local", "farm_setup") .with_source_local_event_id("local-event-1"), @@ -739,7 +739,7 @@ mod tests { fn listing_publish_payload_reports_stable_validation_reason_codes() { let payload = AppPublishPayload::Listing(AppListingPublishPayload { context: AppPublishContext::new("", ""), - product_id: ProductId::new(), + product_id: ProductId::generate(), listing_d_tag: None, farm_id: None, farm_pubkey: None, @@ -789,8 +789,8 @@ mod tests { fn order_request_publish_payload_requires_sdk_publish_inputs() { let payload = AppPublishPayload::OrderRequest(AppOrderRequestPublishPayload { context: AppPublishContext::new("acct_buyer", "place_personal_order"), - order_id: OrderId::new(), - farm_id: FarmId::new(), + order_id: OrderId::generate(), + farm_id: FarmId::generate(), status: Some("needs_action".to_owned()), order_document_json: None, listing_addr: Some(String::new()), @@ -799,7 +799,7 @@ mod tests { buyer_pubkey: None, seller_pubkey: Some(" ".to_owned()), items: vec![AppOrderRequestItemPayload { - product_id: ProductId::new(), + product_id: ProductId::generate(), quantity: 0, }], currency_code: None, @@ -834,8 +834,8 @@ mod tests { fn order_decision_publish_payload_reports_stable_validation_reason_codes() { let payload = AppPublishPayload::OrderDecision(AppOrderDecisionPublishPayload { context: AppPublishContext::new("", ""), - app_order_id: OrderId::new(), - farm_id: FarmId::new(), + app_order_id: OrderId::generate(), + farm_id: FarmId::generate(), trade_order_id: " ".to_owned(), request_event_id: String::new(), listing_event_id: None, @@ -876,8 +876,8 @@ mod tests { #[test] fn cancellation_publish_payload_reports_stable_validation_reason_codes() { - let order_id = OrderId::new(); - let farm_id = FarmId::new(); + let order_id = OrderId::generate(); + let farm_id = FarmId::generate(); let cancellation = AppPublishPayload::OrderCancellation(AppOrderCancellationPublishPayload { context: AppPublishContext::new("", ""), @@ -956,8 +956,8 @@ mod tests { #[test] fn order_revision_publish_payloads_report_stable_validation_reason_codes() { - let order_id = OrderId::new(); - let farm_id = FarmId::new(); + let order_id = OrderId::generate(); + let farm_id = FarmId::generate(); let economics = revision_economics(); let valid_proposal = AppPublishPayload::OrderRevisionProposal(AppOrderRevisionProposalPublishPayload { @@ -1067,7 +1067,7 @@ mod tests { fn existing_raw_payload_outbox_work_rejects_publish_payload() { let pending_operation = PendingSyncOperation { operation_key: "product:greens:upsert".to_owned(), - aggregate: SyncAggregateRef::Product(ProductId::new()), + aggregate: SyncAggregateRef::Product(ProductId::generate()), operation: SyncOperationKind::Upsert, payload_json: "{\"title\":\"greens\"}".to_owned(), created_at: "2026-04-17T19:32:00Z".to_owned(), diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs @@ -54,7 +54,7 @@ macro_rules! typed_id { pub struct $name(Uuid); impl $name { - pub fn new() -> Self { + pub fn generate() -> Self { Self(Uuid::now_v7()) } diff --git a/crates/view/src/lib.rs b/crates/view/src/lib.rs @@ -2292,7 +2292,7 @@ mod tests { custody: AccountCustody::LocalManaged, }, SelectedSurfaceProjection::new(ActiveSurface::Farmer), - FarmerActivationProjection::active(FarmId::new()), + FarmerActivationProjection::active(FarmId::generate()), ); let activation = AccountSurfaceActivationProjection::from(&selected_account); let restored = SelectedAccountProjection::from_surface_activation( @@ -2315,7 +2315,7 @@ mod tests { custody: AccountCustody::LocalManaged, }, SelectedSurfaceProjection::new(ActiveSurface::Farmer), - FarmerActivationProjection::active(FarmId::new()), + FarmerActivationProjection::active(FarmId::generate()), ), ); let personal_identity = AppIdentityProjection::ready( @@ -2419,7 +2419,7 @@ mod tests { custody: AccountCustody::LocalManaged, }, SelectedSurfaceProjection::new(ActiveSurface::Farmer), - FarmerActivationProjection::active(FarmId::new()), + FarmerActivationProjection::active(FarmId::generate()), ); let signed_in_identity = AppIdentityProjection::ready(Vec::new(), selected_account.clone()); let blocked_identity = AppIdentityProjection::blocked_with_selection( @@ -2663,8 +2663,8 @@ mod tests { #[test] fn product_attention_stock_and_projection_states_are_explicit() { let row = ProductsListRow { - product_id: super::ProductId::new(), - farm_id: FarmId::new(), + product_id: super::ProductId::generate(), + farm_id: FarmId::generate(), title: "Pea shoots".to_owned(), subtitle: Some("Tray-grown".to_owned()), status: ProductStatus::Draft, @@ -2723,7 +2723,7 @@ mod tests { price_minor_units: Some(450), price_currency: "USD".to_owned(), stock_quantity: Some(12), - availability_window_id: Some(super::FulfillmentWindowId::new()), + availability_window_id: Some(super::FulfillmentWindowId::generate()), status: ProductStatus::Draft, }; @@ -2934,7 +2934,7 @@ mod tests { "storage keys must parse exactly" ); - let order_id = OrderId::new(); + let order_id = OrderId::generate(); let active_order = test_active_order_projection(RadrootsTradeWorkflowState::Committed); let projection = TradeWorkflowProjection::from_active_order_projection( order_id, @@ -3356,9 +3356,9 @@ mod tests { #[test] fn pack_day_output_source_keeps_export_truth_out_of_ui_display_strings() { - let farm_id = FarmId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); - let order_id = OrderId::new(); + let farm_id = FarmId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); + let order_id = OrderId::generate(); let screen_row = PackDayPackListRow { title: "Salad mix".to_owned(), quantity_display: "Casey: 2 bags".to_owned(), @@ -3405,10 +3405,10 @@ mod tests { #[test] fn pack_day_export_bundle_tracks_output_directory_and_artifacts() { - let fulfillment_window_id = FulfillmentWindowId::new(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let bundle = PackDayExportBundle { fulfillment_window_id, - export_instance_id: PackDayExportInstanceId::new(), + export_instance_id: PackDayExportInstanceId::generate(), generated_at_utc: "2026-04-23T15:00:00Z".to_owned(), bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(), artifacts: vec![ @@ -3432,9 +3432,9 @@ mod tests { #[test] fn orders_and_pack_day_projections_hold_truthful_execution_data() { - let fulfillment_window_id = super::FulfillmentWindowId::new(); - let farm_id = FarmId::new(); - let order_id = super::OrderId::new(); + let fulfillment_window_id = super::FulfillmentWindowId::generate(); + let farm_id = FarmId::generate(); + let order_id = super::OrderId::generate(); let order_economics = TradeEconomicsProjection { subtotal_minor_units: Some(1300), total_minor_units: Some(1300), @@ -3530,9 +3530,9 @@ mod tests { #[test] fn buyer_marketplace_projections_hold_guest_capable_contract_data() { - let farm_id = FarmId::new(); - let product_id = super::ProductId::new(); - let order_id = super::OrderId::new(); + let farm_id = FarmId::generate(); + let product_id = super::ProductId::generate(); + let order_id = super::OrderId::generate(); let buyer_order_economics = TradeEconomicsProjection { subtotal_minor_units: Some(1300), total_minor_units: Some(1300), @@ -3664,19 +3664,19 @@ mod tests { fn today_agenda_stays_on_the_compact_order_row_contract() { let today = TodayAgendaProjection { orders_needing_action: vec![OrderListRow { - order_id: super::OrderId::new(), - farm_id: FarmId::new(), - fulfillment_window_id: Some(super::FulfillmentWindowId::new()), + order_id: super::OrderId::generate(), + farm_id: FarmId::generate(), + fulfillment_window_id: Some(super::FulfillmentWindowId::generate()), order_number: "R-1002".to_owned(), customer_display_name: "Morgan".to_owned(), status: OrderStatus::NeedsAction, }], ..TodayAgendaProjection::default() }; - let orders_row_id = super::OrderId::new(); + let orders_row_id = super::OrderId::generate(); let orders_row = OrdersListRow { order_id: orders_row_id, - farm_id: FarmId::new(), + farm_id: FarmId::generate(), fulfillment_window_id: None, order_number: "R-2002".to_owned(), customer_display_name: "Robin".to_owned(), @@ -3702,14 +3702,14 @@ mod tests { #[test] fn today_summary_attention_state_is_explicit() { let quiet = TodaySummary { - farm_id: FarmId::new(), + farm_id: FarmId::generate(), orders_needing_action: 0, low_stock_products: 0, draft_products: 0, reminders_due_soon: 0, }; let busy = TodaySummary { - farm_id: FarmId::new(), + farm_id: FarmId::generate(), orders_needing_action: 1, low_stock_products: 0, draft_products: 0, @@ -3722,11 +3722,11 @@ mod tests { #[test] fn reminder_and_repeat_demand_contracts_are_explicit() { - let farm_id = FarmId::new(); - let order_id = OrderId::new(); - let fulfillment_window_id = FulfillmentWindowId::new(); + let farm_id = FarmId::generate(); + let order_id = OrderId::generate(); + let fulfillment_window_id = FulfillmentWindowId::generate(); let reminder = ReminderDeadlineProjection { - reminder_id: ReminderId::new(), + reminder_id: ReminderId::generate(), farm_id, order_id: Some(order_id), fulfillment_window_id: Some(fulfillment_window_id), @@ -3781,8 +3781,8 @@ mod tests { let calm = TodayAgendaProjection::default(); let with_attention = TodayAgendaProjection { draft_products: vec![ProductListRow { - product_id: super::ProductId::new(), - farm_id: FarmId::new(), + product_id: super::ProductId::generate(), + farm_id: FarmId::generate(), title: "Spring onions".to_owned(), status: super::ProductStatus::Draft, stock_count: 0, @@ -3809,16 +3809,16 @@ mod tests { fn today_agenda_projection_can_hold_truthful_lists() { let projection = TodayAgendaProjection { orders_needing_action: vec![OrderListRow { - order_id: super::OrderId::new(), - farm_id: FarmId::new(), - fulfillment_window_id: Some(super::FulfillmentWindowId::new()), + order_id: super::OrderId::generate(), + farm_id: FarmId::generate(), + fulfillment_window_id: Some(super::FulfillmentWindowId::generate()), order_number: "R-1001".to_owned(), customer_display_name: "Casey".to_owned(), status: super::OrderStatus::NeedsAction, }], low_stock_products: vec![ProductListRow { - product_id: super::ProductId::new(), - farm_id: FarmId::new(), + product_id: super::ProductId::generate(), + farm_id: FarmId::generate(), title: "Carrots".to_owned(), status: super::ProductStatus::Published, stock_count: 2, @@ -3893,7 +3893,7 @@ mod tests { #[test] fn saved_farm_projection_is_always_ready() { let saved_farm = super::FarmSummary { - farm_id: FarmId::new(), + farm_id: FarmId::generate(), display_name: "North field farm".to_owned(), readiness: super::FarmReadiness::Ready, }; @@ -3927,8 +3927,8 @@ mod tests { blockers: vec![FarmReadinessBlocker::MissingOperatingRules], timing_conflicts: vec![FarmTimingConflict { kind: FarmTimingConflictKind::BlackoutOverlapsFulfillmentWindow, - fulfillment_window_id: Some(super::FulfillmentWindowId::new()), - blackout_period_id: Some(BlackoutPeriodId::new()), + fulfillment_window_id: Some(super::FulfillmentWindowId::generate()), + blackout_period_id: Some(BlackoutPeriodId::generate()), }], }; @@ -3970,10 +3970,10 @@ mod tests { #[test] fn farm_rules_projection_represents_full_v1_inventory() { - let farm_id = FarmId::new(); - let pickup_location_id = PickupLocationId::new(); - let fulfillment_window_id = super::FulfillmentWindowId::new(); - let blackout_period_id = BlackoutPeriodId::new(); + let farm_id = FarmId::generate(); + let pickup_location_id = PickupLocationId::generate(); + let fulfillment_window_id = super::FulfillmentWindowId::generate(); + let blackout_period_id = BlackoutPeriodId::generate(); let projection = super::FarmRulesProjection { farm_profile: Some(super::FarmProfileRecord { farm_id, @@ -4088,12 +4088,12 @@ mod tests { #[test] fn activity_context_preserves_recent_event_order() { let first = AppActivityEvent { - activity_event_id: ActivityEventId::new(), + activity_event_id: ActivityEventId::generate(), recorded_at: "2026-04-18T00:00:00.000Z".to_owned(), kind: AppActivityKind::HomeOpened, }; let second = AppActivityEvent { - activity_event_id: ActivityEventId::new(), + activity_event_id: ActivityEventId::generate(), recorded_at: "2026-04-18T00:01:00.000Z".to_owned(), kind: AppActivityKind::SettingsOpened { section: SettingsSection::About,