feat: database migration system

This commit is contained in:
PandaDEV 2024-12-15 18:50:17 +10:00
parent 2652f26271
commit 8f0d53d355
No known key found for this signature in database
GPG key ID: 13EFF9BAF70EE75C
3 changed files with 86 additions and 15 deletions

View file

@ -2,9 +2,15 @@ use sqlx::sqlite::{SqlitePool, SqlitePoolOptions};
use std::fs; use std::fs;
use tauri::Manager; use tauri::Manager;
use tokio::runtime::Runtime as TokioRuntime; use tokio::runtime::Runtime as TokioRuntime;
use include_dir::{include_dir, Dir};
static MIGRATIONS_DIR: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/db/migrations");
pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> { pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let rt = TokioRuntime::new().expect("Failed to create Tokio runtime"); let rt = TokioRuntime::new().expect("Failed to create Tokio runtime");
app.manage(rt);
let rt = app.state::<TokioRuntime>();
let app_data_dir = app.path().app_data_dir().unwrap(); let app_data_dir = app.path().app_data_dir().unwrap();
fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory"); fs::create_dir_all(&app_data_dir).expect("Failed to create app data directory");
@ -24,8 +30,10 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
.expect("Failed to create pool") .expect("Failed to create pool")
}); });
app.manage(pool.clone());
rt.block_on(async { rt.block_on(async {
apply_schema(&pool).await?; apply_migrations(&pool).await?;
if is_new_db { if is_new_db {
if let Err(e) = super::history::initialize_history(&pool).await { if let Err(e) = super::history::initialize_history(&pool).await {
eprintln!("Failed to initialize history: {}", e); eprintln!("Failed to initialize history: {}", e);
@ -37,26 +45,75 @@ pub fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
Ok::<(), Box<dyn std::error::Error>>(()) Ok::<(), Box<dyn std::error::Error>>(())
})?; })?;
app.manage(pool);
app.manage(rt);
Ok(()) Ok(())
} }
async fn apply_schema(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> { async fn apply_migrations(pool: &SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
let schema = include_str!("scheme.sql"); println!("Starting migration process");
let statements: Vec<&str> = schema // Create schema_version table
.split(';') sqlx::query(
.map(|s| s.trim()) "CREATE TABLE IF NOT EXISTS schema_version (
.filter(|s| !s.is_empty()) version INTEGER PRIMARY KEY,
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
);"
)
.execute(pool)
.await?;
let current_version: Option<i64> = sqlx::query_scalar(
"SELECT MAX(version) FROM schema_version"
)
.fetch_one(pool)
.await?;
let current_version = current_version.unwrap_or(0);
println!("Current database version: {}", current_version);
let mut migration_files: Vec<(i64, &str)> = MIGRATIONS_DIR
.files()
.filter_map(|file| {
let file_name = file.path().file_name()?.to_str()?;
println!("Processing file: {}", file_name);
if file_name.ends_with(".sql") && file_name.starts_with("migration") {
let version: i64 = file_name
.trim_start_matches("migration")
.trim_end_matches(".sql")
.parse()
.ok()?;
println!("Found migration version: {}", version);
Some((version, file.contents_utf8()?))
} else {
None
}
})
.collect(); .collect();
for statement in statements { migration_files.sort_by_key(|(version, _)| *version);
sqlx::query(statement)
.execute(pool) for (version, content) in migration_files {
.await if version > current_version {
.map_err(|e| format!("Failed to execute schema statement: {}", e))?; println!("Applying migration {}", version);
let statements: Vec<&str> = content
.split(';')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
for statement in statements {
println!("Executing statement: {}", statement);
sqlx::query(statement)
.execute(pool)
.await
.map_err(|e| format!("Failed to execute migration {}: {}", version, e))?;
}
sqlx::query("INSERT INTO schema_version (version) VALUES (?)")
.bind(version)
.execute(pool)
.await?;
}
} }
Ok(()) Ok(())

View file

@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
content_type TEXT NOT NULL,
content TEXT NOT NULL,
favicon TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);

View file

@ -0,0 +1,2 @@
ALTER TABLE history ADD COLUMN source TEXT DEFAULT 'System' NOT NULL;
ALTER TABLE history ADD COLUMN source_icon TEXT;