#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use tokio::sync::{Mutex, OnceCell};
use umbral::orm::write::WriteError;
static SERIALISE: Mutex<()> = Mutex::const_new(());
#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize, umbral::orm::Model)]
#[umbral(table = "atomic_post")]
pub struct Post {
pub id: i64,
#[umbral(unique)]
pub slug: String,
pub title: String,
}
static BOOT: OnceCell<()> = OnceCell::const_new();
async fn boot() {
BOOT.get_or_init(|| async {
let settings = umbral::Settings::from_env().expect("figment defaults");
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("atomic_terminals.sqlite");
std::mem::forget(tmp);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(
SqliteConnectOptions::new()
.filename(&path)
.create_if_missing(true),
)
.await
.expect("pool");
let _ = umbral::App::builder()
.settings(settings)
.database("default", pool)
.atomic_transactions(true)
.model::<Post>()
.build()
.expect("App::build");
let pool = umbral::db::pool();
sqlx::query(
"CREATE TABLE atomic_post (\
id INTEGER PRIMARY KEY AUTOINCREMENT,\
slug TEXT NOT NULL UNIQUE,\
title TEXT NOT NULL\
)",
)
.execute(&pool)
.await
.expect("CREATE TABLE");
})
.await;
}
async fn truncate() {
let pool = umbral::db::pool();
sqlx::query("DELETE FROM atomic_post")
.execute(&pool)
.await
.expect("truncate");
}
#[tokio::test]
async fn atomic_create_commits_on_ok() {
let _g = SERIALISE.lock().await;
boot().await;
truncate().await;
let row = Post::objects()
.atomic()
.create(Post {
id: 0,
slug: "atomic-1".into(),
title: "t1".into(),
})
.await
.expect("atomic create");
assert!(row.id > 0);
let count = Post::objects().count().await.expect("count");
assert_eq!(count, 1, "committed row visible to follow-up SELECT");
}
#[tokio::test]
async fn atomic_create_rolls_back_on_unique_violation() {
let _g = SERIALISE.lock().await;
boot().await;
truncate().await;
Post::objects()
.create(Post {
id: 0,
slug: "dup".into(),
title: "first".into(),
})
.await
.expect("seed");
let err = Post::objects()
.atomic()
.create(Post {
id: 0,
slug: "dup".into(),
title: "second".into(),
})
.await
.unwrap_err();
assert!(
matches!(
err,
WriteError::UniqueViolation { .. } | WriteError::Sqlx(_)
),
"unique conflict surfaced; got: {err:?}"
);
let count = Post::objects().count().await.expect("count");
assert_eq!(count, 1, "second insert rolled back");
}
#[tokio::test]
async fn atomic_bulk_create_produces_correct_ids() {
let _g = SERIALISE.lock().await;
boot().await;
truncate().await;
let n = Post::objects()
.atomic()
.bulk_create(vec![
Post {
id: 0,
slug: "b1".into(),
title: "t".into(),
},
Post {
id: 0,
slug: "b2".into(),
title: "t".into(),
},
Post {
id: 0,
slug: "b3".into(),
title: "t".into(),
},
])
.await
.expect("atomic bulk_create");
assert_eq!(n, 3);
let count = Post::objects().count().await.expect("count");
assert_eq!(count, 3);
}
#[tokio::test]
async fn non_atomic_overrides_global_default() {
let _g = SERIALISE.lock().await;
boot().await;
truncate().await;
let row = Post::objects()
.non_atomic()
.create(Post {
id: 0,
slug: "non-atomic-1".into(),
title: "t".into(),
})
.await
.expect("non-atomic create");
assert!(row.id > 0);
}
#[tokio::test]
async fn implicit_atomic_via_global_default() {
let _g = SERIALISE.lock().await;
boot().await;
truncate().await;
let row = Post::objects()
.create(Post {
id: 0,
slug: "default-atomic".into(),
title: "t".into(),
})
.await
.expect("create under global atomic default");
assert!(row.id > 0);
}
#[tokio::test]
async fn atomic_update_values_succeeds() {
let _g = SERIALISE.lock().await;
boot().await;
truncate().await;
let row = Post::objects()
.create(Post {
id: 0,
slug: "u1".into(),
title: "old".into(),
})
.await
.expect("seed");
let mut update = serde_json::Map::new();
update.insert("title".into(), serde_json::json!("new"));
let n = Post::objects()
.filter(post::ID.eq(row.id))
.atomic()
.update_values(update)
.await
.expect("atomic update_values");
assert_eq!(n, 1);
let fresh = Post::objects()
.get(post::ID.eq(row.id))
.await
.expect("re-fetch");
assert_eq!(fresh.title, "new");
}
#[tokio::test]
async fn atomic_delete_succeeds() {
let _g = SERIALISE.lock().await;
boot().await;
truncate().await;
let row = Post::objects()
.create(Post {
id: 0,
slug: "d1".into(),
title: "doomed".into(),
})
.await
.expect("seed");
let n = Post::objects()
.filter(post::ID.eq(row.id))
.atomic()
.delete()
.await
.expect("atomic delete");
assert_eq!(n, 1);
}