floz-orm 0.1.6

A lightweight, typesafe Rust ORM — unifying DAO and DSL from a single schema
Documentation
//! Integration tests for the schema! proc macro.
//!
//! These tests verify that the generated code compiles and behaves correctly.

use floz_orm as floz;

// ── Test: Basic model generation ──

floz_orm::schema! {
    model User("users") {
        id:   integer("id").auto_increment().primary(),
        name: varchar("name", 100),
        age:  short("age").nullable(),
        bio:  text("bio"),
    }
}

#[test]
fn user_struct_exists() {
    let u = User::default();
    assert_eq!(u.id, 0);
    assert_eq!(u.name, "");
    assert!(u.age.is_none());
    assert_eq!(u.bio, "");
    assert_eq!(u._dirty_flags, 0);
}

#[test]
fn user_table_exists() {
    assert_eq!(UserTable::TABLE_NAME, "users");
    assert_eq!(UserTable::COLUMNS, ["id", "name", "age", "bio"]);
}

#[test]
fn user_column_constants() {
    assert_eq!(UserTable::id.name(), "id");
    assert_eq!(UserTable::id.table(), "users");
    assert_eq!(UserTable::name.name(), "name");
    assert_eq!(UserTable::age.name(), "age");
    assert_eq!(UserTable::bio.name(), "bio");
}

#[test]
fn user_column_dsl_operators() {
    // These should compile and produce valid Expr nodes
    let _ = UserTable::age.gt(25i16);
    let _ = UserTable::name.eq("Alice");
    let _ = UserTable::name.contains("foo");
    let _ = UserTable::age.is_null();
    let _ = UserTable::id.in_list(vec![1, 2, 3]);
}

#[test]
fn user_dirty_tracking() {
    let mut u = User::default();
    assert!(!u.is_dirty());

    u.set_name("Alice".to_string());
    assert!(u.is_dirty());
    assert!(u.is_field_dirty(1)); // name is index 1

    u.set_age(Some(30));
    assert!(u.is_field_dirty(2)); // age is index 2

    assert!(!u.is_field_dirty(0)); // id not dirty
    assert!(!u.is_field_dirty(3)); // bio not dirty

    u.clear_dirty();
    assert!(!u.is_dirty());
}

#[test]
fn user_default_does_not_set_dirty() {
    let u = User::default();
    assert_eq!(u._dirty_flags, 0);
}

#[test]
fn user_serde_roundtrip() {
    let mut u = User::default();
    u.id = 1;
    u.name = "Alice".to_string();
    u.age = Some(30);
    u.bio = "Hello!".to_string();
    u._dirty_flags = 0xFF; // should be skipped by serde

    let json = serde_json::to_string(&u).unwrap();
    assert!(!json.contains("dirty")); // _dirty_flags is skipped

    let u2: User = serde_json::from_str(&json).unwrap();
    assert_eq!(u2.name, "Alice");
    assert_eq!(u2._dirty_flags, 0); // defaults to 0 on deserialize
}

// ── Test: Schema-qualified table ──

floz_orm::schema! {
    model AuditLog("audit.logs") {
        event: text("event"),
        timestamp: datetime("created_at").tz(),
    }
}

#[test]
fn schema_qualified_table() {
    assert_eq!(AuditLogTable::TABLE_NAME, "audit.logs");
}

#[test]
fn datetime_with_tz() {
    let log = AuditLog::default();
    // The timestamp field should be DateTime<Utc> due to .tz()
    let _: chrono::DateTime<chrono::Utc> = log.timestamp;
}

// ── Test: Renamed columns ──

floz_orm::schema! {
    model Product("products") {
        product_id:       integer("id").primary(),
        product_name:     varchar("display_name", 255),
    }
}

#[test]
fn renamed_columns() {
    // Column constants use DB names
    assert_eq!(ProductTable::product_id.name(), "id");
    assert_eq!(ProductTable::product_name.name(), "display_name");
    // Rust field names are different
    let p = Product::default();
    let _ = p.product_id;
    let _ = p.product_name;
}

