bottle-orm 0.5.9

A lightweight and simple ORM for Rust built on top of sqlx
Documentation
use bottle_orm::{Database, Model, ColumnInfo};
use uuid::Uuid;
use std::collections::HashMap;

// Version 1 of the Model
#[derive(Debug, Clone, PartialEq)]
struct UserV1 {
    id: Uuid,
    name: String,
}

impl Model for UserV1 {
    fn table_name() -> &'static str { "users_evolution" }
    fn columns() -> Vec<ColumnInfo> {
        vec![
            ColumnInfo { name: "id", sql_type: "UUID", is_primary_key: true, is_nullable: false, create_time: false, update_time: false, unique: false, index: false, foreign_table: None, foreign_key: None, omit: false, soft_delete: false },
            ColumnInfo { name: "name", sql_type: "TEXT", is_primary_key: false, is_nullable: false, create_time: false, update_time: false, unique: false, index: false, foreign_table: None, foreign_key: None, omit: false, soft_delete: false },
        ]
    }
    fn column_names() -> Vec<String> { vec!["id".to_string(), "name".to_string()] }
    fn active_columns() -> Vec<&'static str> { vec!["id", "name"] }
    fn to_map(&self) -> HashMap<String, Option<String>> {
        let mut map = HashMap::new();
        map.insert("id".to_string(), Some(self.id.to_string()));
        map.insert("name".to_string(), Some(self.name.to_string()));
        map
    }
}

// Version 2 of the Model (Adds 'age' and an index on 'email')
#[derive(Debug, Clone, PartialEq)]
struct UserV2 {
    id: Uuid,
    name: String,
    age: i32,
    email: String,
}

impl Model for UserV2 {
    fn table_name() -> &'static str { "users_evolution" }
    fn columns() -> Vec<ColumnInfo> {
        vec![
            ColumnInfo { name: "id", sql_type: "UUID", is_primary_key: true, is_nullable: false, create_time: false, update_time: false, unique: false, index: false, foreign_table: None, foreign_key: None, omit: false, soft_delete: false },
            ColumnInfo { name: "name", sql_type: "TEXT", is_primary_key: false, is_nullable: false, create_time: false, update_time: false, unique: false, index: false, foreign_table: None, foreign_key: None, omit: false, soft_delete: false },
            ColumnInfo { name: "age", sql_type: "INTEGER", is_primary_key: false, is_nullable: false, create_time: false, update_time: false, unique: false, index: false, foreign_table: None, foreign_key: None, omit: false, soft_delete: false },
            ColumnInfo { name: "email", sql_type: "TEXT", is_primary_key: false, is_nullable: false, create_time: false, update_time: false, unique: false, index: true, foreign_table: None, foreign_key: None, omit: false, soft_delete: false },
        ]
    }
    fn column_names() -> Vec<String> { vec!["id".to_string(), "name".to_string(), "age".to_string(), "email".to_string()] }
    fn active_columns() -> Vec<&'static str> { vec!["id", "name", "age", "email"] }
    fn to_map(&self) -> HashMap<String, Option<String>> {
        let mut map = HashMap::new();
        map.insert("id".to_string(), Some(self.id.to_string()));
        map.insert("name".to_string(), Some(self.name.clone()));
        map.insert("age".to_string(), Some(self.age.to_string()));
        map.insert("email".to_string(), Some(self.email.clone()));
        map
    }
}

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

    // 1. Create V1
    db.sync_table::<UserV1>().await?;
    
    // Insert data into V1
    let id = Uuid::new_v4();
    db.raw("INSERT INTO users_evolution (id, name) VALUES (?, ?)")
        .bind(id.to_string())
        .bind("Alice".to_string())
        .execute().await?;

    // 2. Run diffing for V2
    db.sync_table::<UserV2>().await?;

    // 3. Verify that the new columns exist
    let columns = db.get_table_columns("users_evolution").await?;
    assert!(columns.contains(&"age".to_string()));
    assert!(columns.contains(&"email".to_string()));

    // 4. Try to insert and read with the new Model
    let id2 = Uuid::new_v4();
    db.raw("INSERT INTO users_evolution (id, name, age, email) VALUES (?, ?, ?, ?)")
        .bind(id2.to_string())
        .bind("Bob".to_string())
        .bind(30)
        .bind("bob@example.com".to_string())
        .execute().await?;

    println!("Migration Diffing test passed!");
    Ok(())
}

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

    // 1. Create V1 (without index on name)
    db.sync_table::<UserV1>().await?;
    
    let indexes = db.get_table_indexes("users_evolution").await?;
    // SQLite might have internal indexes for PK, but shouldn't have idx_users_evolution_name
    assert!(!indexes.contains(&"idx_users_evolution_name".to_string()));

    // 2. Define Model V1.5 (with index on name)
    #[derive(Debug, Clone, PartialEq)]
    struct UserV1_5 {
        id: Uuid,
        name: String,
    }

    impl Model for UserV1_5 {
        fn table_name() -> &'static str { "users_evolution" }
        fn columns() -> Vec<ColumnInfo> {
            vec![
                ColumnInfo { name: "id", sql_type: "UUID", is_primary_key: true, is_nullable: false, create_time: false, update_time: false, unique: false, index: false, foreign_table: None, foreign_key: None, omit: false, soft_delete: false },
                ColumnInfo { name: "name", sql_type: "TEXT", is_primary_key: false, is_nullable: false, create_time: false, update_time: false, unique: false, index: true, foreign_table: None, foreign_key: None, omit: false, soft_delete: false },
            ]
        }
        fn column_names() -> Vec<String> { vec!["id".to_string(), "name".to_string()] }
        fn active_columns() -> Vec<&'static str> { vec!["id", "name"] }
        fn to_map(&self) -> HashMap<String, Option<String>> {
            let mut map = HashMap::new();
            map.insert("id".to_string(), Some(self.id.to_string()));
            map.insert("name".to_string(), Some(self.name.to_string()));
            map
        }
    }

    // 3. Run sync
    db.sync_table::<UserV1_5>().await?;

    // 4. Verify if the index was created
    let indexes = db.get_table_indexes("users_evolution").await?;
    assert!(indexes.contains(&"idx_users_evolution_name".to_string()));

    println!("Migration Index Diffing test passed!");
    Ok(())
}