#![allow(missing_docs)]
use sqlx::SqlitePool;
const ALREADY_PRESENT_NEEDLES: &[&str] = &["duplicate column name", "already exists"];
pub async fn run_idempotent_alter(pool: &SqlitePool, stmt: &str) -> Result<bool, sqlx::Error> {
match sqlx::query(stmt).execute(pool).await {
Ok(_) => Ok(true),
Err(e) => {
let msg = e.to_string().to_lowercase();
if ALREADY_PRESENT_NEEDLES.iter().any(|n| msg.contains(n)) {
Ok(false)
} else {
Err(e)
}
}
}
}
pub async fn run_idempotent_alters(
pool: &SqlitePool,
stmts: &[&str],
) -> Result<usize, sqlx::Error> {
let mut applied = 0;
for stmt in stmts {
if run_idempotent_alter(pool, stmt).await? {
applied += 1;
}
}
Ok(applied)
}
#[cfg(test)]
mod tests {
use super::*;
use sqlx::sqlite::SqlitePoolOptions;
async fn pool() -> SqlitePool {
let p = SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
sqlx::query("CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT)")
.execute(&p)
.await
.unwrap();
p
}
#[tokio::test]
async fn fresh_alter_returns_applied_true() {
let p = pool().await;
let applied = run_idempotent_alter(&p, "ALTER TABLE t ADD COLUMN b TEXT")
.await
.unwrap();
assert!(applied);
}
#[tokio::test]
async fn duplicate_column_returns_applied_false() {
let p = pool().await;
run_idempotent_alter(&p, "ALTER TABLE t ADD COLUMN b TEXT")
.await
.unwrap();
let applied = run_idempotent_alter(&p, "ALTER TABLE t ADD COLUMN b TEXT")
.await
.unwrap();
assert!(!applied);
}
#[tokio::test]
async fn unrelated_error_bubbles_up() {
let p = pool().await;
let err = run_idempotent_alter(&p, "ALTER TABLE nonexistent ADD COLUMN x TEXT").await;
assert!(err.is_err());
}
#[tokio::test]
async fn batch_counts_only_fresh_applications() {
let p = pool().await;
let applied = run_idempotent_alters(
&p,
&[
"ALTER TABLE t ADD COLUMN b TEXT",
"ALTER TABLE t ADD COLUMN c TEXT",
],
)
.await
.unwrap();
assert_eq!(applied, 2);
let applied = run_idempotent_alters(
&p,
&[
"ALTER TABLE t ADD COLUMN b TEXT",
"ALTER TABLE t ADD COLUMN c TEXT",
],
)
.await
.unwrap();
assert_eq!(applied, 0);
}
#[tokio::test]
async fn batch_stops_at_first_real_error() {
let p = pool().await;
let res = run_idempotent_alters(
&p,
&[
"ALTER TABLE t ADD COLUMN b TEXT",
"ALTER TABLE missing ADD COLUMN x TEXT",
"ALTER TABLE t ADD COLUMN c TEXT",
],
)
.await;
assert!(res.is_err());
let cols = sqlx::query("PRAGMA table_info(t)")
.fetch_all(&p)
.await
.unwrap();
let names: Vec<String> = cols
.iter()
.map(|r| sqlx::Row::try_get::<String, _>(r, 1).unwrap())
.collect();
assert!(!names.contains(&"c".to_string()));
}
#[tokio::test]
async fn case_insensitive_needle_match() {
let p = pool().await;
run_idempotent_alter(&p, "ALTER TABLE t ADD COLUMN b TEXT")
.await
.unwrap();
let applied = run_idempotent_alter(&p, "ALTER TABLE t ADD COLUMN b TEXT")
.await
.unwrap();
assert!(!applied);
}
}