// ── Test: Multiple models in one schema! ──

floz_orm::schema! {
    model Author("authors") {
        id: integer("id").primary(),
        name: varchar("name", 100),
    }
    model Book("books") {
        id: integer("id").primary(),
        title: text("title"),
        author_id: integer("author_id"),
    }
}

#[test]
fn multiple_models() {
    assert_eq!(AuthorTable::TABLE_NAME, "authors");
    assert_eq!(BookTable::TABLE_NAME, "books");

    let _ = Author::default();
    let _ = Book::default();
}

// ── Test: All column types ──

floz_orm::schema! {
    model TypeTest("type_test") {
        a_int:     integer("a_int"),
        b_short:   short("b_short"),
        c_bigint:  bigint("c_bigint"),
        d_real:    real("d_real"),
        e_double:  double("e_double"),
        f_text:    text("f_text"),
        g_bool:    bool("g_bool"),
        h_uuid:    uuid("h_uuid"),
        i_varchar: varchar("i_varchar", 50),
        j_binary:  binary("j_binary"),
        k_date:    date("k_date"),
        l_time:    time("l_time"),
        m_naive:   datetime("m_naive"),
        n_tz:      datetime("n_tz").tz(),
    }
}

#[test]
fn all_types_default() {
    let t = TypeTest::default();
    assert_eq!(t.a_int, 0i32);
    assert_eq!(t.b_short, 0i16);
    assert_eq!(t.c_bigint, 0i64);
    assert_eq!(t.d_real, 0.0f32);
    assert_eq!(t.e_double, 0.0f64);
    assert_eq!(t.f_text, "");
    assert!(!t.g_bool);
    assert_eq!(t.h_uuid, uuid::Uuid::nil());
    assert_eq!(t.i_varchar, "");
    assert!(t.j_binary.is_empty());
}

// ── Test: Nullable types ──

floz_orm::schema! {
    model NullableTest("nullable_test") {
        id:    integer("id"),
        name:  text("name").nullable(),
        age:   short("age").nullable(),
        score: double("score").nullable(),
        tag:   uuid("tag").nullable(),
    }
}

#[test]
fn nullable_defaults() {
    let t = NullableTest::default();
    assert_eq!(t.id, 0);
    assert!(t.name.is_none());
    assert!(t.age.is_none());
    assert!(t.score.is_none());
    assert!(t.tag.is_none());
}

// ── Test: Array column types ──

floz_orm::schema! {
    model WithArrays("with_arrays") {
        id:     integer("id"),
        tags:   text_array("tags"),
        scores: int_array("scores"),
        ids:    uuid_array("ids"),
    }
}

#[test]
fn array_columns() {
    let w = WithArrays::default();
    assert!(w.tags.is_empty());
    assert!(w.scores.is_empty());
    assert!(w.ids.is_empty());
}

// ── Test: JSON and Enum types ──

#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, sqlx::Type, utoipa::ToSchema)]
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
pub enum Status {
    Active,
    Inactive,
}

#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
impl Default for Status {
    fn default() -> Self {
        Status::Active
    }
}

// Ensure the enum prints gracefully when passed to postgres TEXT fallback
#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
impl std::fmt::Display for Status {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Status::Active => write!(f, "active"),
            Status::Inactive => write!(f, "inactive"),
        }
    }
}

#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
floz_orm::schema! {
    model WithExotics("exotics") {
        id:      integer("id").auto_increment().primary(),
        data:    json("data"),
        meta:    jsonb("meta").nullable(),
        status:  enumeration("status", Status),
    }
}

#[cfg(all(feature = "postgres", not(feature = "sqlite")))]
#[test]
fn exotic_columns() {
    let e = WithExotics::default();
    assert_eq!(e.data, serde_json::Value::Null);
    assert!(e.meta.is_none());
    assert!(matches!(e.status, Status::Active));
}