#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
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 = "writes_post")]
pub struct Post {
pub id: i64,
pub title: String,
pub body: String,
pub published: bool,
}
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("model_writes.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 _app = umbral::App::builder()
.settings(settings)
.database("default", pool)
.model::<Post>()
.build()
.expect("App::build");
let pool = umbral::db::pool();
sqlx::query(
"CREATE TABLE writes_post (\
id INTEGER PRIMARY KEY AUTOINCREMENT,\
title TEXT NOT NULL,\
body TEXT NOT NULL,\
published BOOLEAN NOT NULL DEFAULT 0\
)",
)
.execute(&pool)
.await
.expect("create writes_post table");
})
.await;
}
async fn truncate() {
let pool = umbral::db::pool();
sqlx::query("DELETE FROM writes_post")
.execute(&pool)
.await
.expect("truncate");
}
#[tokio::test]
async fn create_inserts_a_row_and_returns_it_populated() {
let _guard = SERIALISE.lock().await;
boot().await;
truncate().await;
let new = Post {
id: 0, title: "hello".into(),
body: "world".into(),
published: true,
};
let row = Post::objects().create(new).await.expect("create");
assert!(row.id > 0, "autoincrement PK should be populated");
assert_eq!(row.title, "hello");
assert_eq!(row.body, "world");
assert!(row.published);
}
#[tokio::test]
async fn create_with_explicit_pk_respects_it() {
let _guard = SERIALISE.lock().await;
boot().await;
truncate().await;
let new = Post {
id: 999,
title: "explicit".into(),
body: "pk".into(),
published: false,
};
let row = Post::objects().create(new).await.expect("create");
assert_eq!(row.id, 999);
}
#[tokio::test]
async fn create_rejects_missing_required_field_through_required_error() {
let _guard = SERIALISE.lock().await;
boot().await;
truncate().await;
let mut bad: Map<String, Value> = Map::new();
bad.insert("title".into(), json!(null));
let err = Post::objects()
.filter(post::ID.eq(123))
.update_values(bad)
.await
.unwrap_err();
assert!(matches!(err, WriteError::RequiredFieldMissing { .. }));
}
#[tokio::test]
async fn bulk_create_inserts_many_rows() {
let _guard = SERIALISE.lock().await;
boot().await;
truncate().await;
let posts = (1..=5)
.map(|i| Post {
id: 0,
title: format!("title {i}"),
body: format!("body {i}"),
published: i % 2 == 0,
})
.collect::<Vec<_>>();
let n = Post::objects()
.bulk_create(posts)
.await
.expect("bulk_create");
assert_eq!(n, 5);
let count = Post::objects().count().await.expect("count");
assert_eq!(count, 5);
}
#[tokio::test]
async fn bulk_create_empty_input_is_a_noop() {
let _guard = SERIALISE.lock().await;
boot().await;
truncate().await;
let n = Post::objects()
.bulk_create(Vec::<Post>::new())
.await
.expect("bulk_create");
assert_eq!(n, 0);
}
#[tokio::test]
async fn delete_with_filter_removes_matching_rows() {
let _guard = SERIALISE.lock().await;
boot().await;
truncate().await;
for i in 1..=4 {
Post::objects()
.create(Post {
id: 0,
title: format!("t{i}"),
body: format!("b{i}"),
published: i % 2 == 0,
})
.await
.unwrap();
}
let n = Post::objects()
.filter(post::PUBLISHED.eq(false))
.delete()
.await
.expect("delete");
assert_eq!(n, 2);
let remaining = Post::objects().count().await.unwrap();
assert_eq!(remaining, 2);
}
#[tokio::test]
async fn delete_without_filter_removes_every_row() {
let _guard = SERIALISE.lock().await;
boot().await;
truncate().await;
for i in 1..=3 {
Post::objects()
.create(Post {
id: 0,
title: format!("t{i}"),
body: "b".into(),
published: true,
})
.await
.unwrap();
}
let n = Post::objects()
.filter(post::ID.gt(0))
.delete()
.await
.unwrap();
assert_eq!(n, 3);
let remaining = Post::objects().count().await.unwrap();
assert_eq!(remaining, 0);
}
#[tokio::test]
async fn update_values_changes_matching_rows() {
let _guard = SERIALISE.lock().await;
boot().await;
truncate().await;
for i in 1..=3 {
Post::objects()
.create(Post {
id: 0,
title: format!("t{i}"),
body: format!("b{i}"),
published: false,
})
.await
.unwrap();
}
let mut updates: Map<String, Value> = Map::new();
updates.insert("published".into(), json!(true));
let n = Post::objects()
.filter(post::TITLE.eq("t2"))
.update_values(updates)
.await
.expect("update");
assert_eq!(n, 1);
let updated = Post::objects()
.filter(post::PUBLISHED.eq(true))
.fetch()
.await
.unwrap();
assert_eq!(updated.len(), 1);
assert_eq!(updated[0].title, "t2");
}
#[tokio::test]
async fn update_values_silently_skips_pk_to_avoid_corruption() {
let _guard = SERIALISE.lock().await;
boot().await;
truncate().await;
let original = Post::objects()
.create(Post {
id: 0,
title: "keep my pk".into(),
body: "b".into(),
published: false,
})
.await
.unwrap();
let mut bad: Map<String, Value> = Map::new();
bad.insert("id".into(), json!(99999));
bad.insert("title".into(), json!("new title"));
let n = Post::objects()
.filter(post::ID.eq(original.id))
.update_values(bad)
.await
.expect("update");
assert_eq!(n, 1);
let still_there = Post::objects()
.filter(post::ID.eq(original.id))
.first()
.await
.unwrap();
assert!(
still_there.is_some(),
"the row identified by the original PK should still exist"
);
assert_eq!(still_there.unwrap().title, "new title");
}
#[tokio::test]
async fn update_values_unknown_column_errors_loudly() {
let _guard = SERIALISE.lock().await;
boot().await;
truncate().await;
let mut bad: Map<String, Value> = Map::new();
bad.insert("definitely_not_a_column".into(), json!(123));
let err = Post::objects()
.filter(post::ID.eq(1))
.update_values(bad)
.await
.unwrap_err();
assert!(matches!(err, WriteError::UnknownColumn { .. }));
}