dbschema 0.1.0

Define database schema's as HCL files, and generate idempotent SQL migrations
Documentation
use super::{LintCheck, LintMessage, LintSeverity};
use crate::ir::{Config, ForeignKeySpec};

pub struct DestructiveChange;

impl DestructiveChange {
    fn ignored(ignores: &[String], rule: &str) -> bool {
        ignores.iter().any(|i| i == rule)
    }

    fn is_destructive_fk(fk: &ForeignKeySpec) -> bool {
        matches!(fk.on_delete.as_deref(), Some(action) if action.eq_ignore_ascii_case("cascade"))
            || matches!(fk.on_update.as_deref(), Some(action) if action.eq_ignore_ascii_case("cascade"))
    }
}

impl LintCheck for DestructiveChange {
    fn name(&self) -> &'static str {
        "destructive-change"
    }

    fn run(&self, cfg: &Config) -> Vec<LintMessage> {
        let mut msgs = Vec::new();
        for table in &cfg.tables {
            if Self::ignored(&table.lint_ignore, self.name()) {
                continue;
            }
            for fk in &table.foreign_keys {
                if Self::is_destructive_fk(fk) {
                    msgs.push(LintMessage {
                        check: self.name(),
                        message: format!(
                            "foreign key on '{}.{}' uses CASCADE action",
                            table.name,
                            fk.columns.join(",")
                        ),
                        severity: LintSeverity::Error,
                    });
                }
            }
        }
        msgs
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ir::{ColumnSpec, Config, ForeignKeySpec, PrimaryKeySpec, TableSpec};
    use crate::lint::{LintSettings, run_with_checks};

    #[test]
    fn detects_cascade_fk() {
        let table = TableSpec {
            name: "t".into(),
            alt_name: None,
            schema: None,
            if_not_exists: false,
            columns: vec![ColumnSpec {
                name: "id".into(),
                r#type: "int".into(),
                nullable: false,
                default: None,
                db_type: None,
                lint_ignore: vec![],
                comment: None,
                count: 1,
            }],
            primary_key: Some(PrimaryKeySpec {
                name: None,
                columns: vec!["id".into()],
            }),
            indexes: vec![],
            checks: vec![],
            foreign_keys: vec![ForeignKeySpec {
                name: None,
                columns: vec!["id".into()],
                ref_schema: None,
                ref_table: "other".into(),
                ref_columns: vec!["id".into()],
                on_delete: Some("cascade".into()),
                on_update: None,
                back_reference_name: None,
            }],
            partition_by: None,
            partitions: vec![],
            back_references: vec![],
            lint_ignore: vec![],
            comment: None,
            map: None,
        };
        let cfg = Config {
            tables: vec![table],
            ..Default::default()
        };
        let msgs = run_with_checks(
            &cfg,
            vec![Box::new(DestructiveChange)],
            &LintSettings::default(),
        );
        assert!(msgs.iter().any(|m| m.check == "destructive-change"));
    }
}