Skip to main content

safe_migrate/rules/
opaque.rs

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