// Copyright (C) 2024-2026 Whiterun LLC // // 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 use anyhow::{bail, Result}; use sqlx::{Sqlite, SqlitePool}; pub const DB_VERSION: u32 = 5; const DB_VERSION_KEY: &str = "db_version"; /// Create the config table pub async fn create_table(pool: &SqlitePool) { sqlx::query( "CREATE TABLE config ( key TEXT PRIMARY KEY, value TEXT )", ) .execute(pool) .await .expect("failed to create config table"); } /// 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 .unwrap(); } /// Get config entry pub async fn config_get(pool: &SqlitePool, key: &str) -> Result> { 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)) } /// Check database version and panic if it doesn't match expected version pub async fn check_db_version(pool: &SqlitePool) -> Result<()> { let version_str = config_get(pool, DB_VERSION_KEY).await?; 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 pub async fn set_db_version(pool: &SqlitePool) { config_set(pool, DB_VERSION_KEY, &DB_VERSION.to_string()).await; } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_db_version_functionality() { let pool = SqlitePool::connect("sqlite::memory:").await.unwrap(); create_table(&pool).await; set_db_version(&pool).await; let version_str = config_get(&pool, DB_VERSION_KEY).await.unwrap().unwrap(); let version: u32 = version_str.parse().unwrap(); assert_eq!(version, DB_VERSION); check_db_version(&pool).await.unwrap(); // Test with wrong version 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; assert!(result.is_err()); assert!(result .unwrap_err() .to_string() .contains("Database version mismatch")); // Test with missing version sqlx::query("DELETE FROM config WHERE key = ?") .bind(DB_VERSION_KEY) .execute(&pool) .await .unwrap(); let result = check_db_version(&pool).await; assert!(result.is_err()); assert!(result .unwrap_err() .to_string() .contains("Database version not found")); } }