ormer 0.2.4

A minimalist ORM framework that supports SQLite, PostgreSQL, MySQL, and SqlServer
Documentation
#![cfg(feature = "sqlite")]

pub mod _test_common;

use ormer::{ConnectionPool, Database, DbType};

define_test_user_simple!(ReplicatedUser, "replicated_users_test_1");
define_test_user_simple!(DynamicUser, "dynamic_users_test_1");

#[derive(Debug, ormer::Model, Clone)]
#[table = "cte_categories_test_1"]
struct Category {
    #[primary]
    id: i32,
    parent_id: Option<i32>,
    name: String,
}

fn sqlite_file(name: &str) -> String {
    let mut path = std::env::temp_dir();
    path.push(format!(
        "ormer_{}_{}_{}.db",
        name,
        std::process::id(),
        chrono::Utc::now().timestamp_nanos_opt().unwrap_or_default()
    ));
    path.to_string_lossy().into_owned()
}

async fn prepare_replica_db(path: &str, id: i32, name: &str) -> ormer::Result<()> {
    let db = Database::connect(DbType::Sqlite, path).await?;
    let _ = db.drop_table::<ReplicatedUser>().execute().await;
    db.create_table::<ReplicatedUser>().execute().await?;
    db.insert(&ReplicatedUser {
        id,
        name: name.to_string(),
        age: id,
    })
    .execute()
    .await?;
    Ok(())
}

#[tokio::test]
async fn replicated_database_routes_read_and_write() -> Result<(), Box<dyn std::error::Error>> {
    let primary = sqlite_file("replicated_primary");
    let replica = sqlite_file("replicated_read");

    prepare_replica_db(&primary, 1, "primary").await?;
    prepare_replica_db(&replica, 2, "replica").await?;

    let db = Database::replicated(DbType::Sqlite)
        .write(&primary)
        .read(&replica)
        .connect()
        .await?;

    let write_rows = db
        .write()
        .select::<ReplicatedUser>()
        .collect::<Vec<_>>()
        .await?;
    let read_rows = db
        .read()
        .select::<ReplicatedUser>()
        .collect::<Vec<_>>()
        .await?;

    assert_eq!(write_rows[0].name, "primary");
    assert_eq!(read_rows[0].name, "replica");

    let _ = std::fs::remove_file(primary);
    let _ = std::fs::remove_file(replica);
    Ok(())
}

#[tokio::test]
async fn replicated_connection_pool_routes_read_and_write() -> Result<(), Box<dyn std::error::Error>>
{
    let primary = sqlite_file("replicated_pool_primary");
    let replica = sqlite_file("replicated_pool_read");

    prepare_replica_db(&primary, 10, "pool-primary").await?;
    prepare_replica_db(&replica, 20, "pool-replica").await?;

    let pool = ConnectionPool::replicated(DbType::Sqlite)
        .write(&primary)
        .read(&replica)
        .range(0..1)
        .connect()
        .await?;

    let writer = pool.write().get().await?;
    let reader = pool.read().get().await?;
    let write_rows = writer
        .select::<ReplicatedUser>()
        .collect::<Vec<_>>()
        .await?;
    let read_rows = reader
        .select::<ReplicatedUser>()
        .collect::<Vec<_>>()
        .await?;

    assert_eq!(write_rows[0].name, "pool-primary");
    assert_eq!(read_rows[0].name, "pool-replica");

    let _ = std::fs::remove_file(primary);
    let _ = std::fs::remove_file(replica);
    Ok(())
}

#[tokio::test]
async fn dynamic_field_filter_and_order_validate_fields() -> Result<(), Box<dyn std::error::Error>>
{
    let db = Database::connect(DbType::Sqlite, ":memory:").await?;
    db.create_table::<DynamicUser>().execute().await?;
    db.insert(&vec![
        DynamicUser {
            id: 1,
            name: "Alice".to_string(),
            age: 30,
        },
        DynamicUser {
            id: 2,
            name: "Bob".to_string(),
            age: 20,
        },
    ])
    .execute()
    .await?;

    let users = db
        .select::<DynamicUser>()
        .filter(|u| u.field("name").ne("Bob"))
        .order_by_dynamic(|u| u.field("age").desc())
        .collect::<Vec<_>>()
        .await?;
    assert_eq!(users.len(), 1);
    assert_eq!(users[0].name, "Alice");

    let err = db
        .select::<DynamicUser>()
        .filter(|u| u.field("missing").eq(1))
        .collect::<Vec<_>>()
        .await
        .expect_err("missing dynamic field should fail before query execution");
    assert!(err.to_string().contains("missing"));

    Ok(())
}

#[tokio::test]
async fn recursive_cte_generates_descendants_and_ancestors_sql()
-> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect(DbType::Sqlite, ":memory:").await?;

    let descendants = db
        .select::<Category>()
        .descendants(|c| (c.id, c.parent_id), 1)
        .order_by(|c| c.id.asc())
        .to_sql()?;
    let descendants_sql = &descendants.statements[0].sql;
    assert!(descendants_sql.contains("WITH RECURSIVE"));
    assert!(descendants_sql.contains("UNION ALL"));
    assert!(descendants_sql.contains("child.parent_id = parent.id"));
    assert_eq!(descendants.statements[0].params.len(), 1);

    let ancestors = db
        .select::<Category>()
        .ancestors(|c| (c.id, c.parent_id), 3)
        .order_by(|c| c.id.asc())
        .to_sql()?;
    let ancestors_sql = &ancestors.statements[0].sql;
    assert!(ancestors_sql.contains("WITH RECURSIVE"));
    assert!(ancestors_sql.contains("UNION ALL"));
    assert!(ancestors_sql.contains("parent.id = child.parent_id"));
    assert_eq!(ancestors.statements[0].params.len(), 1);

    Ok(())
}