KeiSeiKit-1.0/_primitives/_rust/kei-migrate/src/db.rs
Parfii-bot df857923d4 feat(primitives): kei-migrate Rust universal migration runner
Single binary, three backends (Postgres/SQLite/MySQL) autodetected
from DATABASE_URL scheme. Sequential .sql migrations tracked in
_kei_migrations with SHA-256 checksums.

Commands:
  kei-migrate up              — apply pending
  kei-migrate down [n]        — revert last N (requires .down.sql)
  kei-migrate status          — list applied vs pending
  kei-migrate create <name>   — scaffold up+down pair with UTC ts

Constructor Pattern: 10 source files, all <90 LOC, functions <30 LOC.
Deps: sqlx 0.8 (any+postgres+sqlite+mysql, rustls), clap 4, chrono,
sha2, anyhow, tokio.

Tests: 9/9 passing (cargo test, SQLite backend).
Clippy clean: cargo clippy --all-targets -- -D warnings.

Safety features:
- checksum drift detection on applied migrations
- IRREVERSIBLE marker blocks down-revert
- duplicate version detection at scan time
- each migration in its own transaction

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 20:35:29 +08:00

68 lines
2.2 KiB
Rust

//! Database backend detection + pool construction.
//!
//! Uses `sqlx::Any` so one binary covers Postgres / SQLite / MySQL.
//! Detection is purely on URL scheme — no live probe needed.
use anyhow::{bail, Result};
use sqlx::any::{install_default_drivers, AnyPoolOptions};
use sqlx::AnyPool;
/// Backend inferred from the URL scheme. Determines dialect quirks.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Backend {
Postgres,
Sqlite,
Mysql,
}
impl Backend {
/// Backend-specific CREATE TABLE for `_kei_migrations`.
pub fn create_tracker_sql(self) -> &'static str {
match self {
Backend::Postgres | Backend::Mysql => {
"CREATE TABLE IF NOT EXISTS _kei_migrations (
version BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
checksum CHAR(64) NOT NULL,
applied_at VARCHAR(32) NOT NULL
)"
}
Backend::Sqlite => {
"CREATE TABLE IF NOT EXISTS _kei_migrations (
version INTEGER PRIMARY KEY,
name TEXT NOT NULL,
checksum TEXT NOT NULL,
applied_at TEXT NOT NULL
)"
}
}
}
}
/// Parse a database URL into a [`Backend`]. Never touches the network.
pub fn detect_backend(url: &str) -> Result<Backend> {
let lower = url.to_ascii_lowercase();
if lower.starts_with("postgres://") || lower.starts_with("postgresql://") {
Ok(Backend::Postgres)
} else if lower.starts_with("sqlite:") {
Ok(Backend::Sqlite)
} else if lower.starts_with("mysql://") || lower.starts_with("mariadb://") {
Ok(Backend::Mysql)
} else {
bail!(
"unsupported or unrecognised DATABASE_URL scheme: {}. \
Expected postgres://, sqlite:, or mysql://",
url
)
}
}
/// Build a sqlx `AnyPool` for the given URL (max 4 conns — migration runner is not a server).
pub async fn connect(url: &str) -> Result<AnyPool> {
install_default_drivers();
let pool = AnyPoolOptions::new()
.max_connections(4)
.connect(url)
.await?;
Ok(pool)
}