migrated migration logic from rusqlite to sqlx and updated relevant async methods for better database interaction

This commit is contained in:
2025-08-05 04:18:42 +02:00
parent 86b5f83140
commit 59b19a22ff
4 changed files with 2025 additions and 184 deletions

1955
backend-rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,5 +6,7 @@ edition = "2024"
[dependencies]
anyhow = "1.0"
tokio = { version = "1", features = ["full"] }
tokio-rusqlite = "0.6.0"
rusqlite = "=0.32.0"
axum = "0.8.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "sqlite", "macros", "migrate", "chrono", "json"] }

View File

@@ -1,13 +1,16 @@
use anyhow::Result;
use std::path::{Path};
use tokio_rusqlite::Connection as AsyncConn;
use crate::migrations::Migrator;
use anyhow::Result;
use std::path::Path;
use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection};
use sqlx::Connection;
use std::str::FromStr;
pub async fn initialize_db(db_path: &Path, migrations_dir: &Path) -> Result<AsyncConn> {
let conn = AsyncConn::open(db_path).await?;
pub async fn initialize_db(db_path: &Path, migrations_dir: &Path) -> Result<SqliteConnection> {
let options = SqliteConnectOptions::from_str(&format!("sqlite:{}", db_path.display()))?.create_if_missing(true);
let mut conn = sqlx::SqliteConnection::connect_with(&options).await?;
let migrator = Migrator::new(migrations_dir.to_path_buf())?;
migrator.migrate_up_async(&conn).await?;
migrator.migrate_up_async(&mut conn).await?;
Ok(conn)
}

View File

@@ -1,10 +1,9 @@
use anyhow::{Context, Result};
use rusqlite::{Connection, params};
use sqlx::sqlite::SqliteConnection;
use std::collections::HashSet;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::fs;
use tokio_rusqlite::Connection as AsyncConn;
pub struct Migration {
pub version: i64,
@@ -24,33 +23,40 @@ impl Migrator {
Ok(Migrator { migrations_dir })
}
fn initialize(&self, conn: &mut Connection) -> Result<()> {
let tx = conn
.transaction()
async fn initialize(&self, conn: &mut SqliteConnection) -> Result<()> {
let mut tx = sqlx::Connection::begin(conn)
.await
.context("Failed to start transaction for initialization")?;
tx.execute(
"CREATE TABLE IF NOT EXISTS migrations (version INTEGER PRIMARY KEY)",
[],
)
.context("Failed to create migrations table")?;
sqlx::query("CREATE TABLE IF NOT EXISTS migrations (version INTEGER PRIMARY KEY)")
.execute(&mut *tx)
.await
.context("Failed to create migrations table")?;
let columns: HashSet<String> = {
let mut stmt = tx.prepare("PRAGMA table_info(migrations)")?;
stmt.query_map([], |row| row.get(1))?
.collect::<Result<HashSet<String>, _>>()?
let rows: Vec<(i32, String, String, i32, Option<String>, i32)> =
sqlx::query_as("PRAGMA table_info(migrations)")
.fetch_all(&mut *tx)
.await
.context("Failed to get migrations table info")?;
rows.into_iter().map(|row| row.1).collect()
};
if !columns.contains("name") {
tx.execute("ALTER TABLE migrations ADD COLUMN name TEXT", [])
sqlx::query("ALTER TABLE migrations ADD COLUMN name TEXT")
.execute(&mut *tx)
.await
.context("Failed to add 'name' column to migrations table")?;
}
if !columns.contains("applied_at") {
tx.execute("ALTER TABLE migrations ADD COLUMN applied_at INTEGER", [])
sqlx::query("ALTER TABLE migrations ADD COLUMN applied_at INTEGER")
.execute(&mut *tx)
.await
.context("Failed to add 'applied_at' column to migrations table")?;
}
tx.commit()
.await
.context("Failed to commit migrations table initialization")?;
Ok(())
}
@@ -100,70 +106,60 @@ impl Migrator {
Ok(migrations)
}
pub fn get_applied_migrations(&self, conn: &mut Connection) -> Result<HashSet<i64>> {
let mut stmt = conn
.prepare("SELECT version FROM migrations ORDER BY version")
.context("Failed to prepare query for applied migrations")?;
let versions = stmt
.query_map([], |row| row.get(0))?
.collect::<Result<HashSet<i64>, _>>()?;
pub async fn get_applied_migrations<'a, E>(&self, executor: E) -> Result<HashSet<i64>>
where
E: sqlx::Executor<'a, Database = sqlx::Sqlite>,
{
let versions =
sqlx::query_as::<_, (i64,)>("SELECT version FROM migrations ORDER BY version")
.fetch_all(executor)
.await
.context("Failed to get applied migrations")?
.into_iter()
.map(|row| row.0)
.collect();
Ok(versions)
}
pub async fn migrate_up_async(&self, async_conn: &AsyncConn) -> Result<()> {
pub async fn migrate_up_async(&self, conn: &mut SqliteConnection) -> Result<()> {
let migrations = self.load_migrations_async().await?;
let migrator = self.clone();
// Perform all database operations within a blocking-safe context
async_conn
.call(move |conn| {
migrator.initialize(conn).expect("TODO: panic message");
let applied = migrator
.get_applied_migrations(conn)
.expect("TODO: panic message");
self.initialize(conn).await?;
let applied = self.get_applied_migrations(&mut *conn).await?;
let tx = conn
.transaction()
.context("Failed to start transaction for migrations")
.expect("TODO: panic message");
let mut tx = sqlx::Connection::begin(conn)
.await
.context("Failed to start transaction for migrations")?;
for migration in migrations {
if !applied.contains(&migration.version) {
println!(
"Applying migration {}: {}",
for migration in migrations {
if !applied.contains(&migration.version) {
println!(
"Applying migration {}: {}",
migration.version, migration.name
);
sqlx::query(&migration.sql_up)
.execute(&mut *tx)
.await
.with_context(|| {
format!(
"Failed to apply migration {}: {}",
migration.version, migration.name
);
tx.execute_batch(&migration.sql_up)
.with_context(|| {
format!(
"Failed to execute migration {}: {}",
migration.version, migration.name
)
})
.expect("TODO: panic message");
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("TODO: panic message")
.as_secs() as i64;
tx.execute(
"INSERT INTO migrations (version, name, applied_at) VALUES (?, ?, ?)",
params![migration.version, migration.name, now],
)
.with_context(|| {
format!("Failed to record migration {}", migration.version)
})
.expect("TODO: panic message");
}
}
})?;
tx.commit()
.context("Failed to commit migrations")
.expect("TODO: panic message");
Ok(())
})
.await?;
let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
sqlx::query("INSERT INTO migrations (version, name, applied_at) VALUES (?, ?, ?)")
.bind(migration.version)
.bind(&migration.name.clone())
.bind(now)
.execute(&mut *tx)
.await
.with_context(|| format!("Failed to record migration {}", migration.version))?;
}
}
tx.commit().await.context("Failed to commit migrations")?;
Ok(())
}
@@ -171,74 +167,61 @@ impl Migrator {
#[allow(dead_code)]
pub async fn migrate_down_async(
&self,
async_conn: &AsyncConn,
conn: &mut SqliteConnection,
target_version: Option<i64>,
) -> Result<()> {
let migrations = self.load_migrations_async().await?;
let migrator = self.clone();
// Perform all database operations within a blocking-safe context
async_conn
.call(move |conn| {
migrator.initialize(conn).expect("TODO: panic message");
let applied = migrator
.get_applied_migrations(conn)
.expect("TODO: panic message");
self.initialize(conn).await?;
let applied = self.get_applied_migrations(&mut *conn).await?;
// If no target specified, roll back only the latest migration
let max_applied = *applied.iter().max().unwrap_or(&0);
let target =
target_version.unwrap_or(if max_applied > 0 { max_applied - 1 } else { 0 });
// If no target specified, roll back only the latest migration
let max_applied = *applied.iter().max().unwrap_or(&0);
let target = target_version.unwrap_or(if max_applied > 0 { max_applied - 1 } else { 0 });
let tx = conn
.transaction()
.context("Failed to start transaction for migrations")
.expect("TODO: panic message");
let mut tx = sqlx::Connection::begin(conn)
.await
.context("Failed to start transaction for migrations")?;
// Find migrations to roll back (in reverse order)
let mut to_rollback: Vec<&Migration> = migrations
.iter()
.filter(|m| applied.contains(&m.version) && m.version > target)
.collect();
// Find migrations to roll back (in reverse order)
let mut to_rollback: Vec<&Migration> = migrations
.iter()
.filter(|m| applied.contains(&m.version) && m.version > target)
.collect();
to_rollback.sort_by_key(|m| std::cmp::Reverse(m.version));
to_rollback.sort_by_key(|m| std::cmp::Reverse(m.version));
for migration in to_rollback {
println!(
"Rolling back migration {}: {}",
migration.version, migration.name
);
for migration in to_rollback {
println!(
"Rolling back migration {}: {}",
migration.version, migration.name
);
if !migration.sql_down.is_empty() {
tx.execute_batch(&migration.sql_down)
.with_context(|| {
format!(
"Failed to rollback migration {}: {}",
migration.version, migration.name
)
})
.expect("TODO: panic message");
} else {
println!("Warning: No down migration defined for {}", migration.name);
}
// Remove the migration record
tx.execute(
"DELETE FROM migrations WHERE version = ?",
[&migration.version],
)
if !migration.sql_down.is_empty() {
sqlx::query(&migration.sql_down)
.execute(&mut *tx)
.await
.with_context(|| {
format!("Failed to remove migration record {}", migration.version)
})
.expect("TODO: panic message");
}
format!(
"Failed to rollback migration {}: {}",
migration.version, migration.name
)
})?;
} else {
println!("Warning: No down migration defined for {}", migration.name);
}
tx.commit()
.context("Failed to commit rollback")
.expect("TODO: panic message");
Ok(())
})
.await?;
// Remove the migration record
sqlx::query("DELETE FROM migrations WHERE version = ?")
.bind(migration.version)
.execute(&mut *tx)
.await
.with_context(|| {
format!("Failed to remove migration record {}", migration.version)
})?;
}
tx.commit().await.context("Failed to commit rollback")?;
Ok(())
}