floz-orm 0.1.7

A lightweight, typesafe Rust ORM — unifying DAO and DSL from a single schema
Documentation
//! Integration tests for DAO CRUD methods (require DATABASE_URL).
//!
//! These tests verify the full roundtrip: schema! → struct → CREATE TABLE → CRUD → verify.
extern crate floz_orm as floz;

use floz_orm::Executor;

fn test_url() -> Option<String> {
    std::env::var("DATABASE_URL").ok()
}

// ── Schema definition ──

use floz_orm::prelude::*;
use floz_orm::types::*;

#[model("floz_test_employees")]
pub struct Employee {
    #[col(key, auto)]
    pub id: i32,
    #[col(max = 100)]
    pub name: Varchar,
    pub email: Text,
    pub age: Option<i16>,
    pub active: bool,
}

#[model("floz_test_append_log")]
pub struct AppendLog {
    pub event: Text,
    pub timestamp: TimestampTz,
}

// ── DAO Tests ──

#[tokio::test]
async fn crud_full_lifecycle() {
    let Some(url) = test_url() else { return };
    let db = floz_orm::Db::connect(&url).await.unwrap();

    // Setup
    db.execute_raw(
        "CREATE TABLE IF NOT EXISTS floz_test_employees (
            id SERIAL PRIMARY KEY,
            name VARCHAR(100) NOT NULL,
            email TEXT NOT NULL,
            age SMALLINT,
            active BOOLEAN NOT NULL DEFAULT true
        )",
        vec![],
    ).await.unwrap();
    db.execute_raw("DELETE FROM floz_test_employees", vec![]).await.unwrap();

    // CREATE
    let emp = Employee {
        name: "Alice".to_string(),
        email: "alice@example.com".to_string(),
        age: Some(30),
        active: true,
        ..Employee::default()
    };
    let created = emp.create(&db).await.unwrap();
    assert!(created.id > 0, "auto-increment should assign a positive ID");
    assert_eq!(created.name, "Alice");
    assert_eq!(created.age, Some(30));
    let alice_id = created.id;

    // GET
    let fetched = Employee::get(alice_id, &db).await.unwrap();
    assert_eq!(fetched.name, "Alice");
    assert_eq!(fetched.email, "alice@example.com");

    // FIND (exists)
    let found = Employee::find(alice_id, &db).await.unwrap();
    assert!(found.is_some());
    assert_eq!(found.unwrap().name, "Alice");

    // FIND (not exists)
    let not_found = Employee::find(99999, &db).await.unwrap();
    assert!(not_found.is_none());

    // GET (not found → error)
    let err = Employee::get(99999, &db).await;
    assert!(matches!(err, Err(floz_orm::FlozError::NotFound)));

    // SAVE (dirty tracking)
    let mut alice = Employee::get(alice_id, &db).await.unwrap();
    assert!(!alice.is_dirty());
    alice.set_name("Alice Updated".to_string());
    alice.set_age(Some(31));
    assert!(alice.is_dirty());
    alice.save(&db).await.unwrap();
    assert!(!alice.is_dirty()); // cleared after save

    let refreshed = Employee::get(alice_id, &db).await.unwrap();
    assert_eq!(refreshed.name, "Alice Updated");
    assert_eq!(refreshed.age, Some(31));

    // SAVE (no-op when not dirty)
    let mut clean = Employee::get(alice_id, &db).await.unwrap();
    clean.save(&db).await.unwrap(); // should do nothing

    // ALL
    let emp2 = Employee {
        name: "Bob".to_string(),
        email: "bob@example.com".to_string(),
        age: None,
        active: false,
        ..Employee::default()
    };
    emp2.create(&db).await.unwrap();

    let all = Employee::all(&db).await.unwrap();
    assert_eq!(all.len(), 2);

    // FILTER
    let active_emps = Employee::filter(
        EmployeeTable::active.eq(true),
        &db,
    ).await.unwrap();
    assert_eq!(active_emps.len(), 1);
    assert_eq!(active_emps[0].name, "Alice Updated");

    let age_filter = Employee::filter(
        EmployeeTable::age.is_null(),
        &db,
    ).await.unwrap();
    assert_eq!(age_filter.len(), 1);
    assert_eq!(age_filter[0].name, "Bob");

    // DELETE
    let bob = Employee::filter(
        EmployeeTable::name.eq("Bob"),
        &db,
    ).await.unwrap().into_iter().next().unwrap();
    bob.delete(&db).await.unwrap();

    let remaining = Employee::all(&db).await.unwrap();
    assert_eq!(remaining.len(), 1);

    // Cleanup
    db.execute_raw("DROP TABLE IF EXISTS floz_test_employees", vec![]).await.unwrap();
}

