use std::{path::PathBuf, sync::Arc};
use okerr::{Context, Result, ensure};
use sqlx::{
Row, SqlitePool,
sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions},
};
use crate::{MetaStatus, Metadata, init_meta_datetimes_if_empty, meta_loaded};
#[cfg(feature = "sqlx")]
pub async fn connect_to_sqlite(db_path: PathBuf) -> Result<SqlitePool> {
let db_path_name = db_path.display();
let options = SqliteConnectOptions::new()
.filename(&db_path)
.create_if_missing(true)
.journal_mode(SqliteJournalMode::Wal)
.pragma("foreign_keys", "ON");
let pool = SqlitePoolOptions::new()
.max_connections(10)
.min_connections(1)
.connect_with(options)
.await
.with_context(|| format!("failed to connect to database at {}", db_path_name))?;
let row: (i64,) = sqlx::query_as("PRAGMA foreign_keys;")
.fetch_one(&pool)
.await
.context("find foreign_keys pragma failed")?;
ensure!(row.0 == 1, "foreign_keys pragma must be set to 1");
let row: (String,) = sqlx::query_as("PRAGMA journal_mode;")
.fetch_one(&pool)
.await
.context("find journal_mode pragma failed")?;
ensure!(row.0 == "wal", "journal_mode pragma must be set to WAL");
Ok(pool)
}
#[cfg(feature = "sqlx")]
#[derive(Debug, Clone)]
pub struct SqliteStorage {
pub pool: Arc<SqlitePool>,
pub table_name: String,
}
#[cfg(feature = "sqlx")]
impl SqliteStorage {
pub fn new(pool: Arc<SqlitePool>) -> Self {
Self {
pool,
table_name: "_migratex_metadata".to_string(),
}
}
pub fn with_table_name(mut self, name: impl Into<String>) -> Self {
self.table_name = name.into();
self
}
}
#[cfg(feature = "sqlx")]
#[derive(Debug, Clone)]
pub struct SqliteMetadata {
pub version: i32,
pub app_version: String,
pub status: MetaStatus,
pub created_at: String,
pub updated_at: String,
}
#[cfg(feature = "sqlx")]
impl Default for SqliteMetadata {
fn default() -> Self {
Self {
version: 0,
app_version: String::new(),
status: MetaStatus::Clean,
created_at: String::new(),
updated_at: String::new(),
}
}
}
#[cfg(feature = "sqlx")]
impl SqliteMetadata {
pub async fn load_or_init(storage: &SqliteStorage) -> Result<Self> {
Self::ensure_table(storage).await?;
if let Some(meta) = Self::load_from_db(storage).await? {
meta_loaded(meta)
} else {
Self::init_new(storage).await
}
}
pub async fn save(&self, storage: &SqliteStorage) -> Result<()> {
Self::ensure_table(storage).await?;
sqlx::query(&format!(
"INSERT INTO {} (id, version, status, app_version, created_at, updated_at)
VALUES (1, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
version = excluded.version,
status = excluded.status,
app_version = excluded.app_version,
updated_at = excluded.updated_at",
storage.table_name
))
.bind(self.version)
.bind(self.to_status_str())
.bind(&self.app_version)
.bind(&self.created_at)
.bind(&self.updated_at)
.execute(&*storage.pool)
.await?;
Ok(())
}
async fn ensure_table(storage: &SqliteStorage) -> Result<()> {
sqlx::query(&format!(
"CREATE TABLE IF NOT EXISTS {} (
id INTEGER PRIMARY KEY CHECK (id = 1),
version INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT 'Clean',
app_version TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)",
storage.table_name
))
.execute(&*storage.pool)
.await?;
Ok(())
}
async fn load_from_db(storage: &SqliteStorage) -> Result<Option<Self>> {
let row = sqlx::query(&format!(
"SELECT version, status, app_version, created_at, updated_at
FROM {} WHERE id = 1",
storage.table_name
))
.fetch_optional(&*storage.pool)
.await?;
if let Some(row) = row {
let status_str: String = row.try_get("status")?;
let status = match status_str.as_str() {
"Migrating" => MetaStatus::Migrating,
"Failed" => MetaStatus::Failed,
_ => MetaStatus::Clean,
};
Ok(Some(Self {
version: row.try_get("version")?,
app_version: row.try_get("app_version")?,
status,
created_at: row.try_get("created_at")?,
updated_at: row.try_get("updated_at")?,
}))
} else {
Ok(None)
}
}
async fn init_new(storage: &SqliteStorage) -> Result<Self> {
let mut meta = Self::default();
meta.set_version(0);
meta.set_status(MetaStatus::Clean);
meta.set_app_version(env!("CARGO_PKG_VERSION").to_string());
init_meta_datetimes_if_empty(&mut meta);
meta.save(storage).await?;
Ok(meta)
}
}
#[cfg(feature = "sqlx")]
impl Metadata for SqliteMetadata {
crate::metadata_accessors!();
}