#![cfg(test)]
use std::sync::{Mutex, MutexGuard, OnceLock};
use std::time::Duration;
use prax_orm::{Model, PraxClient, client};
use prax_postgres::{PgEngine, PgPool, PgPoolBuilder};
use prax_query::error::QueryResult;
use prax_query::filter::FilterValue;
fn test_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
.lock()
.unwrap_or_else(|p| p.into_inner())
}
#[derive(Model, Debug, Clone)]
#[prax(table = "nested_posts")]
pub struct Post {
#[prax(id, auto)]
pub id: i32,
pub title: String,
pub author_id: i32,
}
#[derive(Model, Debug, Clone)]
#[prax(table = "nested_users")]
pub struct User {
#[prax(id, auto)]
pub id: i32,
#[prax(unique)]
pub email: String,
#[prax(relation(target = "Post", foreign_key = "author_id"))]
pub posts: Vec<Post>,
}
client!(User, Post);
fn postgres_url() -> Option<String> {
if std::env::var("PRAX_E2E").ok().as_deref() != Some("1") {
return None;
}
Some(
std::env::var("POSTGRES_URL").unwrap_or_else(|_| {
"postgres://prax:prax_test_password@localhost:5432/prax_test".into()
}),
)
}
async fn setup() -> Option<PraxClient<PgEngine>> {
let url = postgres_url()?;
let pool: PgPool = PgPoolBuilder::new()
.url(url)
.max_connections(4)
.connection_timeout(Duration::from_secs(10))
.build()
.await
.expect("connect to postgres");
let conn = pool.get().await.expect("acquire conn");
conn.batch_execute(
"BEGIN;
SELECT pg_advisory_xact_lock(0x6e6573746564300a);
CREATE TABLE IF NOT EXISTS nested_users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS nested_posts (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
author_id INTEGER NOT NULL REFERENCES nested_users(id)
);
TRUNCATE nested_posts, nested_users RESTART IDENTITY CASCADE;
COMMIT",
)
.await
.expect("create nested_{users,posts}");
drop(conn);
Some(PraxClient::new(PgEngine::new(pool)))
}
#[tokio::test]
#[ignore = "requires docker-compose postgres (PRAX_E2E=1)"]
async fn create_user_with_nested_posts() {
let _guard = test_lock();
let Some(c) = setup().await else {
eprintln!("skipping: PRAX_E2E not set");
return;
};
let u: User = c
.user()
.create()
.set("email", "nw@x.com")
.with(user::posts::create(vec![
vec![("title".into(), FilterValue::String("p1".into()))],
vec![("title".into(), FilterValue::String("p2".into()))],
]))
.exec()
.await
.expect("nested create");
assert!(u.id > 0, "auto-id should be assigned");
let posts: Vec<Post> = c
.post()
.find_many()
.r#where(post::author_id::equals(u.id))
.exec()
.await
.expect("find posts by author_id");
assert_eq!(posts.len(), 2, "both nested children should be inserted");
let mut titles: Vec<_> = posts.iter().map(|p| p.title.as_str().to_owned()).collect();
titles.sort();
assert_eq!(titles, vec!["p1".to_string(), "p2".to_string()]);
}
#[tokio::test]
#[ignore = "requires docker-compose postgres (PRAX_E2E=1)"]
async fn nested_write_failure_rolls_back_parent() {
let _guard = test_lock();
let Some(c) = setup().await else {
eprintln!("skipping: PRAX_E2E not set");
return;
};
let result: QueryResult<User> = c
.user()
.create()
.set("email", "rollback@x.com")
.with(user::posts::create(vec![vec![(
"nonexistent_column".into(),
FilterValue::String("p1".into()),
)]]))
.exec()
.await;
assert!(
result.is_err(),
"nested child failure must surface as Err, got: {:?}",
result.ok().map(|u| u.id)
);
let users: Vec<User> = c
.user()
.find_many()
.r#where(user::email::equals("rollback@x.com".into()))
.exec()
.await
.expect("post-rollback find");
assert!(
users.is_empty(),
"parent INSERT should have rolled back; found {} row(s)",
users.len()
);
}