sql-splitter 1.13.1

High-performance CLI tool for splitting large SQL dump files into individual table files
Documentation
//! SQL migration output formatter for diff results.
//!
//! Generates DDL statements for schema changes.

use crate::differ::DiffResult;
use crate::parser::SqlDialect;

/// Format diff result as SQL migration script
pub fn format_sql(result: &DiffResult, dialect: SqlDialect) -> String {
    let mut output = String::new();

    output.push_str("-- SQL Migration Script\n");
    output.push_str(&format!(
        "-- Generated by sql-splitter diff (dialect: {})\n",
        dialect
    ));
    output.push_str("--\n\n");

    let Some(ref schema) = result.schema else {
        output.push_str("-- No schema changes detected\n");
        return output;
    };

    if !schema.has_changes() {
        output.push_str("-- No schema changes detected\n");
        return output;
    }

    // Quote identifier based on dialect
    let quote = |name: &str| -> String {
        match dialect {
            SqlDialect::MySql => format!("`{}`", name),
            SqlDialect::Postgres | SqlDialect::Sqlite => format!("\"{}\"", name),
            SqlDialect::Mssql => format!("[{}]", name),
        }
    };

    // New tables - use CREATE TABLE statement if available
    for table in &schema.tables_added {
        output.push_str(&format!("-- New table: {}\n", table.name));
        if let Some(ref stmt) = table.create_statement {
            output.push_str(stmt.trim());
            if !stmt.trim().ends_with(';') {
                output.push(';');
            }
            output.push_str("\n\n");
        } else {
            // Generate CREATE TABLE from column info
            output.push_str(&format!("CREATE TABLE {} (\n", quote(&table.name)));

            let col_defs: Vec<String> = table
                .columns
                .iter()
                .map(|col| {
                    let nullable = if col.is_nullable { "" } else { " NOT NULL" };
                    format!("  {} {}{}", quote(&col.name), col.col_type, nullable)
                })
                .collect();

            output.push_str(&col_defs.join(",\n"));

            if !table.primary_key.is_empty() {
                let pk_cols: Vec<String> = table.primary_key.iter().map(|c| quote(c)).collect();
                output.push_str(&format!(",\n  PRIMARY KEY ({})", pk_cols.join(", ")));
            }

            output.push_str("\n);\n\n");
        }
    }

    // Removed tables
    for table_name in &schema.tables_removed {
        output.push_str(&format!("-- Removed table: {}\n", table_name));
        output.push_str(&format!("DROP TABLE IF EXISTS {};\n\n", quote(table_name)));
    }

    // Modified tables
    for modification in &schema.tables_modified {
        let has_changes = !modification.columns_added.is_empty()
            || !modification.columns_removed.is_empty()
            || !modification.columns_modified.is_empty()
            || !modification.fks_added.is_empty()
            || !modification.fks_removed.is_empty()
            || !modification.indexes_added.is_empty()
            || !modification.indexes_removed.is_empty();

        if !has_changes && !modification.pk_changed {
            continue;
        }

        output.push_str(&format!("-- Modified table: {}\n", modification.table_name));

        // Add columns
        for col in &modification.columns_added {
            let nullable = if col.is_nullable { "" } else { " NOT NULL" };
            output.push_str(&format!(
                "ALTER TABLE {} ADD COLUMN {} {}{};\n",
                quote(&modification.table_name),
                quote(&col.name),
                col.col_type,
                nullable
            ));
        }

        // Drop columns
        for col in &modification.columns_removed {
            output.push_str(&format!(
                "ALTER TABLE {} DROP COLUMN {};\n",
                quote(&modification.table_name),
                quote(&col.name)
            ));
        }

        // Modify columns
        for change in &modification.columns_modified {
            let new_type = change.new_type.as_deref().unwrap_or("-- unknown type");
            let nullable = match change.new_nullable {
                Some(true) => "",
                Some(false) => " NOT NULL",
                None => "",
            };

            match dialect {
                SqlDialect::MySql => {
                    output.push_str(&format!(
                        "ALTER TABLE {} MODIFY COLUMN {} {}{};\n",
                        quote(&modification.table_name),
                        quote(&change.name),
                        new_type,
                        nullable
                    ));
                }
                SqlDialect::Postgres => {
                    // PostgreSQL uses separate commands for type and nullability
                    if change.new_type.is_some() {
                        output.push_str(&format!(
                            "ALTER TABLE {} ALTER COLUMN {} TYPE {};\n",
                            quote(&modification.table_name),
                            quote(&change.name),
                            new_type
                        ));
                    }
                    if let Some(is_nullable) = change.new_nullable {
                        let constraint = if is_nullable {
                            "DROP NOT NULL"
                        } else {
                            "SET NOT NULL"
                        };
                        output.push_str(&format!(
                            "ALTER TABLE {} ALTER COLUMN {} {};\n",
                            quote(&modification.table_name),
                            quote(&change.name),
                            constraint
                        ));
                    }
                }
                SqlDialect::Sqlite => {
                    output.push_str(&format!(
                        "-- SQLite does not support ALTER COLUMN; manual migration required for {}.{}\n",
                        modification.table_name, change.name
                    ));
                }
                SqlDialect::Mssql => {
                    output.push_str(&format!(
                        "ALTER TABLE {} ALTER COLUMN {} {}{};\n",
                        quote(&modification.table_name),
                        quote(&change.name),
                        new_type,
                        nullable
                    ));
                }
            }
        }

        // Add foreign keys
        for fk in &modification.fks_added {
            let fk_cols: Vec<String> = fk.columns.iter().map(|c| quote(c)).collect();
            let ref_cols: Vec<String> = fk.referenced_columns.iter().map(|c| quote(c)).collect();

            let constraint_name = fk
                .name
                .as_ref()
                .map(|n| format!("CONSTRAINT {} ", quote(n)))
                .unwrap_or_default();

            output.push_str(&format!(
                "ALTER TABLE {} ADD {}FOREIGN KEY ({}) REFERENCES {}({});\n",
                quote(&modification.table_name),
                constraint_name,
                fk_cols.join(", "),
                quote(&fk.referenced_table),
                ref_cols.join(", ")
            ));
        }

        // Drop foreign keys
        for fk in &modification.fks_removed {
            if let Some(ref name) = fk.name {
                match dialect {
                    SqlDialect::MySql => {
                        output.push_str(&format!(
                            "ALTER TABLE {} DROP FOREIGN KEY {};\n",
                            quote(&modification.table_name),
                            quote(name)
                        ));
                    }
                    SqlDialect::Postgres | SqlDialect::Mssql => {
                        output.push_str(&format!(
                            "ALTER TABLE {} DROP CONSTRAINT {};\n",
                            quote(&modification.table_name),
                            quote(name)
                        ));
                    }
                    SqlDialect::Sqlite => {
                        output.push_str(&format!(
                            "-- SQLite does not support DROP CONSTRAINT; manual migration required for FK {}\n",
                            name
                        ));
                    }
                }
            } else {
                output.push_str(&format!(
                    "-- Cannot drop unnamed FK ({}) -> {}.({}) without constraint name\n",
                    fk.columns.join(", "),
                    fk.referenced_table,
                    fk.referenced_columns.join(", ")
                ));
            }
        }

        // Add indexes
        for idx in &modification.indexes_added {
            let unique = if idx.is_unique { "UNIQUE " } else { "" };
            let idx_cols: Vec<String> = idx.columns.iter().map(|c| quote(c)).collect();

            match dialect {
                SqlDialect::Postgres => {
                    let using = idx
                        .index_type
                        .as_ref()
                        .map(|t| format!(" USING {}", t.to_lowercase()))
                        .unwrap_or_default();
                    output.push_str(&format!(
                        "CREATE {}INDEX {} ON {}{}({});\n",
                        unique,
                        quote(&idx.name),
                        quote(&modification.table_name),
                        using,
                        idx_cols.join(", ")
                    ));
                }
                _ => {
                    output.push_str(&format!(
                        "CREATE {}INDEX {} ON {}({});\n",
                        unique,
                        quote(&idx.name),
                        quote(&modification.table_name),
                        idx_cols.join(", ")
                    ));
                }
            }
        }

        // Drop indexes
        for idx in &modification.indexes_removed {
            match dialect {
                SqlDialect::MySql => {
                    output.push_str(&format!(
                        "DROP INDEX {} ON {};\n",
                        quote(&idx.name),
                        quote(&modification.table_name)
                    ));
                }
                SqlDialect::Postgres | SqlDialect::Sqlite => {
                    output.push_str(&format!("DROP INDEX IF EXISTS {};\n", quote(&idx.name)));
                }
                SqlDialect::Mssql => {
                    output.push_str(&format!(
                        "DROP INDEX {} ON {};\n",
                        quote(&idx.name),
                        quote(&modification.table_name)
                    ));
                }
            }
        }

        // PK changes (complex - often requires table recreation)
        if modification.pk_changed {
            let old_pk = modification
                .old_pk
                .as_ref()
                .map(|pk| pk.join(", "))
                .unwrap_or_else(|| "(none)".to_string());
            let new_pk = modification
                .new_pk
                .as_ref()
                .map(|pk| pk.join(", "))
                .unwrap_or_else(|| "(none)".to_string());

            output.push_str(&format!(
                "-- WARNING: PRIMARY KEY change ({}) -> ({}) may require table recreation\n",
                old_pk, new_pk
            ));

            match dialect {
                SqlDialect::MySql => {
                    output.push_str(&format!(
                        "ALTER TABLE {} DROP PRIMARY KEY;\n",
                        quote(&modification.table_name)
                    ));
                    if let Some(ref new_pk_cols) = modification.new_pk {
                        if !new_pk_cols.is_empty() {
                            let pk_cols: Vec<String> =
                                new_pk_cols.iter().map(|c| quote(c)).collect();
                            output.push_str(&format!(
                                "ALTER TABLE {} ADD PRIMARY KEY ({});\n",
                                quote(&modification.table_name),
                                pk_cols.join(", ")
                            ));
                        }
                    }
                }
                SqlDialect::Postgres | SqlDialect::Mssql => {
                    output.push_str(&format!(
                        "ALTER TABLE {} DROP CONSTRAINT IF EXISTS {}_pkey;\n",
                        quote(&modification.table_name),
                        modification.table_name
                    ));
                    if let Some(ref new_pk_cols) = modification.new_pk {
                        if !new_pk_cols.is_empty() {
                            let pk_cols: Vec<String> =
                                new_pk_cols.iter().map(|c| quote(c)).collect();
                            output.push_str(&format!(
                                "ALTER TABLE {} ADD PRIMARY KEY ({});\n",
                                quote(&modification.table_name),
                                pk_cols.join(", ")
                            ));
                        }
                    }
                }
                SqlDialect::Sqlite => {
                    output.push_str(
                        "-- SQLite does not support ALTER PRIMARY KEY; table recreation required\n",
                    );
                }
            }
        }

        output.push('\n');
    }

    // Note about data changes
    if let Some(ref data) = result.data {
        let has_data_changes = data
            .tables
            .values()
            .any(|t| t.added_count > 0 || t.removed_count > 0 || t.modified_count > 0);

        if has_data_changes {
            output.push_str("-- Data changes detected but not included in migration script\n");
            output.push_str("-- Use text or JSON output format for data diff details\n");
        }
    }

    output
}