#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use tokio::sync::OnceCell;
use umbral::orm::M2M;
use umbral_core::db;
#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize, umbral::orm::Model)]
#[umbral(table = "pkm2m_course")]
pub struct Course {
#[umbral(primary_key)]
pub code: String,
pub title: String,
#[sqlx(skip)]
#[serde(skip)]
pub students: M2M<Student, String>,
}
#[derive(Debug, Clone, sqlx::FromRow, Serialize, Deserialize, umbral::orm::Model)]
#[umbral(table = "pkm2m_student")]
pub struct Student {
pub id: i64,
pub name: 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 pool = db::connect_sqlite("sqlite::memory:")
.await
.expect("in-memory sqlite");
umbral::App::builder()
.settings(settings)
.database("default", pool.clone())
.model::<Course>()
.model::<Student>()
.build()
.expect("App::build");
for ddl in [
"CREATE TABLE pkm2m_course (code TEXT PRIMARY KEY, title TEXT NOT NULL)",
"CREATE TABLE pkm2m_student (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL)",
"CREATE TABLE pkm2m_course_students (
parent_id TEXT NOT NULL,
child_id INTEGER NOT NULL,
PRIMARY KEY (parent_id, child_id)
)",
] {
sqlx::query(ddl).execute(&pool).await.expect("ddl");
}
for (code, title) in &[("rust101", "Intro to Rust"), ("go101", "Intro to Go")] {
sqlx::query("INSERT INTO pkm2m_course (code, title) VALUES (?, ?)")
.bind(*code)
.bind(*title)
.execute(&pool)
.await
.expect("seed course");
}
for name in &["alice", "bob", "carol"] {
sqlx::query("INSERT INTO pkm2m_student (name) VALUES (?)")
.bind(*name)
.execute(&pool)
.await
.expect("seed student");
}
})
.await;
}
#[tokio::test]
async fn m2m_add_and_prefetch_on_a_string_pk_parent() {
boot().await;
let students = Student::objects().fetch().await.expect("students");
let by_name = |n: &str| students.iter().find(|s| s.name == n).unwrap().clone();
let alice = by_name("alice");
let bob = by_name("bob");
let rust = Course::objects()
.filter(course::CODE.eq("rust101"))
.first()
.await
.expect("query")
.expect("rust101 present");
rust.students.add(&alice).await.expect("add alice");
rust.students.add(&bob).await.expect("add bob");
let courses = Course::objects()
.prefetch_related("students")
.fetch()
.await
.expect("prefetch");
let rust = courses.iter().find(|c| c.code == "rust101").unwrap();
let mut names: Vec<&str> = rust
.students
.resolved()
.expect("M2M hydrated for a String-PK parent")
.iter()
.map(|s| s.name.as_str())
.collect();
names.sort();
assert_eq!(names, vec!["alice", "bob"]);
let go = courses.iter().find(|c| c.code == "go101").unwrap();
assert!(
go.students.resolved().expect("hydrated (empty)").is_empty(),
"go101 has no students"
);
let joined = Course::objects()
.join_related("students")
.fetch()
.await
.expect("join_related");
let rust = joined.iter().find(|c| c.code == "rust101").unwrap();
let mut jnames: Vec<&str> = rust
.students
.resolved()
.expect("M2M resolved via join_related")
.iter()
.map(|s| s.name.as_str())
.collect();
jnames.sort();
assert_eq!(jnames, vec!["alice", "bob"]);
}