app

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

commit c0b5c2deef93095613f50e27fcbb53a26db61b2b
parent fdf71dacc29d186713482f1437e30ebb58b40b6d
Author: triesap <tyson@radroots.org>
Date:   Tue, 30 Jun 2026 10:13:25 +0000

store: enforce current schema cleanup

Diffstat:
Mcrates/desktop/src/source_guards.rs | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/store/migrations/0010_sync_contract_alignment.sql | 8++++----
Mcrates/store/migrations/0016_deterministic_outbox.sql | 6+++---
Mcrates/store/migrations/0020_declined_order_status.sql | 18+++++++++---------
Mcrates/store/migrations/0027_local_interop_validation_receipt_projection_kind.sql | 6+++---
Mcrates/store/src/lib.rs | 99+++++++++++++++++++++----------------------------------------------------------
Mcrates/sync/src/publish.rs | 2+-
7 files changed, 100 insertions(+), 93 deletions(-)

diff --git a/crates/desktop/src/source_guards.rs b/crates/desktop/src/source_guards.rs @@ -1517,6 +1517,60 @@ fn app_production_sdk_boundary_usage_is_exception_scoped() { } #[test] +fn app_store_current_schema_surfaces_reject_retired_terms() { + let app_root = app_root(); + let mut paths = vec![ + app_root.join("crates/store/src/lib.rs"), + app_root.join("crates/sync/src/publish.rs"), + ]; + for entry in + fs::read_dir(app_root.join("crates/store/migrations")).expect("read store migrations") + { + let path = entry.expect("migration entry").path(); + if path.extension().and_then(|ext| ext.to_str()) == Some("sql") { + paths.push(path); + } + } + + let offenders = paths + .iter() + .flat_map(|path| { + let source = read_source_path(path); + let relative_path = path + .strip_prefix(app_root.as_path()) + .expect("app relative path") + .display() + .to_string(); + APP_STORE_RETIRED_SCHEMA_TERMS + .iter() + .filter(move |term| source.contains(**term)) + .map(move |term| (relative_path.clone(), *term)) + }) + .collect::<Vec<_>>(); + + assert!( + offenders.is_empty(), + "app store current-schema surfaces retain retired terms: {offenders:?}" + ); +} + +const APP_STORE_RETIRED_SCHEMA_TERMS: &[&str] = &[ + "legacy", + "compat", + "shim", + "deprecated", + "dual_read", + "dual_write", + "dual-read", + "dual-write", + "AppSdkMigration", + "sdk_migration", + "migration_receipt", + "migration_audit", + "migration_scaffold", +]; + +#[test] fn strict_sdk_boundary_scanner_rejects_unexcepted_new_production_paths() { let findings = unexcepted_sdk_boundary_patterns( "crates/desktop/src/new_workflow.rs", diff --git a/crates/store/migrations/0010_sync_contract_alignment.sql b/crates/store/migrations/0010_sync_contract_alignment.sql @@ -1,5 +1,5 @@ -ALTER TABLE local_outbox RENAME TO local_outbox_legacy; -ALTER TABLE local_conflicts RENAME TO local_conflicts_legacy; +ALTER TABLE local_outbox RENAME TO local_outbox_rebuild_source; +ALTER TABLE local_conflicts RENAME TO local_conflicts_rebuild_source; DROP TABLE sync_checkpoints; CREATE TABLE local_outbox ( @@ -56,8 +56,8 @@ CREATE TABLE sync_checkpoints ( last_error_message TEXT ); -DROP TABLE local_outbox_legacy; -DROP TABLE local_conflicts_legacy; +DROP TABLE local_outbox_rebuild_source; +DROP TABLE local_conflicts_rebuild_source; CREATE INDEX idx_local_outbox_account_available_at ON local_outbox( account_id, diff --git a/crates/store/migrations/0016_deterministic_outbox.sql b/crates/store/migrations/0016_deterministic_outbox.sql @@ -1,4 +1,4 @@ -ALTER TABLE local_outbox RENAME TO local_outbox_legacy; +ALTER TABLE local_outbox RENAME TO local_outbox_rebuild_source; CREATE TABLE local_outbox ( id TEXT PRIMARY KEY NOT NULL, @@ -59,10 +59,10 @@ SELECT attempt_count, 'pending', NULL -FROM local_outbox_legacy +FROM local_outbox_rebuild_source ORDER BY available_at ASC, created_at ASC, id ASC; -DROP TABLE local_outbox_legacy; +DROP TABLE local_outbox_rebuild_source; CREATE INDEX idx_local_outbox_account_available_at ON local_outbox( account_id, diff --git a/crates/store/migrations/0020_declined_order_status.sql b/crates/store/migrations/0020_declined_order_status.sql @@ -5,9 +5,9 @@ DROP INDEX IF EXISTS idx_orders_farm_status; DROP INDEX IF EXISTS idx_orders_farm_window_status_updated_at; DROP INDEX IF EXISTS idx_orders_buyer_context_updated_at; -ALTER TABLE order_lines RENAME TO order_lines_legacy; -ALTER TABLE buyer_order_coordination_records RENAME TO buyer_order_coordination_records_legacy; -ALTER TABLE orders RENAME TO orders_legacy; +ALTER TABLE order_lines RENAME TO order_lines_rebuild_source; +ALTER TABLE buyer_order_coordination_records RENAME TO buyer_order_coordination_records_rebuild_source; +ALTER TABLE orders RENAME TO orders_rebuild_source; CREATE TABLE orders ( id TEXT PRIMARY KEY NOT NULL, @@ -50,7 +50,7 @@ SELECT buyer_email, buyer_phone, buyer_order_note -FROM orders_legacy; +FROM orders_rebuild_source; CREATE TABLE order_lines ( id TEXT PRIMARY KEY NOT NULL, @@ -105,7 +105,7 @@ SELECT listing_event_id, seller_pubkey, listing_relays_json -FROM order_lines_legacy; +FROM order_lines_rebuild_source; CREATE TABLE buyer_order_coordination_records ( order_id TEXT PRIMARY KEY NOT NULL REFERENCES orders(id) ON DELETE CASCADE, @@ -143,7 +143,7 @@ SELECT created_at, updated_at, synced_at -FROM buyer_order_coordination_records_legacy; +FROM buyer_order_coordination_records_rebuild_source; CREATE INDEX idx_orders_farm_status ON orders(farm_id, status); CREATE INDEX idx_orders_farm_window_status_updated_at @@ -158,6 +158,6 @@ CREATE INDEX idx_buyer_order_coordination_context_state_updated_at CREATE INDEX idx_buyer_order_coordination_state_updated_at ON buyer_order_coordination_records(state, updated_at); -DROP TABLE order_lines_legacy; -DROP TABLE buyer_order_coordination_records_legacy; -DROP TABLE orders_legacy; +DROP TABLE order_lines_rebuild_source; +DROP TABLE buyer_order_coordination_records_rebuild_source; +DROP TABLE orders_rebuild_source; diff --git a/crates/store/migrations/0027_local_interop_validation_receipt_projection_kind.sql b/crates/store/migrations/0027_local_interop_validation_receipt_projection_kind.sql @@ -2,7 +2,7 @@ DROP INDEX IF EXISTS idx_local_interop_imports_seq; DROP INDEX IF EXISTS idx_local_interop_imports_owner_status; DROP INDEX IF EXISTS idx_local_interop_imports_projected; -ALTER TABLE local_interop_imports RENAME TO local_interop_imports_validation_receipt_projection_kind_legacy; +ALTER TABLE local_interop_imports RENAME TO local_interop_imports_validation_receipt_projection_kind_rebuild_source; CREATE TABLE local_interop_imports ( record_id TEXT PRIMARY KEY NOT NULL, @@ -89,7 +89,7 @@ SELECT event_content, event_sig, raw_event_json -FROM local_interop_imports_validation_receipt_projection_kind_legacy; +FROM local_interop_imports_validation_receipt_projection_kind_rebuild_source; CREATE INDEX idx_local_interop_imports_seq ON local_interop_imports(local_seq); @@ -100,4 +100,4 @@ CREATE INDEX idx_local_interop_imports_owner_status CREATE INDEX idx_local_interop_imports_projected ON local_interop_imports(projected_kind, projected_id); -DROP TABLE local_interop_imports_validation_receipt_projection_kind_legacy; +DROP TABLE local_interop_imports_validation_receipt_projection_kind_rebuild_source; diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs @@ -789,7 +789,7 @@ fn apply_migrations(connection: &mut Connection) -> Result<(), AppSqliteError> { #[cfg(test)] mod tests { - use super::{AppSqliteStore, DatabaseTarget, latest_schema_version, migrations}; + use super::{AppSqliteStore, DatabaseTarget, latest_schema_version}; use rusqlite::{Connection, params}; use std::{ env, fs, @@ -1190,27 +1190,8 @@ mod tests { } #[test] - fn legacy_sync_scaffolding_migrates_to_account_scoped_contract() { - let path = temp_database_path("legacy-sync-contract"); - fs::create_dir_all(path.parent().expect("temp database should have a parent")) - .expect("legacy database parent should exist"); - let connection = Connection::open(&path).expect("legacy database should open"); - - for (version, sql) in migrations::pending_migrations(0) - .filter(|(version, _)| *version < latest_schema_version()) - { - connection - .execute_batch(sql) - .expect("legacy migration should apply"); - connection - .pragma_update(None, "user_version", version) - .expect("legacy schema version should record"); - } - - drop(connection); - - let store = - AppSqliteStore::open(DatabaseTarget::Path(path.clone())).expect("store should open"); + fn sync_contract_schema_bootstraps_account_scoped_tables() { + let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); let connection = store.connection(); assert_eq!( @@ -1229,38 +1210,19 @@ mod tests { assert!(column_exists(connection, "sync_checkpoints", "state")); assert!(table_exists(connection, "app_relay_ingest_freshness")); assert_eq!(row_count(connection, "sync_checkpoints"), 0); - - drop(store); - remove_database_artifacts(&path); } #[test] - fn legacy_orders_status_migration_preserves_child_rows_and_accepts_declined() { - let path = temp_database_path("legacy-declined-orders"); - fs::create_dir_all(path.parent().expect("temp database should have a parent")) - .expect("legacy database parent should exist"); - let connection = Connection::open(&path).expect("legacy database should open"); - connection - .execute_batch("PRAGMA foreign_keys = ON") - .expect("foreign keys should enable"); - - for (version, sql) in migrations::pending_migrations(0).filter(|(version, _)| *version < 20) - { - connection - .execute_batch(sql) - .expect("legacy migration should apply"); - connection - .pragma_update(None, "user_version", version) - .expect("legacy schema version should record"); - } - + fn order_status_schema_preserves_child_rows_and_accepts_declined() { + let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open"); + let connection = store.connection(); connection .execute( "INSERT INTO farms (id, display_name, readiness, created_at, updated_at) - VALUES (?1, 'Legacy Farm', 'ready', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')", - params!["farm_legacy"], + VALUES (?1, 'Schema Farm', 'ready', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')", + params!["farm_status"], ) - .expect("legacy farm should insert"); + .expect("farm should insert"); connection .execute( "INSERT INTO orders ( @@ -1276,11 +1238,11 @@ mod tests { buyer_phone, buyer_order_note ) VALUES ( - 'order_legacy', - 'farm_legacy', + 'order_status', + 'farm_status', NULL, 'R-900', - 'Legacy Buyer', + 'Schema Buyer', 'needs_action', '2026-01-01T00:00:00Z', 'account:buyer', @@ -1290,7 +1252,7 @@ mod tests { )", [], ) - .expect("legacy order should insert"); + .expect("order should insert"); connection .execute( "INSERT INTO order_lines ( @@ -1300,15 +1262,15 @@ mod tests { quantity_value, quantity_display ) VALUES ( - 'line_legacy', - 'order_legacy', - 'Legacy Eggs', + 'line_status', + 'order_status', + 'Schema Eggs', 2, '2 each' )", [], ) - .expect("legacy order line should insert"); + .expect("order line should insert"); connection .execute( "INSERT INTO buyer_order_coordination_records ( @@ -1318,7 +1280,7 @@ mod tests { created_at, updated_at ) VALUES ( - 'order_legacy', + 'order_status', 'account:buyer', 'pending', '2026-01-01T00:00:00Z', @@ -1326,13 +1288,7 @@ mod tests { )", [], ) - .expect("legacy buyer coordination should insert"); - - drop(connection); - - let store = - AppSqliteStore::open(DatabaseTarget::Path(path.clone())).expect("store should open"); - let connection = store.connection(); + .expect("buyer coordination should insert"); assert_eq!( store.schema_version().expect("schema version"), @@ -1345,20 +1301,20 @@ mod tests { connection .execute( - "UPDATE orders SET status = 'declined' WHERE id = 'order_legacy'", + "UPDATE orders SET status = 'declined' WHERE id = 'order_status'", [], ) - .expect("declined status should satisfy migrated check"); + .expect("declined status should satisfy current check"); connection .execute( - "UPDATE orders SET status = 'needs_review' WHERE id = 'order_legacy'", + "UPDATE orders SET status = 'needs_review' WHERE id = 'order_status'", [], ) - .expect("needs review status should satisfy migrated check"); + .expect("needs review status should satisfy current check"); let status: String = connection .query_row( - "SELECT status FROM orders WHERE id = 'order_legacy'", + "SELECT status FROM orders WHERE id = 'order_status'", [], |row| row.get(0), ) @@ -1366,13 +1322,10 @@ mod tests { assert_eq!(status, "needs_review"); connection .execute( - "UPDATE orders SET workflow_agreement = 'agreed_pending_rhi' WHERE id = 'order_legacy'", + "UPDATE orders SET workflow_agreement = 'agreed_pending_rhi' WHERE id = 'order_status'", [], ) - .expect("agreed pending rhi agreement should satisfy migrated check"); - - drop(store); - remove_database_artifacts(&path); + .expect("agreed pending rhi agreement should satisfy current check"); } fn table_exists(connection: &Connection, table_name: &str) -> bool { diff --git a/crates/sync/src/publish.rs b/crates/sync/src/publish.rs @@ -1051,7 +1051,7 @@ mod tests { } #[test] - fn existing_raw_payload_outbox_work_remains_local_save_compatible() { + 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()),