floz-orm 0.1.7

A lightweight, typesafe Rust ORM — unifying DAO and DSL from a single schema
Documentation
//! Integration tests for the #[model] proc macro.
//!
//! These tests verify that the generated code compiles and behaves correctly.
extern crate floz_orm as floz;

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

// ── Test: Basic model generation ──

#[model("users")]
pub struct User {
    #[col(auto, key)]
    pub id: i32,
    #[col(max = 100)]
    pub name: Varchar,
    pub age: Option<i16>,
    pub bio: Text,
}

#[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 ──

#[model("audit.logs")]
pub struct AuditLog {
    pub event: Text,
    pub timestamp: TimestampTz,
}

#[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 TimestampTz alias
    let _: chrono::DateTime<chrono::Utc> = log.timestamp;
}

// ── Test: Renamed columns ──

#[model("products")]
pub struct Product {
    #[col(name = "id", key)]
    pub product_id: i32,
    #[col(name = "display_name", max = 255)]
    pub product_name: Varchar,
}

#[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! ──

#[model("authors")]
pub struct Author {
    #[col(key)]
    pub id: i32,
    #[col(max = 100)]
    pub name: Varchar,
}

#[model("books")]
pub struct Book {
    #[col(key)]
    pub id: i32,
    pub title: Text,
    pub author_id: i32,
}

#[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 ──

#[model("type_test")]
pub struct TypeTest {
    pub a_int: i32,
    pub b_short: i16,
    pub c_bigint: i64,
    pub d_real: f32,
    pub e_double: f64,
    pub f_text: Text,
    pub g_bool: bool,
    pub h_uuid: uuid::Uuid,
    #[col(max = 50)]
    pub i_varchar: Varchar,
    pub j_binary: Bytes,
    pub k_date: Date,
    pub l_time: chrono::NaiveTime,
    pub m_naive: Timestamp,
    pub n_tz: TimestampTz,
}

#[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 ──

#[model("nullable_test")]
pub struct NullableTest {
    pub id: i32,
    pub name: Option<Text>,
    pub age: Option<i16>,
    pub score: Option<f64>,
    pub tag: Option<uuid::Uuid>,
}

#[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: 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")))]
#[model("exotics")]
pub struct WithExotics {
    #[col(auto, key)]
    pub id: i32,
    pub data: Json,
    pub meta: Option<Jsonb>,
    pub 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));
}