migration_repairs.rs (5924B)
1 use radroots_replica_db::migrations; 2 use radroots_sql_core::{SqlExecutor, SqliteExecutor}; 3 use serde_json::Value; 4 5 fn query_rows(exec: &SqliteExecutor, sql: &str) -> Vec<Value> { 6 serde_json::from_str(&exec.query_raw(sql, "[]").expect("query should succeed")) 7 .expect("query should decode") 8 } 9 10 fn create_legacy_schema_without_secondary_indexes(exec: &SqliteExecutor) { 11 let schema = [ 12 "CREATE TABLE __migrations (id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, applied_at TEXT NOT NULL DEFAULT (datetime('now')))", 13 "CREATE TABLE farm (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, pubkey TEXT NOT NULL, name TEXT NOT NULL, about TEXT, website TEXT, picture TEXT, banner TEXT, location_primary TEXT, location_city TEXT, location_region TEXT, location_country TEXT)", 14 "CREATE TABLE gcs_location (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, lat REAL NOT NULL, lng REAL NOT NULL, geohash TEXT NOT NULL, point TEXT NOT NULL, polygon TEXT NOT NULL, accuracy REAL, altitude REAL, tag_0 TEXT, label TEXT, area REAL, elevation INTEGER, soil TEXT, climate TEXT, gc_id TEXT, gc_name TEXT, gc_admin1_id TEXT, gc_admin1_name TEXT, gc_country_id TEXT, gc_country_name TEXT)", 15 "CREATE TABLE trade_product (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), key TEXT NOT NULL, category TEXT NOT NULL, title TEXT NOT NULL, summary TEXT NOT NULL, process TEXT NOT NULL, lot TEXT NOT NULL, profile TEXT NOT NULL, year INTEGER NOT NULL, qty_amt INTEGER NOT NULL, qty_unit CHAR(4) NOT NULL, qty_label TEXT, qty_avail INTEGER, price_amt REAL NOT NULL, price_currency CHAR(3) NOT NULL, price_qty_amt INTEGER NOT NULL, price_qty_unit CHAR(4) NOT NULL, notes TEXT)", 16 "CREATE TABLE plot (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), d_tag TEXT NOT NULL, farm_id CHAR(36) NOT NULL, name TEXT NOT NULL, about TEXT, location_primary TEXT, location_city TEXT, location_region TEXT, location_country TEXT, FOREIGN KEY (farm_id) REFERENCES farm(id) ON DELETE CASCADE)", 17 "CREATE TABLE nostr_event_head (id CHAR(36) PRIMARY KEY NOT NULL UNIQUE CHECK(length(id) = 36), created_at DATETIME NOT NULL CHECK(length(created_at) = 24), updated_at DATETIME NOT NULL CHECK(length(updated_at) = 24), key TEXT NOT NULL UNIQUE, kind INTEGER NOT NULL, pubkey CHAR(64) NOT NULL CHECK(length(pubkey) = 64), d_tag TEXT NOT NULL, last_event_id CHAR(64) NOT NULL CHECK(length(last_event_id) = 64), last_created_at INTEGER NOT NULL, content_hash TEXT NOT NULL)", 18 ]; 19 20 for sql in schema { 21 exec.exec(sql, "[]") 22 .expect("schema statement should succeed"); 23 } 24 25 for name in [ 26 "0000_init", 27 "0001_log_error", 28 "0002_farm", 29 "0003_gcs_location", 30 "0004_trade_product", 31 "0005_nostr_profile", 32 "0006_nostr_relay", 33 "0007_media_image", 34 "0008_farm_gcs_location", 35 "0009_nostr_profile_relay", 36 "0010_trade_product_location", 37 "0011_trade_product_media", 38 "0012_plot", 39 "0013_plot_gcs_location", 40 "0014_farm_tag", 41 "0015_plot_tag", 42 "0016_farm_member", 43 "0017_farm_member_claim", 44 "0018_nostr_event_head", 45 ] { 46 let sql = format!("INSERT INTO __migrations(name) VALUES ('{name}')"); 47 exec.exec(&sql, "[]") 48 .expect("legacy migration marker should succeed"); 49 } 50 } 51 52 #[test] 53 fn run_all_up_repairs_missing_indexes_in_legacy_sqlite_dbs() { 54 let exec = SqliteExecutor::open_memory().expect("open sqlite memory"); 55 create_legacy_schema_without_secondary_indexes(&exec); 56 57 let before = query_rows( 58 &exec, 59 "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('farm_pubkey_d_tag_idx', 'gcs_location_geohash_idx', 'plot_farm_d_tag_idx', 'nostr_event_head_kind_idx') ORDER BY name", 60 ); 61 assert!(before.is_empty()); 62 63 migrations::run_all_up(&exec).expect("repair migration should succeed"); 64 65 let after = query_rows( 66 &exec, 67 "SELECT name FROM sqlite_master WHERE type = 'index' AND name IN ('farm_pubkey_d_tag_idx', 'gcs_location_geohash_idx', 'plot_farm_d_tag_idx', 'nostr_event_head_kind_idx') ORDER BY name", 68 ); 69 let names = after 70 .iter() 71 .map(|row| { 72 row.get("name") 73 .and_then(Value::as_str) 74 .expect("index name should exist") 75 .to_string() 76 }) 77 .collect::<Vec<_>>(); 78 assert_eq!( 79 names, 80 vec![ 81 "farm_pubkey_d_tag_idx".to_string(), 82 "gcs_location_geohash_idx".to_string(), 83 "nostr_event_head_kind_idx".to_string(), 84 "plot_farm_d_tag_idx".to_string(), 85 ] 86 ); 87 88 let trade_product_columns = query_rows(&exec, "PRAGMA table_info(trade_product)"); 89 assert!(trade_product_columns.iter().any(|row| { 90 row.get("name") 91 .and_then(Value::as_str) 92 .is_some_and(|name| name == "listing_addr") 93 })); 94 assert!(trade_product_columns.iter().any(|row| { 95 row.get("name") 96 .and_then(Value::as_str) 97 .is_some_and(|name| name == "primary_bin_id") 98 })); 99 assert!(trade_product_columns.iter().any(|row| { 100 row.get("name") 101 .and_then(Value::as_str) 102 .is_some_and(|name| name == "verified_primary_bin_id") 103 })); 104 }