#[tokio::test]
async fn crud_with_transaction() {
    let Some(url) = test_url() else { return };
    let db = floz_orm::Db::connect(&url).await.unwrap();

    db.execute_raw(
        "CREATE TABLE IF NOT EXISTS floz_test_employees (
            id SERIAL PRIMARY KEY,
            name VARCHAR(100) NOT NULL,
            email TEXT NOT NULL,
            age SMALLINT,
            active BOOLEAN NOT NULL DEFAULT true
        )",
        vec![],
    ).await.unwrap();
    db.execute_raw("DELETE FROM floz_test_employees", vec![]).await.unwrap();

    // Transaction + commit
    let tx = db.begin().await.unwrap();
    let emp = Employee {
        name: "TxUser".to_string(),
        email: "tx@example.com".to_string(),
        active: true,
        ..Employee::default()
    };
    let created = emp.create(&tx).await.unwrap();
    assert!(created.id > 0);
    tx.commit().await.unwrap();

    // Verify visible after commit
    let found = Employee::filter(
        EmployeeTable::name.eq("TxUser"),
        &db,
    ).await.unwrap();
    assert_eq!(found.len(), 1);

    // Transaction + rollback
    let tx2 = db.begin().await.unwrap();
    let emp2 = Employee {
        name: "RollbackUser".to_string(),
        email: "rb@example.com".to_string(),
        active: true,
        ..Employee::default()
    };
    emp2.create(&tx2).await.unwrap();
    tx2.rollback().await.unwrap();

    // Verify NOT visible after rollback
    let found2 = Employee::filter(
        EmployeeTable::name.eq("RollbackUser"),
        &db,
    ).await.unwrap();
    assert_eq!(found2.len(), 0);

    // Cleanup
    db.execute_raw("DROP TABLE IF EXISTS floz_test_employees", vec![]).await.unwrap();
}

#[tokio::test]
async fn crud_append_only_no_pk() {
    let Some(url) = test_url() else { return };
    let db = floz_orm::Db::connect(&url).await.unwrap();

    db.execute_raw(
        "CREATE TABLE IF NOT EXISTS floz_test_append_log (
            event TEXT NOT NULL,
            timestamp TIMESTAMPTZ NOT NULL
        )",
        vec![],
    ).await.unwrap();
    db.execute_raw("DELETE FROM floz_test_append_log", vec![]).await.unwrap();

    // CREATE (no PK model)
    let log = AppendLog {
        event: "user_login".to_string(),
        timestamp: chrono::Utc::now(),
        ..AppendLog::default()
    };
    let created = log.create(&db).await.unwrap();
    assert_eq!(created.event, "user_login");

    // ALL
    let all = AppendLog::all(&db).await.unwrap();
    assert_eq!(all.len(), 1);

    // FILTER
    let logs = AppendLog::filter(
        AppendLogTable::event.eq("user_login"),
        &db,
    ).await.unwrap();
    assert_eq!(logs.len(), 1);

    // Note: no get(), find(), save(), delete() — no primary key!

    // Cleanup
    db.execute_raw("DROP TABLE IF EXISTS floz_test_append_log", vec![]).await.unwrap();
}

#[tokio::test]
async fn save_only_updates_dirty_fields() {
    let Some(url) = test_url() else { return };
    let db = floz_orm::Db::connect(&url).await.unwrap();

    db.execute_raw(
        "CREATE TABLE IF NOT EXISTS floz_test_employees (
            id SERIAL PRIMARY KEY,
            name VARCHAR(100) NOT NULL,
            email TEXT NOT NULL,
            age SMALLINT,
            active BOOLEAN NOT NULL DEFAULT true
        )",
        vec![],
    ).await.unwrap();
    db.execute_raw("DELETE FROM floz_test_employees", vec![]).await.unwrap();

    let emp = Employee {
        name: "DirtyTest".to_string(),
        email: "dirty@example.com".to_string(),
        age: Some(25),
        active: true,
        ..Employee::default()
    };
    let created = emp.create(&db).await.unwrap();

    // Only modify name — age and email should remain unchanged
    let mut e = Employee::get(created.id, &db).await.unwrap();
    e.set_name("DirtyTest Updated".to_string());
    // DON'T touch age or email
    e.save(&db).await.unwrap();

    let refreshed = Employee::get(created.id, &db).await.unwrap();
    assert_eq!(refreshed.name, "DirtyTest Updated");
    assert_eq!(refreshed.email, "dirty@example.com"); // unchanged
    assert_eq!(refreshed.age, Some(25)); // unchanged
    assert!(refreshed.active); // unchanged

    // Cleanup
    db.execute_raw("DROP TABLE IF EXISTS floz_test_employees", vec![]).await.unwrap();
}