2026-01-21 12:34:59 +01:00
|
|
|
// Copyright (C) 2024-2026 Whiterun LLC
|
2024-03-04 16:40:50 +01:00
|
|
|
//
|
|
|
|
|
// This software is licensed under the GNU Affero General Public License (AGPL), version 3.0 or later.
|
|
|
|
|
// A copy of the license can be found in the LICENSE file or at https://www.gnu.org/licenses/agpl-3.0.html
|
|
|
|
|
|
2025-08-15 18:07:15 +02:00
|
|
|
use anyhow::{bail, Result};
|
2026-02-17 17:47:43 +01:00
|
|
|
use sqlx::{Sqlite, SqlitePool};
|
2024-03-04 16:40:50 +01:00
|
|
|
|
Convert hash columns from TEXT to BLOB storage
Migrate all 32-byte hash fields from TEXT (64-char hex) to BLOB
(32 bytes) for ~50% storage savings on hash columns.
Changes:
- Add src/db/blob.rs with ToBlob/FromBlob traits for hash types
- Update all table schemas to use BLOB for hash columns
- Replace .to_hex() with .to_blob() for DB inserts
- Replace ::from_hex() with ::from_blob() for DB reads
- Use SQL hex() function where API responses need hex strings
- Bump DB_VERSION from 4 to 5
Affected tables: tx, pool, pool_history_entry, utxo_spending,
utxo_funding, user_action, auth_chain_entry, bcmr_data,
bcmr_failure, bcmr_well_known, crc20, crc20_candidates,
delphi_entry, cached_token_metrics
Fixes #1
2026-01-21 13:25:35 +01:00
|
|
|
pub const DB_VERSION: u32 = 5;
|
2025-08-15 18:07:15 +02:00
|
|
|
const DB_VERSION_KEY: &str = "db_version";
|
|
|
|
|
|
|
|
|
|
/// Create the config table
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn create_table(pool: &SqlitePool) {
|
|
|
|
|
sqlx::query(
|
2025-08-15 18:07:15 +02:00
|
|
|
"CREATE TABLE config (
|
|
|
|
|
key TEXT PRIMARY KEY,
|
|
|
|
|
value TEXT
|
|
|
|
|
)",
|
|
|
|
|
)
|
2026-02-17 17:47:43 +01:00
|
|
|
.execute(pool)
|
|
|
|
|
.await
|
2025-08-15 18:07:15 +02:00
|
|
|
.expect("failed to create config table");
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
/// Add or update config entry.
|
|
|
|
|
/// Accepts both `&SqlitePool` and `&mut SqliteConnection` (including transactions).
|
|
|
|
|
pub async fn config_set<'e>(
|
|
|
|
|
executor: impl sqlx::Executor<'e, Database = Sqlite>,
|
|
|
|
|
key: &str,
|
|
|
|
|
value: &str,
|
|
|
|
|
) {
|
|
|
|
|
sqlx::query("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)")
|
|
|
|
|
.bind(key)
|
|
|
|
|
.bind(value)
|
|
|
|
|
.execute(executor)
|
|
|
|
|
.await
|
2024-03-04 16:40:50 +01:00
|
|
|
.unwrap();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Get config entry
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn config_get(pool: &SqlitePool, key: &str) -> Result<Option<String>> {
|
|
|
|
|
let row: Option<(String,)> = sqlx::query_as("SELECT value FROM config WHERE key = ?")
|
|
|
|
|
.bind(key)
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
.await?;
|
|
|
|
|
Ok(row.map(|r| r.0))
|
2024-03-04 16:40:50 +01:00
|
|
|
}
|
2025-08-15 18:07:15 +02:00
|
|
|
|
|
|
|
|
/// Check database version and panic if it doesn't match expected version
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn check_db_version(pool: &SqlitePool) -> Result<()> {
|
|
|
|
|
let version_str = config_get(pool, DB_VERSION_KEY).await?;
|
2025-08-15 18:07:15 +02:00
|
|
|
|
|
|
|
|
match version_str {
|
|
|
|
|
Some(version_str) => {
|
|
|
|
|
let version: u32 = version_str.parse().unwrap_or(0);
|
|
|
|
|
if version != DB_VERSION {
|
|
|
|
|
bail!(
|
|
|
|
|
"Database version mismatch. Expected {}, got {}",
|
|
|
|
|
DB_VERSION,
|
|
|
|
|
version
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
bail!(
|
|
|
|
|
"Database version not found. Expected version {}",
|
|
|
|
|
DB_VERSION
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Set database version
|
2026-02-17 17:47:43 +01:00
|
|
|
pub async fn set_db_version(pool: &SqlitePool) {
|
|
|
|
|
config_set(pool, DB_VERSION_KEY, &DB_VERSION.to_string()).await;
|
2025-08-15 18:07:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
#[tokio::test]
|
|
|
|
|
async fn test_db_version_functionality() {
|
|
|
|
|
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
|
2025-08-15 18:07:15 +02:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
create_table(&pool).await;
|
|
|
|
|
set_db_version(&pool).await;
|
2025-08-15 18:07:15 +02:00
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
let version_str = config_get(&pool, DB_VERSION_KEY).await.unwrap().unwrap();
|
2025-08-15 18:07:15 +02:00
|
|
|
let version: u32 = version_str.parse().unwrap();
|
|
|
|
|
assert_eq!(version, DB_VERSION);
|
|
|
|
|
|
2026-02-17 17:47:43 +01:00
|
|
|
check_db_version(&pool).await.unwrap();
|
2025-08-15 18:07:15 +02:00
|
|
|
|
|
|
|
|
// Test with wrong version
|
2026-02-17 17:47:43 +01:00
|
|
|
sqlx::query("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)")
|
|
|
|
|
.bind(DB_VERSION_KEY)
|
|
|
|
|
.bind("1")
|
|
|
|
|
.execute(&pool)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
let result = check_db_version(&pool).await;
|
2025-08-15 18:07:15 +02:00
|
|
|
assert!(result.is_err());
|
|
|
|
|
assert!(result
|
|
|
|
|
.unwrap_err()
|
|
|
|
|
.to_string()
|
|
|
|
|
.contains("Database version mismatch"));
|
|
|
|
|
|
|
|
|
|
// Test with missing version
|
2026-02-17 17:47:43 +01:00
|
|
|
sqlx::query("DELETE FROM config WHERE key = ?")
|
|
|
|
|
.bind(DB_VERSION_KEY)
|
|
|
|
|
.execute(&pool)
|
|
|
|
|
.await
|
2025-08-15 18:07:15 +02:00
|
|
|
.unwrap();
|
2026-02-17 17:47:43 +01:00
|
|
|
let result = check_db_version(&pool).await;
|
2025-08-15 18:07:15 +02:00
|
|
|
assert!(result.is_err());
|
|
|
|
|
assert!(result
|
|
|
|
|
.unwrap_err()
|
|
|
|
|
.to_string()
|
|
|
|
|
.contains("Database version not found"));
|
|
|
|
|
}
|
|
|
|
|
}
|