Skip to main content

safe_migrate/rules/
opaque.rs

1// FILE: src/rules/opaque.rs
2use crate::analysis::mutations::Mutation;
3use crate::analysis::state::{AnalysisState, CascadeResult, MutationResult};
4use crate::engine::config::Config;
5use crate::report::violations::{ObjectKind, OperationKind, Violation, ViolationTier};
6use crate::rules::Rule;
7
8pub struct OpaqueDynamicSqlRule;
9
10impl Rule for OpaqueDynamicSqlRule {
11    fn id(&self) -> &'static str {
12        "opaque-dynamic-sql"
13    }
14    fn default_tier(&self) -> ViolationTier {
15        ViolationTier::Tier2
16    }
17    fn recipe(&self) -> &'static str {
18        "Procedural or dynamic SQL (DO blocks, EXECUTE) obscures schema mutations. Lock analysis confidence is heavily degraded."
19    }
20
21    fn evaluate(
22        &self,
23        mutation: &Mutation,
24        _result: &MutationResult,
25        _pre_state: &crate::analysis::state::PreState,
26        _state: &AnalysisState,
27        _config: &Config,
28        _cascade: Option<&CascadeResult>,
29    ) -> Vec<Violation> {
30        let mut violations = Vec::new();
31
32        if let Mutation::Opaque(op) = mutation {
33            if matches!(
34                op,
35                crate::analysis::mutations::OpaqueMutation::UnresolvedReference { .. }
36            ) {
37                return vec![];
38            }
39            let (block_type, is_collision, recipe) = match &op {
40                crate::analysis::mutations::OpaqueMutation::UnsupportedStatement => (
41                    "unsupported SQL statement",
42                    false,
43                    "This SQL statement is not modeled. Review its PostgreSQL behavior before deploying.",
44                ),
45                crate::analysis::mutations::OpaqueMutation::DoBlock => {
46                    ("DO block", false, self.recipe())
47                }
48                crate::analysis::mutations::OpaqueMutation::Execute => {
49                    ("EXECUTE statement", false, self.recipe())
50                }
51                crate::analysis::mutations::OpaqueMutation::DynamicSql => {
52                    ("Dynamic SQL", false, self.recipe())
53                }
54                crate::analysis::mutations::OpaqueMutation::PrepareTransaction => {
55                    ("PREPARE TRANSACTION", false, self.recipe())
56                }
57                crate::analysis::mutations::OpaqueMutation::SetTransaction => {
58                    ("SET TRANSACTION", false, self.recipe())
59                }
60                crate::analysis::mutations::OpaqueMutation::SetConstraints => {
61                    ("SET CONSTRAINTS", false, self.recipe())
62                }
63                crate::analysis::mutations::OpaqueMutation::StateCollision(msg) => {
64                    (msg.as_str(), true, self.recipe())
65                }
66                crate::analysis::mutations::OpaqueMutation::UnresolvedReference { .. } => {
67                    unreachable!()
68                }
69            };
70
71            if is_collision {
72                violations.push(Violation {
73                    source_range: None,
74                    rule_id: "schema-drift",
75                    operation_kind: OperationKind::Conflict,
76                    object_kind: ObjectKind::Opaque,
77                    object_name: "<dynamic>".to_string(),
78                    tier: ViolationTier::Tier1,
79                    reason: format!("Migration state conflict: {}", block_type),
80                    recipe: "This migration attempts to create an object that already exists, or alter an object that does not. The simulated state has derailed.",
81                    dedup_key: None,
82                    sql: None,
83                    fk_dependency_related: false,
84                });
85            } else {
86                violations.push(Violation {
87                    source_range: None,
88                    rule_id: self.id(),
89                    operation_kind: OperationKind::OpaqueSql,
90                    object_kind: ObjectKind::Opaque,
91                    object_name: "<dynamic>".to_string(),
92                    tier: self.default_tier(),
93                    reason: format!("Encountered opaque {}", block_type),
94                    recipe,
95                    dedup_key: None,
96                    sql: None,
97                    fk_dependency_related: false,
98                });
99            }
100        }
101
102        violations
103    }
104}