sql-schema 0.6.2

Declarative SQL schema migrations
Documentation
use sqlparser::ast::{
    AlterTable, AlterTableOperation, AlterType, ColumnDef, CreateIndex, CreateTable, ObjectName,
    ObjectType, RenameTableNameKind, Statement,
};

use crate::SyntaxTree;

#[bon::builder(finish_fn = build)]
pub fn generate_name(
    #[builder(start_fn)] tree: &SyntaxTree,
    max_len: Option<usize>,
) -> Option<String> {
    let mut parts = tree
        .0
        .iter()
        .filter_map(|s| match s {
            Statement::CreateTable(CreateTable { name, .. }) => Some(format!("create_{name}")),
            Statement::AlterTable(AlterTable {
                name, operations, ..
            }) => alter_table_name(name, operations),
            Statement::Drop {
                object_type, names, ..
            } => {
                let object_type = match object_type {
                    ObjectType::Table => String::new(),
                    _ => object_type.to_string().to_lowercase() + "_",
                };
                let names = names
                    .iter()
                    .map(ToString::to_string)
                    .collect::<Vec<String>>()
                    .join("_and_");
                Some(format!("drop_{object_type}{names}"))
            }
            Statement::CreateType { name, .. } => Some(format!("create_type_{name}")),
            Statement::AlterType(AlterType { name, .. }) => Some(format!("alter_type_{name}")),
            Statement::CreateIndex(CreateIndex {
                name, table_name, ..
            }) => {
                let name = name.as_ref().map(|n| format!("_{n}")).unwrap_or_default();
                Some(format!("create_{table_name}{name}"))
            }
            _ => None,
        })
        .collect::<Vec<_>>();

    let mut suffix = None;
    let mut name = parts.join("__");
    let max_len = max_len.unwrap_or(50);
    while name.len() > max_len {
        suffix = Some("etc");
        parts.pop();
        name = parts.join("__");
    }

    if let Some(suffix) = suffix {
        name = format!("{name}__{suffix}");
    }

    if name.is_empty() {
        None
    } else {
        Some(name)
    }
}

fn alter_table_name(name: &ObjectName, operations: &[AlterTableOperation]) -> Option<String> {
    let mut table_verb = "alter";
    let ops = operations
        .iter()
        .filter_map(|op| match op {
            AlterTableOperation::AddColumn {
                column_def: ColumnDef { name, .. },
                ..
            } => Some(format!("add_{name}")),
            AlterTableOperation::DropColumn { column_names, .. } => Some(format!(
                "drop_{}",
                column_names
                    .iter()
                    .map(|ident| ident.value.clone())
                    .collect::<Vec<_>>()
                    .join("_")
            )),
            AlterTableOperation::RenameColumn {
                old_column_name,
                new_column_name,
            } => Some(format!("rename_{old_column_name}_to_{new_column_name}")),
            AlterTableOperation::AlterColumn { column_name, .. } => {
                Some(format!("alter_{column_name}"))
            }
            AlterTableOperation::RenameTable { table_name } => {
                table_verb = "rename";
                Some(format!(
                    "to_{table_name}",
                    table_name = match table_name {
                        RenameTableNameKind::As(name) => name,
                        RenameTableNameKind::To(name) => name,
                    }
                ))
            }
            _ => None,
        })
        .collect::<Vec<_>>();

    Some(if ops.is_empty() || ops.len() > 2 {
        format!("{table_verb}_{name}")
    } else {
        format!("{table_verb}_{name}_{}", ops.join("_"))
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[derive(Debug)]
    struct TestCase {
        sql: &'static str,
        name: &'static str,
    }

    fn run_test_case(tc: &TestCase) {
        let tree = SyntaxTree::builder().sql(tc.sql).build().unwrap();
        let actual = generate_name(&tree).build();
        assert_eq!(actual, Some(tc.name.to_owned()), "{tc:?}");
    }

    fn run_test_cases(test_cases: Vec<TestCase>) {
        test_cases.iter().for_each(run_test_case);
    }

    #[test]
    fn test_generate_name() {
        run_test_cases(vec![
            TestCase {
                sql: "CREATE TABLE foo(bar TEXT);",
                name: "create_foo",
            },
            TestCase {
                sql: "CREATE TABLE foo(bar TEXT); CREATE TABLE bar(foo TEXT);",
                name: "create_foo__create_bar",
            },
            TestCase {
                sql: "CREATE TABLE foo(bar TEXT); CREATE TABLE bar(foo TEXT); CREATE TABLE baz(id INT); CREATE TABLE some_really_long_name(id INT);",
                name: "create_foo__create_bar__create_baz__etc",
            },
            TestCase {
                sql: "ALTER TABLE foo DROP COLUMN bar;",
                name: "alter_foo_drop_bar",
            },
            TestCase {
                sql: "ALTER TABLE foo ADD COLUMN bar TEXT;",
                name: "alter_foo_add_bar",
            },
            TestCase {
                sql: "ALTER TABLE foo ALTER COLUMN bar SET DATA TYPE INT;",
                name: "alter_foo_alter_bar",
            },
            TestCase {
                sql: "ALTER TABLE foo RENAME bar TO id;",
                name: "alter_foo_rename_bar_to_id",
            },
            TestCase {
                sql: "ALTER TABLE foo RENAME TO bar;",
                name: "rename_foo_to_bar",
            },
            TestCase {
                sql: "DROP TABLE foo;",
                name: "drop_foo",
            },
            TestCase {
                sql: "CREATE TYPE status AS ENUM('one', 'two', 'three');",
                name: "create_type_status",
            },
            TestCase {
                sql: "DROP TYPE status;",
                name: "drop_type_status",
            },
            TestCase {
                sql: "CREATE UNIQUE INDEX title_idx ON films (title);",
                name: "create_films_title_idx",
            },
            TestCase {
                sql: "DROP INDEX title_idx",
                name: "drop_index_title_idx",
            },
        ]);
    }
}