floz-orm 0.1.7

A lightweight, typesafe Rust ORM — unifying DAO and DSL from a single schema
Documentation
use sqlx::{AnyPool, Row};
use floz_macros_core::model::snapshot::{SnapshotModel, SnapshotField};

/// Structural Fingerprinting Engine entry point.
/// Compares Rust Abstract Syntax Tree snapshots loaded from `.json` 
/// iteratively against `information_schema.columns`.
pub struct Migrator {
    pool: AnyPool,
}

impl Migrator {
    /// Initializes a new Migrator against the provided `sqlx` Database Pool.
    pub fn new(pool: AnyPool) -> Self {
        Self { pool }
    }

    /// Migrate a single table using the Structural Fingerprinting Engine.
    pub async fn migrate_table(&self, table_name: &str, history: &[SnapshotModel]) -> Result<(), String> {
        let live_model: Option<SnapshotModel> = self.introspect_table(table_name).await
            .map_err(|e| format!("Introspection failed: {}", e))?;

        if live_model.is_none() {
            // Table doesn't exist, execute all from v0 to the end.
            println!("Table {} does not exist. Creating from v0.", table_name);
            return self.apply_transitions(None, history).await;
        }
        
        let live_model = live_model.unwrap();
        
        // Fingerprinting Algorithm: Scan backwards
        let mut target_index = None;
        for (i, snapshot) in history.iter().enumerate().rev() {
            if self.is_structural_match(&live_model, snapshot) {
                target_index = Some(i);
                break;
            }
        }

        if let Some(idx) = target_index {
            if idx == history.len() - 1 {
                // We perfectly match the latest!
                return Ok(());
            }
            // Transition up through history
            let upcoming_history = &history[idx..]; 
            return self.apply_transitions(Some(&live_model), upcoming_history).await;
        } else {
            return Err("Error: Database structure does not match any known historical state. Tampering detected.".to_string());
        }
    }

    /// Rollback a single table by one exact structural fingerprint.
    pub async fn rollback_table(&self, table_name: &str, history: &[SnapshotModel]) -> Result<(), String> {
        let live_model: Option<SnapshotModel> = self.introspect_table(table_name).await
            .map_err(|e| format!("Introspection failed: {}", e))?;

        if live_model.is_none() {
            return Err(format!("Table {} does not exist, cannot rollback.", table_name));
        }
        
        let live_model = live_model.unwrap();
        
        let mut target_index = None;
        for (i, snapshot) in history.iter().enumerate().rev() {
            if self.is_structural_match(&live_model, snapshot) {
                target_index = Some(i);
                break;
            }
        }

        if let Some(idx) = target_index {
            if idx == 0 {
                println!("Table {} is already at v0. Rollback would drop it (not implemented automatically for safety).", table_name);
                return Ok(());
            }
            // Transition down 1 version
            let target_version = &history[idx - 1];
            return self.apply_transitions(Some(&live_model), std::slice::from_ref(target_version)).await;
        } else {
            return Err("Error: Database structure does not match any known historical state. Tampering detected.".to_string());
        }
    }

    async fn introspect_table(&self, table_name: &str) -> sqlx::Result<Option<SnapshotModel>> {
        // Query postgres information_schema
        let rows = sqlx::query(
            "SELECT column_name, data_type, is_nullable, character_maximum_length, column_default 
             FROM information_schema.columns 
             WHERE table_name = $1 AND table_schema = 'public'"
        )
        .bind(table_name)
        .fetch_all(&self.pool)
        .await?;

        if rows.is_empty() {
            return Ok(None);
        }

        let mut db_columns = Vec::new();
        for row in rows {
            let col_name: String = row.get("column_name");
            let data_type: String = row.get("data_type");
            // let is_nullable: String = row.get("is_nullable"); // YES / NO
            
            db_columns.push(SnapshotField {
                rust_name: col_name.clone(),
                column_name: col_name,
                type_info: data_type, // Postgres data type
                modifiers: Vec::new(),
                renamed_from: None,
            });
        }

        Ok(Some(SnapshotModel {
            name: table_name.to_string(), // Dummy
            table_name: table_name.to_string(),
            db_columns,
            relationships: Vec::new(),
            constraints: Vec::new(),
        }))
    }

