From 66f13673e567eed9b095d57cfa58bde043aa5754 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Fri, 1 May 2026 13:50:01 -0400 Subject: [PATCH] schema: Add 0004_schema.up.sql The network table should hold at most one row (the network used which the wallet), however it previously had no unique constraint, meaning every INSERT would accumulate duplicate rows. Recreate the network table with an `id` column that is both the PRIMARY KEY and always checked to be 0. This makes it structurally impossible for more than one row to exist. --- migrations/0004_schema.up.sql | 21 +++++++++++++++++++++ src/wallet.rs | 30 +++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 migrations/0004_schema.up.sql diff --git a/migrations/0004_schema.up.sql b/migrations/0004_schema.up.sql new file mode 100644 index 0000000..62ad791 --- /dev/null +++ b/migrations/0004_schema.up.sql @@ -0,0 +1,21 @@ +-- Recreate the network table with an `id` column that is both the PRIMARY KEY and +-- always checked to be 0. This makes it structurally impossible for more +-- than one row to exist. + +-- ********************************************** -- +-- Add singleton constraint to the network table. -- +-- ********************************************** -- + +CREATE TABLE IF NOT EXISTS network_new( + id INTEGER NOT NULL DEFAULT 0 CHECK(id = 0), + network TEXT NOT NULL, + PRIMARY KEY(id) +); + +-- Copy at most one existing row, pinning id to 0. +INSERT OR IGNORE INTO network_new(id, network) +SELECT 0, network FROM network LIMIT 1; + +DROP TABLE network; + +ALTER TABLE network_new RENAME TO network; diff --git a/src/wallet.rs b/src/wallet.rs index 72c47f7..f6beb6b 100644 --- a/src/wallet.rs +++ b/src/wallet.rs @@ -42,7 +42,7 @@ impl Store { /// Write network. pub async fn write_network(conn: &mut SqliteConnection, network: Network) -> Result<(), Error> { - sqlx::query("INSERT OR IGNORE INTO network(network) VALUES($1)") + sqlx::query("INSERT OR IGNORE INTO network(id, network) VALUES(0, $1)") .bind(network.to_string()) .execute(&mut *conn) .await?; @@ -208,3 +208,31 @@ impl AsyncWalletPersister for Store { Box::pin(async { persister.write_changeset(changeset).await }) } } + +#[cfg(test)] +mod test { + use super::*; + + use bitcoin::Network; + + #[tokio::test] + async fn network_table_has_at_most_one_row() -> anyhow::Result<()> { + let store = Store::new_memory().await?; + store.migrate().await?; + + { + let mut txn = store.pool.begin().await?; + Store::write_network(&mut txn, Network::Bitcoin).await?; + Store::write_network(&mut txn, Network::Bitcoin).await?; + txn.commit().await?; + } + + let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM network") + .fetch_one(&store.pool) + .await?; + + assert_eq!(count, 1, "network table should have at most 1 row"); + + Ok(()) + } +}