ormer 0.2.7

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

use std::collections::HashMap;

use ormer::{Model, Value};

#[derive(Debug, Clone, PartialEq, ormer::Model)]
#[table = "polymorphic_documents"]
struct Document {
    #[primary]
    id: i64,
    tenant_id: i64,
    title: String,
    body: DocumentBody,
}

#[derive(Debug, Clone, PartialEq, ormer::ModelEnum)]
#[db_type(String)]
enum DocumentBody {
    Article {
        article_body: String,
        article_word_count: i32,
    },
    Video {
        video_url: String,
        video_duration_seconds: i32,
    },
}

#[test]
fn polymorphic_enum_metadata_expands_flat_columns() {
    assert_eq!(
        Document::columns(),
        vec![
            "id",
            "tenant_id",
            "title",
            "body",
            "article_body",
            "article_word_count",
            "video_url",
            "video_duration_seconds",
        ]
    );

    let body = Document::column_schema()
        .into_iter()
        .find(|column| column.name == "body")
        .expect("body column");
    assert_eq!(body.rust_type, "String");
    assert!(!body.is_nullable);
    assert_eq!(body.enum_variants, None);

    let article_body = Document::column_schema()
        .into_iter()
        .find(|column| column.name == "article_body")
        .expect("article_body column");
    assert_eq!(article_body.rust_name, "body.article_body");
    assert!(article_body.is_nullable);
}

#[test]
fn polymorphic_enum_field_values_write_inactive_columns_as_null() {
    let document = Document {
        id: 1,
        tenant_id: 10,
        title: "Intro".to_string(),
        body: DocumentBody::Article {
            article_body: "Hello".to_string(),
            article_word_count: 1,
        },
    };

    let values = document.field_values();
    assert_eq!(values.len(), Document::columns().len());
    assert!(matches!(&values[3], Value::Text(value) if value == "article"));
    assert!(matches!(&values[4], Value::Text(value) if value == "Hello"));
    assert!(matches!(&values[5], Value::Integer(value) if *value == 1));
    assert!(matches!(&values[6], Value::Null));
    assert!(matches!(&values[7], Value::Null));

    assert!(matches!(
        document.column_value("body"),
        Some(Value::Text(value)) if value == "article"
    ));
    assert!(matches!(
        document.column_value("video_url"),
        Some(Value::Null)
    ));
}

#[test]
fn polymorphic_enum_from_row_values_dispatches_by_discriminator() -> ormer::Result<()> {
    let article = Document::from_row_values(&[
        Value::Integer(1),
        Value::Integer(10),
        Value::Text("Intro".to_string()),
        Value::Text("article".to_string()),
        Value::Text("Hello".to_string()),
        Value::Integer(1),
        Value::Null,
        Value::Null,
    ])?;
    assert_eq!(
        article.body,
        DocumentBody::Article {
            article_body: "Hello".to_string(),
            article_word_count: 1,
        }
    );

    let video = Document::from_row_values(&[
        Value::Integer(2),
        Value::Integer(10),
        Value::Text("Clip".to_string()),
        Value::Text("video".to_string()),
        Value::Null,
        Value::Null,
        Value::Text("https://example.test/video".to_string()),
        Value::Integer(30),
    ])?;
    assert_eq!(
        video.body,
        DocumentBody::Video {
            video_url: "https://example.test/video".to_string(),
            video_duration_seconds: 30,
        }
    );

    Ok(())
}

#[test]
fn polymorphic_enum_from_row_dispatches_by_discriminator() -> ormer::Result<()> {
    let row = ormer::Row::new(HashMap::from([
        ("id".to_string(), Value::Integer(1)),
        ("tenant_id".to_string(), Value::Integer(10)),
        ("title".to_string(), Value::Text("Intro".to_string())),
        ("body".to_string(), Value::Text("article".to_string())),
        ("article_body".to_string(), Value::Text("Hello".to_string())),
        ("article_word_count".to_string(), Value::Integer(1)),
        ("video_url".to_string(), Value::Null),
        ("video_duration_seconds".to_string(), Value::Null),
    ]));

    let document = Document::from_row(&row)?;
    assert_eq!(
        document.body,
        DocumentBody::Article {
            article_body: "Hello".to_string(),
            article_word_count: 1,
        }
    );

    Ok(())
}

#[test]
fn polymorphic_enum_rejects_unknown_discriminator() {
    let result = Document::from_row_values(&[
        Value::Integer(1),
        Value::Integer(10),
        Value::Text("Intro".to_string()),
        Value::Text("unknown".to_string()),
        Value::Null,
        Value::Null,
        Value::Null,
        Value::Null,
    ]);

    assert!(result.is_err());
}

#[tokio::test]
async fn polymorphic_enum_sqlite_roundtrip() -> ormer::Result<()> {
    let db = ormer::Database::connect(ormer::DbType::Sqlite, ":memory:").await?;
    db.create_table::<Document>().execute().await?;

    db.insert(&Document {
        id: 1,
        tenant_id: 10,
        title: "Intro".to_string(),
        body: DocumentBody::Article {
            article_body: "Hello".to_string(),
            article_word_count: 1,
        },
    })
    .execute()
    .await?;

    db.insert(&Document {
        id: 2,
        tenant_id: 10,
        title: "Clip".to_string(),
        body: DocumentBody::Video {
            video_url: "https://example.test/video".to_string(),
            video_duration_seconds: 30,
        },
    })
    .execute()
    .await?;

    let documents = db
        .select::<Document>()
        .order_by(|d| d.id.asc())
        .collect::<Vec<_>>()
        .await?;

    assert_eq!(documents.len(), 2);
    assert!(matches!(documents[0].body, DocumentBody::Article { .. }));
    assert_eq!(
        documents[1].body,
        DocumentBody::Video {
            video_url: "https://example.test/video".to_string(),
            video_duration_seconds: 30,
        }
    );

    Ok(())
}