safe_migrate/rules/
opaque.rs1use 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}