schema-model 0.2.0

A set of tools to manage relational database schemas
Documentation
use crate::model::enum_type::EnumType;
use crate::model::function::Function;
use crate::model::other_sql::OtherSql;
use crate::model::procedure::Procedure;
use crate::model::table::Table;
use crate::model::types::{DatabaseType, RelationType};
use crate::model::view::View;
use std::collections::HashMap;

#[derive(Debug)]
pub struct Schema {
    schema_name: Option<String>,
    case_sensitive_text: bool,
    tables: Vec<Table>,
    views: Vec<View>,
    functions: Vec<Function>,
    procedures: Vec<Procedure>,
    other_sql: Vec<OtherSql>,
    // Case-insensitive map: store lowercase name -> index in tables vec
    table_map: HashMap<String, usize>,
    enum_types: HashMap<String, EnumType>,
}

impl Default for Schema {
    fn default() -> Self {
        Self {
            schema_name: None,
            case_sensitive_text: true,
            tables: Vec::new(),
            views: Vec::new(),
            functions: Vec::new(),
            procedures: Vec::new(),
            other_sql: Vec::new(),
            table_map: HashMap::new(),
            enum_types: HashMap::new(),
        }
    }
}

impl Schema {
    pub fn new<S: Into<String>>(schema_name: Option<S>) -> Self {
        Self {
            schema_name: schema_name.map(|s| s.into()),
            ..Default::default()
        }
    }

    pub fn schema_name(&self) -> Option<&str> {
        self.schema_name.as_deref()
    }

    pub fn case_sensitive_text(&self) -> bool {
        self.case_sensitive_text
    }

    pub fn set_case_sensitive_text(&mut self, value: bool) {
        self.case_sensitive_text = value;
    }

    pub fn tables(&self) -> &[Table] {
        &self.tables
    }

    pub fn get_table(&self, name: &str) -> &Table {
        let idx = self.table_index(name);
        &self.tables[idx]
    }

    pub(crate) fn get_table_mut(&mut self, name: &str) -> &mut Table {
        let idx = self.table_index(name);
        &mut self.tables[idx]
    }

    fn table_index(&self, name: &str) -> usize {
        let name_lower = name.to_lowercase();
        *self.table_map.get(&name_lower)
            .unwrap_or_else(|| panic!("Unable to locate a table with the name '{}'", name))
    }

    pub fn get_optional_table(&self, name: &str) -> Option<&Table> {
        let name_lower = name.to_lowercase();
        self.table_map.get(&name_lower).map(|&idx| &self.tables[idx])
    }

    pub fn all_views(&self) -> &[View] {
        &self.views
    }

    pub fn views(&self, database_type: DatabaseType) -> Vec<View> {
        self.views
            .iter()
            .filter(|view| view.database_type().is_none() || view.database_type().unwrap() == database_type)
            .cloned()
            .collect()
    }

    pub fn enum_types(&self) -> impl Iterator<Item = &EnumType> {
        self.enum_types.values()
    }

    pub fn get_enum_type(&self, type_name: &str) -> &EnumType {
        self.enum_types
            .get(type_name)
            .unwrap_or_else(|| panic!("Unable to locate an enum type with the name '{}'", type_name))
    }

    pub fn functions(&self) -> &[Function] {
        &self.functions
    }

    pub fn procedures(&self) -> &[Procedure] {
        &self.procedures
    }

    pub fn other_sql(&self) -> &[OtherSql] {
        &self.other_sql
    }

    pub fn validate(&self) -> Vec<String> {
        let mut errors: Vec<String> = Vec::new();
        for table in &self.tables {
            for relation in table.relations() {
                if relation.relation_type() == RelationType::SetNull {
                    let from_table_name = relation.from_table_name().to_string();
                    let from_column_name = relation.from_column_name().to_string();
                    let from_table = self.get_table(&from_table_name);
                    if from_table.column(&from_column_name).is_required() {
                        errors.push(format!(
                            "ERROR: {}.{} is required. The {}.{} relation specifies setnull, which is not allowed",
                            from_table_name,
                            from_column_name,
                            relation.to_table_name(),
                            relation.to_column_name()
                        ));
                    }
                }
            }
        }
        errors
    }

    // pub fn build_reverse_relations(&mut self) {
    //     // We need mutable access to parent tables too, so handle indices carefully.
    //     // First, collect the relations to add per parent table to avoid multiple mutable borrows.
    //     let mut to_add: HashMap<usize, Vec<Relation>> = HashMap::new();
    //     for (child_idx, table) in self.tables.iter().enumerate() {
    //         if !table.relations().is_empty() {
    //             for relation in table.relations() {
    //                 let parent_name = relation.to_table_name();
    //                 if let Some(&parent_idx) = self.table_map.get(&parent_name.to_lowercase()) {
    //                     let reverse = Relation::new(
    //                         relation.to_table_name().to_string(),
    //                         relation.to_column_name().to_string(),
    //                         relation.from_table_name().to_string(),
    //                         relation.from_column_name().to_string(),
    //                         relation.relation_type(),
    //                         false,
    //                     );
    //                     to_add.entry(parent_idx).or_default().push(reverse);
    //                 } else {
    //                     // Parent not found; ignore or log in real implementation
    //                     let _ = child_idx; // keep variable used
    //                 }
    //             }
    //         }
    //     }
    //     for (idx, rels) in to_add {
    //         if let Some(parent) = self.tables.get_mut(idx) {
    //             parent.reverse_relations_mut().extend(rels);
    //         }
    //     }
    // }

