iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

commit 7b1cd479f0ac5e5fafc6b033abaa8c0e6c06159c
parent 5196438ecee2685eda24b83417048953a65858f9
Author: Joris Hartog <jorishartog@hotmail.com>
Date:   Wed, 26 Aug 2026 01:26:05 +0200

Rebuild incompatible UI caches on startup

Diffstat:
Msrc/adapters/ui_data_store.rs | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
1 file changed, 122 insertions(+), 9 deletions(-)

diff --git a/src/adapters/ui_data_store.rs b/src/adapters/ui_data_store.rs @@ -123,6 +123,21 @@ CREATE TABLE IF NOT EXISTS ui_burn_leader_rank_blocks ( ); "#; +const RESET_SCHEMA: &str = r#" +DROP TABLE IF EXISTS block_metrics; +DROP TABLE IF EXISTS metrics_cache_meta; +DROP TABLE IF EXISTS metric_known_addresses; +DROP TABLE IF EXISTS ui_leaderboards; +DROP TABLE IF EXISTS ui_cache_meta; +DROP TABLE IF EXISTS ui_output_index; +DROP TABLE IF EXISTS ui_utxos; +DROP TABLE IF EXISTS ui_wallet_transactions; +DROP TABLE IF EXISTS ui_revealed_transactions; +DROP TABLE IF EXISTS ui_burn_leader_ranks; +DROP TABLE IF EXISTS ui_burn_leader_rank_blocks; +"#; + +const UI_DATA_SCHEMA_VERSION: u32 = 1; const UI_CACHE_SCHEMA_VERSION: u32 = 3; const METRICS_CACHE_SCHEMA_VERSION: u32 = 1; @@ -184,15 +199,7 @@ impl SqliteUiDataStore { let store = Self { path }; store.with_connection_mut(|connection| { - connection - .execute_batch(SCHEMA) - .context("failed to initialize UI data database schema")?; - ensure_block_metrics_column( - connection, - "known_wallet_addresses", - "INTEGER NOT NULL DEFAULT 0", - )?; - Ok(()) + initialize_ui_data_schema(connection, store.path()) })?; Ok(store) } @@ -482,6 +489,55 @@ PRAGMA synchronous = NORMAL; } } +fn initialize_ui_data_schema(connection: &mut Connection, path: &Path) -> Result<()> { + let stored_version = connection + .pragma_query_value(None, "user_version", |row| row.get::<_, u32>(0)) + .context("failed to inspect UI data database schema version")?; + let has_tables = connection + .query_row( + "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%')", + [], + |row| row.get::<_, bool>(0), + ) + .context("failed to inspect UI data database tables")?; + + if has_tables && stored_version != UI_DATA_SCHEMA_VERSION { + println!( + "rebuilding incompatible UI cache schema at {} (version {stored_version}, expected {UI_DATA_SCHEMA_VERSION})", + path.display() + ); + let transaction = connection + .transaction() + .context("failed to start UI data schema rebuild transaction")?; + transaction + .execute_batch(RESET_SCHEMA) + .context("failed to clear incompatible UI data database schema")?; + transaction + .execute_batch(SCHEMA) + .context("failed to recreate UI data database schema")?; + transaction + .pragma_update(None, "user_version", UI_DATA_SCHEMA_VERSION) + .context("failed to record UI data database schema version")?; + transaction + .commit() + .context("failed to commit UI data database schema rebuild")?; + return Ok(()); + } + + connection + .execute_batch(SCHEMA) + .context("failed to initialize UI data database schema")?; + ensure_block_metrics_column( + connection, + "known_wallet_addresses", + "INTEGER NOT NULL DEFAULT 0", + )?; + connection + .pragma_update(None, "user_version", UI_DATA_SCHEMA_VERSION) + .context("failed to record UI data database schema version")?; + Ok(()) +} + #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct UiLeaderboards { pub balances: Vec<UiLeaderboardEntry>, @@ -1595,6 +1651,63 @@ mod tests { } #[test] + fn opening_legacy_ui_schema_rebuilds_the_derived_cache() { + let dir = tempdir().unwrap(); + let path = dir.path().join("ui_data.sqlite3"); + let connection = Connection::open(&path).unwrap(); + connection + .execute_batch( + r#" +CREATE TABLE ui_wallet_transactions ( + address TEXT NOT NULL, + sort_key INTEGER NOT NULL, + kind TEXT NOT NULL, + signature TEXT NOT NULL, + block_height INTEGER NOT NULL, + timestamp_ms INTEGER NOT NULL, + block_finalizer TEXT NOT NULL, + blinded INTEGER NOT NULL, + transaction_json BLOB NOT NULL, + PRIMARY KEY (address, signature) +); +"#, + ) + .unwrap(); + drop(connection); + + let store = SqliteUiDataStore::open(&path).unwrap(); + let connection = Connection::open(&path).unwrap(); + let columns = connection + .prepare("PRAGMA table_info(ui_wallet_transactions)") + .unwrap() + .query_map([], |row| row.get::<_, String>(1)) + .unwrap() + .collect::<rusqlite::Result<Vec<_>>>() + .unwrap(); + let schema_version = connection + .pragma_query_value(None, "user_version", |row| row.get::<_, u32>(0)) + .unwrap(); + assert!(!columns.iter().any(|column| column == "blinded")); + assert_eq!(schema_version, super::UI_DATA_SCHEMA_VERSION); + drop(connection); + + let seed = "legacy-ui-schema"; + let snapshot = test_snapshot(seed); + let tip = snapshot.blocks.last().unwrap().hash.clone(); + let wallet = Wallet::from_seed(seed); + store.project_snapshot(&snapshot, true).unwrap(); + let (_, total) = store + .load_wallet_transactions(wallet.address(), &["burn"], 0, 10) + .unwrap(); + assert!(total > 0); + + drop(store); + let reopened = SqliteUiDataStore::open(&path).unwrap(); + assert!(reopened.is_projected_to(&tip).unwrap()); + assert!(reopened.metrics_are_projected_to(&tip).unwrap()); + } + + #[test] fn failed_ui_projection_keeps_last_committed_projection() { let dir = tempdir().unwrap(); let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap();