ormer 0.2.7

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

pub mod _test_common;

// 使用宏定义测试专用模型(每个测试使用唯一表名)
define_test_user_simple!(ValidateTestUserSuccess, "validate_table_success_users_1");
define_test_user_simple!(
    ValidateTestUserNotExists,
    "validate_table_notexists_users_1"
);
define_test_user_simple!(
    ValidateTestUserNoValidation,
    "validate_table_novalidation_users_1"
);

#[cfg(feature = "sqlite")]
#[derive(Debug, ormer::Model, Clone)]
#[table = "validate_table_primary_key_mismatch_1"]
struct ValidateTestPrimaryKeyMismatch {
    #[primary]
    id: i32,
    name: String,
}

#[cfg(feature = "postgresql")]
#[derive(Debug, ormer::Model, Clone)]
#[table = "validate_table_hypertable_mismatch_1"]
struct ValidateTestHypertable {
    #[primary]
    id: i32,
    #[hypertable(std::time::Duration::from_secs(3600))]
    created_at: chrono::NaiveDateTime,
    name: String,
}

#[cfg(feature = "postgresql")]
#[derive(Debug, ormer::Model, Clone)]
#[table = "validate_table_hypertable_interval_mismatch_1"]
struct ValidateTestHypertableInterval {
    #[primary]
    id: i32,
    #[primary]
    #[hypertable(std::time::Duration::from_secs(3600))]
    created_at: chrono::NaiveDateTime,
    name: String,
}

#[cfg(any(feature = "sqlite", feature = "postgresql", feature = "mysql"))]
mod validate_table_tests {
    use super::*;
    use _test_common::{DbConfig, create_db_connection};

    async fn test_validate_table_success_impl(
        config: &DbConfig,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let db = create_db_connection(config).await?;

        // 先删除表(如果存在)
        db.drop_table::<ValidateTestUserSuccess>()
            .execute()
            .await
            .ok();

        // 创建表
        db.create_table::<ValidateTestUserSuccess>()
            .execute()
            .await?;

        // 验证表结构应该成功
        db.validate_table::<ValidateTestUserSuccess>().await?;

        println!("validate_table succeeded for existing table");

        // 清理
        db.drop_table::<ValidateTestUserSuccess>().execute().await?;

        Ok(())
    }

    async fn test_validate_table_not_exists_impl(
        config: &DbConfig,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let db = create_db_connection(config).await?;

        // 确保表不存在
        db.drop_table::<ValidateTestUserNotExists>()
            .execute()
            .await
            .ok();

        // 验证不存在的表应该失败
        // 验证表不存在时的错误处理
        let result = db.validate_table::<ValidateTestUserNotExists>().await;
        assert!(
            result.is_err(),
            "validate_table should fail for non-existent table"
        );

        // 错误已被包装为 ormer::OrmerError,只需验证它确实是错误
        println!("Correctly detected non-existent table: {:?}", result.err());

        Ok(())
    }

    async fn test_create_table_without_validation_impl(
        config: &DbConfig,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let db = create_db_connection(config).await?;

        // 先删除表(如果存在)
        db.drop_table::<ValidateTestUserNoValidation>()
            .execute()
            .await?;

        // 创建表(不应该进行验证)
        db.create_table::<ValidateTestUserNoValidation>()
            .execute()
            .await?;

        println!("create_table succeeded without validation");

        // 清理
        db.drop_table::<ValidateTestUserNoValidation>()
            .execute()
            .await?;

        Ok(())
    }

    #[cfg(feature = "sqlite")]
    #[tokio::test]
    async fn test_sqlite_validate_table_detects_primary_key_mismatch()
    -> Result<(), Box<dyn std::error::Error>> {
        let db = create_db_connection(&_test_common::sqlite_config()).await?;
        db.drop_table::<ValidateTestPrimaryKeyMismatch>()
            .execute()
            .await
            .ok();
        db.execute_sql(
            "CREATE TABLE validate_table_primary_key_mismatch_1 \
             (id INTEGER NOT NULL, name TEXT NOT NULL)",
        )
        .await?;

        let result = db.validate_table::<ValidateTestPrimaryKeyMismatch>().await;
        db.drop_table::<ValidateTestPrimaryKeyMismatch>()
            .execute()
            .await?;
        let error = result.expect_err("primary-key mismatch must be detected");
        assert!(error.to_string().contains("Primary key mismatch"));
        Ok(())
    }

    #[cfg(feature = "postgresql")]
    async fn test_validate_table_hypertable_mismatch_impl(
        config: &DbConfig,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let db = create_db_connection(config).await?;

        db.drop_table::<ValidateTestHypertable>()
            .execute()
            .await
            .ok();
        db.execute_sql(
            r#"
            CREATE TABLE validate_table_hypertable_mismatch_1 (
                id INTEGER PRIMARY KEY,
                created_at TIMESTAMP NOT NULL,
                name VARCHAR(255) NOT NULL
            )
            "#,
        )
        .await?;

        let result = db.validate_table::<ValidateTestHypertable>().await;
        db.drop_table::<ValidateTestHypertable>().execute().await?;

        let error = result
            .expect_err("validate_table should reject a regular table for a hypertable model");
        assert!(
            format!("{error:?}").contains("Hypertable mismatch"),
            "unexpected error: {error:?}"
        );

        Ok(())
    }

    #[cfg(feature = "postgresql")]
    async fn test_validate_table_hypertable_interval_mismatch_impl(
        config: &DbConfig,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let db = create_db_connection(config).await?;

        db.drop_table::<ValidateTestHypertableInterval>()
            .execute()
            .await
            .ok();
        db.execute_sql(
            r#"
            CREATE TABLE validate_table_hypertable_interval_mismatch_1 (
                id INTEGER NOT NULL,
                created_at TIMESTAMP NOT NULL,
                name VARCHAR(255) NOT NULL,
                PRIMARY KEY (id, created_at)
            )
            "#,
        )
        .await?;

        let create_result = db
            .execute_sql(
                "SELECT create_hypertable('validate_table_hypertable_interval_mismatch_1', 'created_at', chunk_time_interval => INTERVAL '2 hours', if_not_exists => TRUE, migrate_data => TRUE)",
            )
            .await;
        if let Err(error) = create_result {
            db.drop_table::<ValidateTestHypertableInterval>()
                .execute()
                .await
                .ok();
            let message = format!("{error:?}").to_lowercase();
            if message.contains("create_hypertable")
                && (message.contains("function") || message.contains("函数"))
            {
                return Ok(());
            }
            return Err(Box::new(error));
        }

        let result = db.validate_table::<ValidateTestHypertableInterval>().await;
        db.drop_table::<ValidateTestHypertableInterval>()
            .execute()
            .await?;

        let error = result.expect_err(
            "validate_table should reject a hypertable with a different chunk interval",
        );
        assert!(
            format!("{error:?}").contains("Hypertable dimension mismatch"),
            "unexpected error: {error:?}"
        );

        Ok(())
    }

    test_on_all_dbs_result!(test_validate_table_success_impl);
    test_on_all_dbs_result!(test_validate_table_not_exists_impl);
    test_on_all_dbs_result!(test_create_table_without_validation_impl);

    #[cfg(feature = "postgresql")]
    #[tokio::test]
    async fn test_postgresql_validate_table_hypertable_mismatch()
    -> Result<(), Box<dyn std::error::Error>> {
        let config = _test_common::postgresql_config();
        test_validate_table_hypertable_mismatch_impl(&config).await
    }

    #[cfg(feature = "postgresql")]
    #[tokio::test]
    async fn test_postgresql_validate_table_hypertable_interval_mismatch()
    -> Result<(), Box<dyn std::error::Error>> {
        let config = _test_common::postgresql_config();
        test_validate_table_hypertable_interval_mismatch_impl(&config).await
    }
}