Skip to content

Commit d5dcd7b

Browse files
lklimekclaude
andcommitted
fix(db): handle missing network column in v0.9.0 scheduled_votes migration
The v0.9.0 release created scheduled_votes without a network column. The v6 migration (update_scheduled_votes_table) assumed it existed, causing migration failure for v0.9.0 users upgrading to v1.0. Fix: check if scheduled_votes_old has a network column before copying data. If missing, default to 'dash' (the only network at v0.9.0). Add test_migration_from_v090_to_current that creates the exact v0.9.0 schema at DB version 5, populates realistic data, and migrates all the way to current version — verifying data survives with correct network rename. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a8db546 commit d5dcd7b

2 files changed

Lines changed: 315 additions & 5 deletions

File tree

src/database/initialization.rs

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,4 +1820,299 @@ mod test {
18201820
"valid asset_lock identity_id should be preserved"
18211821
);
18221822
}
1823+
1824+
/// Test migration from v0.9.0 schema (DB version 5) all the way to current.
1825+
/// This is the exact schema shipped in the v0.9.0 release, with realistic
1826+
/// data including wallets, addresses, identities, and asset locks.
1827+
#[test]
1828+
fn test_migration_from_v090_to_current() {
1829+
let temp_dir = tempfile::tempdir().unwrap();
1830+
let db_file_path = temp_dir.path().join("v090.db");
1831+
let db = super::Database::new(&db_file_path).unwrap();
1832+
1833+
{
1834+
let conn = db.conn.lock().unwrap();
1835+
1836+
// Exact v0.9.0 schema — copied from git show v0.9.0:src/database/initialization.rs
1837+
conn.execute_batch(
1838+
"CREATE TABLE IF NOT EXISTS settings (
1839+
id INTEGER PRIMARY KEY CHECK (id = 1),
1840+
password_check BLOB,
1841+
main_password_salt BLOB,
1842+
main_password_nonce BLOB,
1843+
network TEXT NOT NULL,
1844+
start_root_screen INTEGER NOT NULL,
1845+
custom_dash_qt_path TEXT,
1846+
overwrite_dash_conf INTEGER,
1847+
database_version INTEGER NOT NULL
1848+
);
1849+
1850+
CREATE TABLE IF NOT EXISTS wallet (
1851+
seed_hash BLOB NOT NULL PRIMARY KEY,
1852+
encrypted_seed BLOB NOT NULL,
1853+
salt BLOB NOT NULL,
1854+
nonce BLOB NOT NULL,
1855+
master_ecdsa_bip44_account_0_epk BLOB NOT NULL,
1856+
alias TEXT,
1857+
is_main INTEGER,
1858+
uses_password INTEGER NOT NULL,
1859+
password_hint TEXT,
1860+
network TEXT NOT NULL
1861+
);
1862+
1863+
CREATE TABLE IF NOT EXISTS wallet_addresses (
1864+
seed_hash BLOB NOT NULL,
1865+
address TEXT NOT NULL,
1866+
derivation_path TEXT NOT NULL,
1867+
balance INTEGER,
1868+
path_reference INTEGER NOT NULL,
1869+
path_type INTEGER NOT NULL,
1870+
PRIMARY KEY (seed_hash, address),
1871+
FOREIGN KEY (seed_hash) REFERENCES wallet(seed_hash) ON DELETE CASCADE
1872+
);
1873+
1874+
CREATE INDEX IF NOT EXISTS idx_wallet_addresses_path_reference
1875+
ON wallet_addresses (path_reference);
1876+
CREATE INDEX IF NOT EXISTS idx_wallet_addresses_path_type
1877+
ON wallet_addresses (path_type);
1878+
1879+
CREATE TABLE IF NOT EXISTS utxos (
1880+
txid BLOB NOT NULL,
1881+
vout INTEGER NOT NULL,
1882+
address TEXT NOT NULL,
1883+
value INTEGER NOT NULL,
1884+
script_pubkey BLOB NOT NULL,
1885+
network TEXT NOT NULL,
1886+
PRIMARY KEY (txid, vout, network)
1887+
);
1888+
1889+
CREATE INDEX IF NOT EXISTS idx_utxos_address ON utxos (address);
1890+
CREATE INDEX IF NOT EXISTS idx_utxos_network ON utxos (network);
1891+
1892+
CREATE TABLE IF NOT EXISTS asset_lock_transaction (
1893+
tx_id BLOB PRIMARY KEY,
1894+
transaction_data BLOB NOT NULL,
1895+
amount INTEGER,
1896+
instant_lock_data BLOB,
1897+
chain_locked_height INTEGER,
1898+
identity_id BLOB,
1899+
identity_id_potentially_in_creation BLOB,
1900+
wallet BLOB NOT NULL,
1901+
network TEXT NOT NULL,
1902+
FOREIGN KEY (identity_id) REFERENCES identity(id) ON DELETE CASCADE,
1903+
FOREIGN KEY (identity_id_potentially_in_creation) REFERENCES identity(id),
1904+
FOREIGN KEY (wallet) REFERENCES wallet(seed_hash) ON DELETE CASCADE
1905+
);
1906+
1907+
CREATE TABLE IF NOT EXISTS identity (
1908+
id BLOB PRIMARY KEY,
1909+
data BLOB,
1910+
is_in_creation INTEGER NOT NULL DEFAULT 0,
1911+
is_local INTEGER NOT NULL,
1912+
alias TEXT,
1913+
info TEXT,
1914+
wallet BLOB,
1915+
wallet_index INTEGER,
1916+
identity_type TEXT,
1917+
network TEXT NOT NULL,
1918+
CHECK ((wallet IS NOT NULL AND wallet_index IS NOT NULL)
1919+
OR (wallet IS NULL AND wallet_index IS NULL)),
1920+
FOREIGN KEY (wallet) REFERENCES wallet(seed_hash) ON DELETE CASCADE
1921+
);
1922+
1923+
CREATE INDEX IF NOT EXISTS idx_identity_local_network_type
1924+
ON identity (is_local, network, identity_type);
1925+
1926+
CREATE TABLE IF NOT EXISTS contested_name (
1927+
normalized_contested_name TEXT NOT NULL,
1928+
locked_votes INTEGER,
1929+
abstain_votes INTEGER,
1930+
awarded_to BLOB,
1931+
end_time INTEGER,
1932+
locked INTEGER NOT NULL DEFAULT 0,
1933+
last_updated INTEGER,
1934+
network TEXT NOT NULL,
1935+
PRIMARY KEY (normalized_contested_name, network)
1936+
);
1937+
1938+
CREATE TABLE IF NOT EXISTS contestant (
1939+
normalized_contested_name TEXT NOT NULL,
1940+
identity_id BLOB NOT NULL,
1941+
name TEXT,
1942+
votes INTEGER,
1943+
created_at INTEGER,
1944+
created_at_block_height INTEGER,
1945+
created_at_core_block_height INTEGER,
1946+
document_id BLOB,
1947+
network TEXT NOT NULL,
1948+
PRIMARY KEY (normalized_contested_name, identity_id, network),
1949+
FOREIGN KEY (normalized_contested_name, network)
1950+
REFERENCES contested_name(normalized_contested_name, network)
1951+
ON DELETE CASCADE
1952+
);
1953+
1954+
CREATE TABLE IF NOT EXISTS contract (
1955+
contract_id BLOB,
1956+
contract BLOB,
1957+
name TEXT,
1958+
network TEXT NOT NULL,
1959+
PRIMARY KEY (contract_id, network)
1960+
);
1961+
1962+
CREATE INDEX IF NOT EXISTS idx_name_network ON contract (name, network);",
1963+
)
1964+
.unwrap();
1965+
1966+
// v0.9.0 also created these via separate functions
1967+
// proof_log (v2)
1968+
conn.execute_batch(
1969+
"CREATE TABLE IF NOT EXISTS proof_log (
1970+
proof_log_id INTEGER PRIMARY KEY AUTOINCREMENT,
1971+
proof_log BLOB NOT NULL,
1972+
proof_log_timestamp INTEGER NOT NULL
1973+
);",
1974+
)
1975+
.unwrap();
1976+
1977+
// top_up (v4)
1978+
conn.execute_batch(
1979+
"CREATE TABLE IF NOT EXISTS top_up (
1980+
identity_id BLOB NOT NULL,
1981+
top_up_index INTEGER NOT NULL,
1982+
amount INTEGER NOT NULL,
1983+
PRIMARY KEY (identity_id, top_up_index),
1984+
FOREIGN KEY (identity_id) REFERENCES identity(id) ON DELETE CASCADE
1985+
);",
1986+
)
1987+
.unwrap();
1988+
1989+
// scheduled_votes (v5) — v0.9.0 schema had NO network column
1990+
// and NO FK to identity. The v6 migration handles both.
1991+
conn.execute_batch(
1992+
"CREATE TABLE IF NOT EXISTS scheduled_votes (
1993+
identity_id BLOB NOT NULL,
1994+
contested_name TEXT NOT NULL,
1995+
vote_choice TEXT NOT NULL,
1996+
time INTEGER NOT NULL,
1997+
executed INTEGER NOT NULL DEFAULT 0,
1998+
PRIMARY KEY (identity_id, contested_name)
1999+
);",
2000+
)
2001+
.unwrap();
2002+
2003+
// Insert settings at version 5
2004+
conn.execute(
2005+
"INSERT INTO settings (id, network, start_root_screen, database_version)
2006+
VALUES (1, 'dash', 0, 5)",
2007+
[],
2008+
)
2009+
.unwrap();
2010+
2011+
// Insert a wallet with some addresses and an identity
2012+
let seed_hash = vec![0xAAu8; 32];
2013+
conn.execute(
2014+
"INSERT INTO wallet (seed_hash, encrypted_seed, salt, nonce,
2015+
master_ecdsa_bip44_account_0_epk, alias, is_main, uses_password, network)
2016+
VALUES (?1, ?2, ?3, ?4, ?5, 'test-wallet', 1, 0, 'dash')",
2017+
params![
2018+
seed_hash,
2019+
vec![1u8; 64],
2020+
vec![2u8; 16],
2021+
vec![3u8; 12],
2022+
vec![4u8; 33]
2023+
],
2024+
)
2025+
.unwrap();
2026+
2027+
conn.execute(
2028+
"INSERT INTO wallet_addresses (seed_hash, address, derivation_path,
2029+
balance, path_reference, path_type)
2030+
VALUES (?1, 'yTestAddr1', 'm/44''/1''/0''/0/0', 50000, 0, 0)",
2031+
params![seed_hash],
2032+
)
2033+
.unwrap();
2034+
2035+
let identity_id = vec![0xBBu8; 32];
2036+
conn.execute(
2037+
"INSERT INTO identity (id, is_local, alias, wallet, wallet_index,
2038+
identity_type, network)
2039+
VALUES (?1, 1, 'my-identity', ?2, 0, 'user', 'dash')",
2040+
params![identity_id, seed_hash],
2041+
)
2042+
.unwrap();
2043+
2044+
conn.execute(
2045+
"INSERT INTO asset_lock_transaction (tx_id, transaction_data, amount,
2046+
identity_id, wallet, network)
2047+
VALUES (?1, ?2, 100000, ?3, ?4, 'dash')",
2048+
params![vec![0xCCu8; 32], vec![0u8; 50], identity_id, seed_hash],
2049+
)
2050+
.unwrap();
2051+
2052+
conn.execute(
2053+
"INSERT INTO contract (contract_id, contract, name, network)
2054+
VALUES (?1, ?2, 'dpns', 'dash')",
2055+
params![vec![0xDDu8; 32], vec![0u8; 100]],
2056+
)
2057+
.unwrap();
2058+
}
2059+
2060+
assert_eq!(db.db_schema_version().unwrap(), 5);
2061+
2062+
// Run full migration from v5 to current
2063+
let result = db.try_perform_migration(5, DEFAULT_DB_VERSION);
2064+
assert!(
2065+
result.is_ok(),
2066+
"migration from v0.9.0 (v5) to v{DEFAULT_DB_VERSION} failed: {:?}",
2067+
result.err()
2068+
);
2069+
2070+
assert_eq!(db.db_schema_version().unwrap(), DEFAULT_DB_VERSION);
2071+
2072+
let conn = db.conn.lock().unwrap();
2073+
assert_v33_schema(&conn);
2074+
2075+
// Verify data survived migration
2076+
let wallet_network: String = conn
2077+
.query_row(
2078+
"SELECT network FROM wallet WHERE seed_hash = ?1",
2079+
params![vec![0xAAu8; 32]],
2080+
|row| row.get(0),
2081+
)
2082+
.unwrap();
2083+
assert_eq!(
2084+
wallet_network, "mainnet",
2085+
"wallet network should be renamed"
2086+
);
2087+
2088+
// wallet_addresses should have total_received column (added by v17)
2089+
assert_column_exists(&conn, "wallet_addresses", "total_received");
2090+
2091+
// wallet should have balance columns (added by v16)
2092+
assert_column_exists(&conn, "wallet", "confirmed_balance");
2093+
assert_column_exists(&conn, "wallet", "total_balance");
2094+
2095+
// wallet should have core_wallet_name (added by v33)
2096+
assert_column_exists(&conn, "wallet", "core_wallet_name");
2097+
2098+
// Identity should survive with network renamed
2099+
let id_network: String = conn
2100+
.query_row(
2101+
"SELECT network FROM identity WHERE id = ?1",
2102+
params![vec![0xBBu8; 32]],
2103+
|row| row.get(0),
2104+
)
2105+
.unwrap();
2106+
assert_eq!(id_network, "mainnet");
2107+
2108+
// Asset lock should survive with identity_id intact
2109+
let lock_identity: Option<Vec<u8>> = conn
2110+
.query_row(
2111+
"SELECT identity_id FROM asset_lock_transaction WHERE tx_id = ?1",
2112+
params![vec![0xCCu8; 32]],
2113+
|row| row.get(0),
2114+
)
2115+
.unwrap();
2116+
assert_eq!(lock_identity, Some(vec![0xBBu8; 32]));
2117+
}
18232118
}