    fn is_structural_match(&self, live: &SnapshotModel, snapshot: &SnapshotModel) -> bool {
        // Simple heuristic: just check if column names match exactly.
        // We can add data_type mapping later for robust type-change diffing.
        if live.db_columns.len() != snapshot.db_columns.len() {
            return false;
        }

        for live_col in &live.db_columns {
            if !snapshot.db_columns.iter().any(|c| c.column_name == live_col.column_name) {
                return false; // Live has a column snapshot doesn't
            }
        }
        true
    }

    async fn apply_transitions(&self, current: Option<&SnapshotModel>, upcoming: &[SnapshotModel]) -> Result<(), String> {
        let mut current_state = current;
        let mut tx = self.pool.begin().await.map_err(|e| e.to_string())?;

        for next_state in upcoming {
            let sqls = self.diff_snapshots(current_state, next_state);
            for sql in sqls {
                println!("INFO: Executing: {}", sql);
                sqlx::query(&sql).execute(&mut *tx).await.map_err(|e| format!("Migration failed: {}\nSQL: {}", e, sql))?;
            }
            current_state = Some(next_state);
        }

        tx.commit().await.map_err(|e| e.to_string())?;
        Ok(())
    }

    /// Analytically computes exact directional `ALTER TABLE` / `CREATE TABLE` translations
    /// between two structural states using the AST Snapshot models.
    pub fn diff_snapshots(&self, a: Option<&SnapshotModel>, b: &SnapshotModel) -> Vec<String> {
        let mut queries = Vec::new();
        let table = &b.table_name;
        
        if let Some(old) = a {
            // Renames
            for new_col in &b.db_columns {
                if let Some(ref old_name) = new_col.renamed_from {
                    queries.push(format!("ALTER TABLE {} RENAME COLUMN {} TO {};", table, old_name, new_col.column_name));
                }
            }
            
            // Drops
            for old_col in &old.db_columns {
                if b.db_columns.iter().any(|c| c.renamed_from.as_ref() == Some(&old_col.column_name)) { continue; }
                if !b.db_columns.iter().any(|c| c.column_name == old_col.column_name) {
                    queries.push(format!("ALTER TABLE {} DROP COLUMN {};", table, old_col.column_name));
                }
            }
            
            // Adds
            for new_col in &b.db_columns {
                if new_col.renamed_from.is_some() { continue; }
                if !old.db_columns.iter().any(|c| c.column_name == new_col.column_name) {
                    let pg_type = self.map_type_to_pg(&new_col.type_info);
                    let mut def = format!("ALTER TABLE {} ADD COLUMN {} {}", table, new_col.column_name, pg_type);
                    if new_col.modifiers.contains(&"Primary".to_string()) { def.push_str(" PRIMARY KEY"); }
                    if new_col.modifiers.contains(&"Unique".to_string()) { def.push_str(" UNIQUE"); }
                    if !new_col.modifiers.contains(&"Nullable".to_string()) { def.push_str(" NOT NULL"); }
                    queries.push(def); 
                }
            }
        } else {
            // Create Table
            let mut defs = Vec::new();
            for col in &b.db_columns {
                let pg_type = self.map_type_to_pg(&col.type_info);
                let mut def = format!("{} {}", col.column_name, pg_type);
                
                if col.modifiers.contains(&"Primary".to_string()) { def.push_str(" PRIMARY KEY"); }
                if col.modifiers.contains(&"AutoIncrement".to_string()) { 
                    def = def.replace("INTEGER", "SERIAL").replace("BIGINT", "BIGSERIAL"); 
                }
                if col.modifiers.contains(&"Unique".to_string()) { def.push_str(" UNIQUE"); }
                if !col.modifiers.contains(&"Nullable".to_string()) { def.push_str(" NOT NULL"); }
                defs.push(def);
            }
            queries.push(format!("CREATE TABLE {} (\n  {}\n);", table, defs.join(",\n  ")));
        }
        
        queries
    }

    /// Fallbacks Rust AST macro-string definitions securely into Native PostgreSQL string syntaxes.
    pub fn map_type_to_pg(&self, ti: &str) -> String {
        if ti.starts_with("Varchar") || ti.starts_with("Decimal") {
            ti.to_uppercase()
        } else {
            match ti {
                "Integer" => "INTEGER".to_string(),
                "BigInt" => "BIGINT".to_string(),
                "Text" => "TEXT".to_string(),
                "DateTime" => "TIMESTAMPTZ".to_string(),
                "Uuid" => "UUID".to_string(),
                "Bool" => "BOOLEAN".to_string(),
                _ => "TEXT".to_string(),
            }
        }
    }
}