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