src/database/scheduled_votes.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,28 @@ impl Database {
7979
[],
8080
)?;
8181

82-
// Copy data from old to new table
83-
conn.execute(
84-
"INSERT INTO scheduled_votes (identity_id, contested_name, vote_choice, time, executed, network)
85-
SELECT identity_id, contested_name, vote_choice, time, executed, network
86-
FROM scheduled_votes_old",
82+
// Copy data from old to new table. The v0.9.0 schema created
83+
// scheduled_votes without a network column, so handle both cases.
84+
let has_network: bool = conn.query_row(
85+
"SELECT COUNT(*) FROM pragma_table_info('scheduled_votes_old') WHERE name='network'",
8786
[],
87+
|row| row.get::<_, i32>(0).map(|count| count > 0),
8888
)?;
89+
if has_network {
90+
conn.execute(
91+
"INSERT INTO scheduled_votes (identity_id, contested_name, vote_choice, time, executed, network)
92+
SELECT identity_id, contested_name, vote_choice, time, executed, network
93+
FROM scheduled_votes_old",
94+
[],
95+
)?;
96+
} else {
97+
conn.execute(
98+
"INSERT INTO scheduled_votes (identity_id, contested_name, vote_choice, time, executed, network)
99+
SELECT identity_id, contested_name, vote_choice, time, executed, 'dash'
100+
FROM scheduled_votes_old",
101+
[],
102+
)?;
103+
}
89104

90105
// Drop the old table
91106
conn.execute("DROP TABLE scheduled_votes_old", [])?;

0 commit comments

Comments
 (0)