#![cfg(feature = "postgres")]
use chrono::Utc;
use sqlx::{PgPool, postgres::PgPoolOptions};
use std::env;
extern crate floz_orm as floz;
use floz_orm::prelude::*;
use floz_orm::types::*;
#[model("authors")]
#[hooks]
pub struct Author {
#[col(auto, key)]
pub id: i32,
#[col(max = 100)]
pub name: Varchar,
pub bio: Option<Text>,
#[col(now)]
pub created_at: TimestampTz,
#[col(now)]
pub updated_at: TimestampTz,
#[rel(has_many(model = "Book", foreign_key = "author_id"))]
pub books: Vec<Book>,
}
#[model("books")]
pub struct Book {
#[col(auto, key)]
pub id: i32,
pub author_id: i32,
#[col(max = 100)]
pub title: Varchar,
}
impl floz_orm::FlozHooks for Author {
fn before_save(&mut self) -> Result<(), floz_orm::FlozError> {
self.set_updated_at(Utc::now());
if self.name.trim().is_empty() {
return Err(floz_orm::FlozError::ValidationError("Name cannot be empty".into()));
}
Ok(())
}
fn before_delete(&self) -> Result<(), floz_orm::FlozError> {
if self.name == "Admin" {
return Err(floz_orm::FlozError::ValidationError("Cannot delete admin".into()));
}
Ok(())
}
}
fn test_url() -> Option<String> {
std::env::var("DATABASE_URL").ok()
}
async fn get_db() -> Option<Db> {
let url = test_url()?;
let pool = PgPoolOptions::new()
.max_connections(2)
.connect(&url)
.await
.expect("Failed to connect to PG");
setup_schema(&pool).await;
Some(floz_orm::Db::from_pg_pool(pool))
}
async fn setup_schema(pool: &PgPool) {
sqlx::query("DROP TABLE IF EXISTS books;")
.execute(pool).await.unwrap();
sqlx::query("DROP TABLE IF EXISTS authors;")
.execute(pool).await.unwrap();
sqlx::query(
r#"
CREATE TABLE authors (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
bio TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
"#,
)
.execute(pool)
.await
.unwrap();
sqlx::query(
r#"
CREATE TABLE books (
id SERIAL PRIMARY KEY,
author_id INT NOT NULL REFERENCES authors(id) ON DELETE CASCADE,
title VARCHAR(100) NOT NULL
);
"#,
)
.execute(pool)
.await
.unwrap();
}
#[tokio::test]
async fn test_hooks_lifecycle() {
let Some(db) = get_db().await else { return };
let mut author = Author::default();
author.name = "John Doe".into();
let mut author = author.create(&db).await.unwrap();
let old_updated = author.updated_at;
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
author.set_bio(Some("A writer".into()));
author.save(&db).await.unwrap();
let latest_author = Author::get(author.id, &db).await.unwrap();
assert!(latest_author.updated_at > old_updated);
author.set_name("".into());
let res = author.save(&db).await;
assert!(res.is_err());
let mut admin = Author::default();
admin.name = "Admin".into();
let admin = admin.create(&db).await.unwrap();
let res = admin.delete(&db).await;
assert!(res.is_err());
}
#[tokio::test]
async fn test_pagination() {
let Some(db) = get_db().await else { return };
let mut author = Author::default();
author.name = "Tolkien".into();
let author = author.create(&db).await.unwrap();
for i in 1..=25 {
let mut b = Book::default();
b.author_id = author.id;
b.title = format!("Book {:02}", i);
b.create(&db).await.unwrap();
}
let page1 = Book::paginate()
.where_(BookTable::author_id.eq(author.id))
.per_page(10)
.page(1)
.order_by(BookTable::title.desc())
.execute(&db)
.await
.unwrap();
assert_eq!(page1.total, 25);
assert_eq!(page1.total_pages, 3);
assert_eq!(page1.items.len(), 10);
assert!(page1.has_next);
assert!(!page1.has_prev);
assert_eq!(page1.items[0].title, "Book 25");
let page3 = Book::paginate()
.where_(BookTable::author_id.eq(author.id))
.per_page(10)
.page(3)
.order_by(BookTable::title.asc()) .execute(&db)
.await
.unwrap();
assert_eq!(page3.total, 25);
assert_eq!(page3.items.len(), 5);
assert!(!page3.has_next);
assert!(page3.has_prev);
assert_eq!(page3.items[0].title, "Book 21");
}
#[tokio::test]
async fn test_relationships() {
let Some(db) = get_db().await else { return };
let mut a1 = Author::default();
a1.name = "Author 1".into();
let a1 = a1.create(&db).await.unwrap();
let mut a2 = Author::default();
a2.name = "Author 2".into();
let a2 = a2.create(&db).await.unwrap();
for i in 1..=3 {
let mut b = Book::default();
b.author_id = a1.id;
b.title = format!("A1 Book {}", i);
b.create(&db).await.unwrap();
}
for i in 1..=2 {
let mut b = Book::default();
b.author_id = a2.id;
b.title = format!("A2 Book {}", i);
b.create(&db).await.unwrap();
}
let a1_books = a1.fetch_books(&db).await.unwrap();
assert_eq!(a1_books.len(), 3);
assert_eq!(a1_books[0].title, "A1 Book 1");
let mut authors = Author::all(&db).await.unwrap();
authors.sort_by_key(|a| a.id);
assert_eq!(authors.len(), 2);
assert!(authors[0].books.is_empty());
Author::preload_books(&mut authors, &db).await.unwrap();
assert_eq!(authors[0].books.len(), 3);
assert_eq!(authors[1].books.len(), 2);
assert_eq!(authors[0].books[0].title, "A1 Book 1");
assert_eq!(authors[1].books[0].title, "A2 Book 1");
}