    pub(crate) fn add_table(&mut self, table: Table) {
        let idx = self.tables.len();
        self.table_map.insert(table.name().to_lowercase(), idx);
        self.tables.push(table);
    }

    pub(crate) fn add_view(&mut self, view: View) {
        self.views.push(view);
    }

    pub(crate) fn add_enum_type(&mut self, enum_type: EnumType) {
        self.enum_types
            .insert(enum_type.name().to_string(), enum_type);
    }

    pub(crate) fn add_functions(&mut self, functions: Vec<Function>) {
        self.functions.extend(functions);
    }

    pub(crate) fn add_procedures(&mut self, procedures: Vec<Procedure>) {
        self.procedures.extend(procedures);
    }

    pub(crate) fn add_other_sql(&mut self, other_sql: OtherSql) {
        self.other_sql.push(other_sql);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::model::column::Column;
    use crate::model::column_type::ColumnType;
    use crate::model::relation::Relation;

    fn make_schema() -> Schema {
        Schema::new(Some("schema"))
    }

    #[test]
    fn add_and_get_table_and_sort() {
        let mut schema = make_schema();
        let table1 = Table::new(
            Some("schema"),
            "Table1",
            Option::<&str>::None,
            crate::model::types::LockEscalation::Auto,
            false,
            vec![Column::new(Some("s"), "id", ColumnType::Int, 0, 0, true)],
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
        );
        let table2 = Table::new(
            Some("schema"),
            "Table2",
            Option::<&str>::None,
            crate::model::types::LockEscalation::Auto,
            false,
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
        );
        schema.add_table(table1);
        schema.add_table(table2);
        assert_eq!(schema.get_table("Table2").name(), "Table2"); // case-insensitive
        let names: Vec<_> = schema.tables().iter().map(|t| t.name()).collect();
        assert_eq!(names, vec!["Table1", "Table2"]);
        // table_map rebuilt so get_table still works
        assert_eq!(schema.get_table("Table1").name(), "Table1");
    }

    #[test]
    fn views_filtered_by_database_type() {
        let mut s = make_schema();
        s.add_view(View::new(Some("s"), "v1", "sql1", Some(DatabaseType::Postgresql)));
        s.add_view(View::new(Some("s"), "v2", "sql2", Some(DatabaseType::SqlServer)));
        let pg = s.views(DatabaseType::Postgresql);
        assert_eq!(pg.len(), 1);
        assert_eq!(pg[0].name(), "v1");
    }

    #[test]
    fn validate_setnull_error_when_required() {
        let mut s = make_schema();
        let parent = Table::new(
            Some("s"),
            "parent",
            Option::<&str>::None,
            crate::model::types::LockEscalation::Auto,
            false,
            vec![Column::new(Some("s"), "id", ColumnType::Int, 0, 0, true)],
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
        );
        s.add_table(parent);

        let child = Table::new(
            Some("s"),
            "child",
            Option::<&str>::None,
            crate::model::types::LockEscalation::Auto,
            false,
            vec![Column::new(Some("s"), "pid", ColumnType::Int, 0, 0, true)],
            Vec::new(),
            Vec::new(),
            vec![Relation::new(
                "parent",
                "id",
                "child",
                "pid",
                RelationType::SetNull,
                false,
            )],
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
            Vec::new(),
        );
        s.add_table(child);

        let errors = s.validate();
        assert_eq!(errors.len(), 1);
        assert!(errors[0].contains("setnull"));
    }

    // #[test]
    // fn build_reverse_relations_creates_back_refs() {
    //     let mut s = make_schema();
    //     let mut parent = Table::new(
    //         Some("s"),
    //         "p",
    //         Option::<&str>::None,
    //         crate::model::types::LockEscalation::Auto,
    //         false,
    //         vec![Column::new(Some("s"), "id", ColumnType::Int, 0, 0, true)],
    //         Vec::new(),
    //         Vec::new(),
    //         Vec::new(),
    //         Vec::new(),
    //         Vec::new(),
    //         Vec::new(),
    //         Vec::new(),
    //         Vec::new(),
    //     );
    //     let mut child = Table::new(
    //         Some("s"),
    //         "c",
    //         Option::<&str>::None,
    //         crate::model::types::LockEscalation::Auto,
    //         false,
    //         vec![Column::new(Some("s"), "pid", ColumnType::Int, 0, 0, false)],
    //         Vec::new(),
    //         Vec::new(),
    //         vec![Relation::new(
    //             "p",
    //             "id",
    //             "c",
    //             "pid",
    //             RelationType::Cascade,
    //             false,
    //         )],
    //         Vec::new(),
    //         Vec::new(),
    //         Vec::new(),
    //         Vec::new(),
    //         Vec::new(),
    //     );
    //     s.add_table(parent);
    //     s.add_table(child);
    //
    //     s.build_reverse_relations();
    //     let p_ref = s.get_table("p");
    //     assert_eq!(p_ref.reverse_relations().len(), 1);
    //     let rr = &p_ref.reverse_relations()[0];
    //     assert_eq!(rr.from_table_name(), "c");
    //     assert_eq!(rr.to_table_name(), "p");
    // }
}