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) = match &op {
40                crate::analysis::mutations::OpaqueMutation::DoBlock => ("DO block", false),
41                crate::analysis::mutations::OpaqueMutation::Execute => ("EXECUTE statement", false),
42                crate::analysis::mutations::OpaqueMutation::DynamicSql => ("Dynamic SQL", false),
43                crate::analysis::mutations::OpaqueMutation::PrepareTransaction => {
44                    ("PREPARE TRANSACTION", false)
45                }
46                crate::analysis::mutations::OpaqueMutation::SetTransaction => {
47                    ("SET TRANSACTION", false)
48                }
49                crate::analysis::mutations::OpaqueMutation::SetConstraints => {
50                    ("SET CONSTRAINTS", false)
51                }
52                crate::analysis::mutations::OpaqueMutation::StateCollision(msg) => {
53                    (msg.as_str(), true)
54                }
55                crate::analysis::mutations::OpaqueMutation::UnresolvedReference { .. } => {
56                    unreachable!()
57                }
58            };
59
60            if is_collision {
61                violations.push(Violation {
62                    source_range: None,
63                    rule_id: "schema-drift",
64                    operation_kind: OperationKind::Conflict,
65                    object_kind: ObjectKind::Opaque,
66                    object_name: "<dynamic>".to_string(),
67                    tier: ViolationTier::Tier1,
68                    reason: format!("Migration state conflict: {}", block_type),
69                    recipe: "This migration attempts to create an object that already exists, or alter an object that does not. The simulated state has derailed.",
70                    dedup_key: None,
71                    sql: None,
72                    fk_dependency_related: false,
73                });
74            } else {
75                violations.push(Violation {
76                    source_range: None,
77                    rule_id: self.id(),
78                    operation_kind: OperationKind::OpaqueSql,
79                    object_kind: ObjectKind::Opaque,
80                    object_name: "<dynamic>".to_string(),
81                    tier: self.default_tier(),
82                    reason: format!("Encountered opaque {}", block_type),
83                    recipe: self.recipe(),
84                    dedup_key: None,
85                    sql: None,
86                    fk_dependency_related: false,
87                });
88            }
89        }
90
91        violations
92    }
93}