mod common;
use common::setup_users_posts_schema;
use pgorm::{FromRow, GenericClient, Model, ModelPk as _, OrmError, create_pool, query};
use std::env;
#[derive(Debug, Clone, FromRow, Model)]
#[orm(table = "users")]
#[orm(has_many(Post, foreign_key = "user_id", as = "posts"))]
struct User {
#[orm(id)]
id: i64,
name: String,
}
#[allow(clippy::duplicated_attributes)]
#[derive(Debug, Clone, FromRow, Model)]
#[orm(table = "posts")]
#[orm(belongs_to(User, foreign_key = "user_id", as = "author"))]
#[orm(belongs_to(User, foreign_key = "editor_id", as = "editor"))]
struct Post {
#[orm(id)]
id: i64,
user_id: i64,
editor_id: Option<i64>,
title: String,
}
#[tokio::main]
async fn main() -> Result<(), OrmError> {
dotenvy::dotenv().ok();
let database_url =
env::var("DATABASE_URL").expect("DATABASE_URL must be set in .env or environment");
let pool = create_pool(&database_url)?;
let client = pool.get().await?;
setup_users_posts_schema(&client).await?;
seed_data(&client).await?;
let users = User::select_all(&client).await?;
let posts = Post::select_all(&client).await?;
println!("\n== Base ==");
println!("users: {}", users.len());
println!("posts: {}", posts.len());
println!("\n== Map style (has_many) ==");
let posts_by_user = User::load_posts_map_with(&client, &users, |q| {
q.push(" ORDER BY id DESC");
})
.await?;
for u in &users {
let count = posts_by_user.get(u.pk()).map(|v| v.len()).unwrap_or(0);
println!("- user {} ({}) posts={}", u.id, u.name, count);
}
println!("\n== Attach style (has_many) ==");
let users_loaded = User::load_posts(&client, users.clone()).await?;
for u in &users_loaded {
println!("- user {} posts={}", u.id, u.rel.len());
}
println!("\n== Attach style (belongs_to) ==");
let posts_with_author = Post::load_author(&client, posts.clone()).await?;
for p in &posts_with_author {
let author = p
.rel
.as_ref()
.map(|u| u.name.as_str())
.unwrap_or("(missing)");
println!("- post {} title={:?} author={}", p.id, p.title, author);
}
let posts_with_editor = Post::load_editor(&client, posts.clone()).await?;
for p in &posts_with_editor {
let editor = p.rel.as_ref().map(|u| u.name.as_str()).unwrap_or("(null)");
println!("- post {} editor={}", p.id, editor);
}
println!("\n== belongs_to strict (expected error if any editor_id is NULL) ==");
match Post::load_editor_strict(&client, posts.clone()).await {
Ok(_) => println!("unexpected: all posts have editor_id"),
Err(e) => println!("expected error: {e}"),
}
Ok(())
}
async fn seed_data(conn: &impl GenericClient) -> Result<(), OrmError> {
let alice: User = query("INSERT INTO users (name) VALUES ($1) RETURNING id, name")
.bind("alice")
.fetch_one_as(conn)
.await?;
let bob: User = query("INSERT INTO users (name) VALUES ($1) RETURNING id, name")
.bind("bob")
.fetch_one_as(conn)
.await?;
let _p1: Post = query(
"INSERT INTO posts (user_id, editor_id, title)
VALUES ($1, $2, $3)
RETURNING id, user_id, editor_id, title",
)
.bind(alice.id)
.bind(Some(bob.id))
.bind("Hello, pgorm")
.fetch_one_as(conn)
.await?;
let _p2: Post = query(
"INSERT INTO posts (user_id, editor_id, title)
VALUES ($1, $2, $3)
RETURNING id, user_id, editor_id, title",
)
.bind(alice.id)
.bind(None::<i64>)
.bind("Editor is NULL (to demo strict)")
.fetch_one_as(conn)
.await?;
let _p3: Post = query(
"INSERT INTO posts (user_id, editor_id, title)
VALUES ($1, $2, $3)
RETURNING id, user_id, editor_id, title",
)
.bind(bob.id)
.bind(Some(bob.id))
.bind("Second author")
.fetch_one_as(conn)
.await?;
Ok(())
}