#![allow(dead_code)]
use std::collections::HashMap;
use tokio::sync::OnceCell;
use umbral::forms::FormValidate;
use umbral::orm::ForeignKey;
use umbral_core::db;
#[derive(Debug, Clone, sqlx::FromRow, serde::Serialize, serde::Deserialize, umbral::orm::Model)]
#[umbral(table = "ffk_author")]
pub struct Author {
pub id: i64,
pub name: String,
}
#[derive(
Debug,
Clone,
Default,
sqlx::FromRow,
serde::Serialize,
serde::Deserialize,
umbral::orm::Model,
umbral::forms::Form,
)]
#[umbral(table = "ffk_book")]
pub struct Book {
#[umbral(primary_key)]
pub id: i64,
#[form(required, length(min = 1, max = 200))]
pub title: String,
pub author: ForeignKey<Author>,
}
fn data(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect()
}
static BOOT: OnceCell<()> = OnceCell::const_new();
async fn boot() {
BOOT.get_or_init(|| async {
let settings = umbral::Settings::from_env().expect("figment defaults");
let pool = db::connect_sqlite("sqlite::memory:").await.expect("sqlite");
umbral::App::builder()
.settings(settings)
.database("default", pool.clone())
.model::<Author>()
.model::<Book>()
.model::<Passport>()
.build()
.expect("App::build");
sqlx::query("CREATE TABLE ffk_author (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)")
.execute(&pool).await.expect("create author");
sqlx::query("CREATE TABLE ffk_book (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author INTEGER NOT NULL REFERENCES ffk_author(id))")
.execute(&pool).await.expect("create book");
sqlx::query("INSERT INTO ffk_author (name) VALUES ('Ada')")
.execute(&pool).await.expect("seed author");
})
.await;
}
#[tokio::test]
async fn fk_field_parses_and_links_real_parent() {
boot().await;
let book = Book::validate(&data(&[("title", "Notes"), ("author", "1")]))
.await
.expect("valid FK");
assert_eq!(book.author.id(), 1);
let created = Book::objects().create(book).await.expect("create book");
let parent = created
.author
.resolve(&db::pool())
.await
.expect("resolve parent");
assert_eq!(
parent.name, "Ada",
"FK resolves to the actual seeded parent"
);
}
#[derive(
Debug,
Clone,
Default,
sqlx::FromRow,
serde::Serialize,
serde::Deserialize,
umbral::orm::Model,
umbral::forms::Form,
)]
#[umbral(table = "ffk_passport")]
pub struct Passport {
#[umbral(primary_key)]
pub id: i64,
#[umbral(unique)]
pub holder: ForeignKey<Author>,
#[form(required, length(min = 1, max = 40))]
pub number: String,
}
#[tokio::test]
async fn fk_field_renders_select_with_seeded_options() {
boot().await;
let html = Book::render_html(&data(&[])).await;
assert!(
html.contains("<select name=\"author\""),
"renders a select: {html}"
);
assert!(
html.contains("value=\"1\""),
"seeded author id is an option: {html}"
);
assert!(
html.contains("Ada"),
"label is the parent's text column: {html}"
);
}
#[tokio::test]
async fn fk_field_rejects_nonexistent_parent_and_inserts_no_row() {
boot().await;
let err = Book::validate(&data(&[
("title", "Ghost-unique-title"),
("author", "9999"),
]))
.await
.expect_err("nonexistent FK rejected");
assert!(
err.fields.contains_key("author"),
"error keyed to the FK field"
);
let count = Book::objects()
.filter(book::TITLE.eq("Ghost-unique-title"))
.count()
.await
.expect("count by title");
assert_eq!(count, 0, "no row inserted on a bad FK");
}
#[tokio::test]
async fn forward_o2o_unique_violation_surfaces_as_write_error() {
boot().await;
sqlx::query("CREATE TABLE IF NOT EXISTS ffk_passport (id INTEGER PRIMARY KEY AUTOINCREMENT, holder INTEGER NOT NULL UNIQUE REFERENCES ffk_author(id), number TEXT NOT NULL)")
.execute(&db::pool()).await.expect("create passport");
let p1 = Passport::validate(&data(&[("holder", "1"), ("number", "A1")]))
.await
.expect("valid o2o");
Passport::objects().create(p1).await.expect("first o2o row");
let p2 = Passport::validate(&data(&[("holder", "1"), ("number", "B2")]))
.await
.expect("validates (existence ok); UNIQUE fires at insert");
let err = Passport::objects()
.create(p2)
.await
.expect_err("duplicate target");
assert!(
matches!(
err,
umbral::orm::write::WriteError::UniqueViolation { .. }
| umbral::orm::write::WriteError::Multiple { .. }
| umbral::orm::write::WriteError::Sqlx(_)
),
"duplicate forward-O2O surfaces a WriteError: {err:?}"
);
}