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) = 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}