Skip to main content

safe_migrate/rules/
registry.rs

1use crate::report::violations::ViolationTier;
2use crate::rules::Rule;
3use crate::rules::conflict::ConflictRule;
4use crate::rules::constraints::BlockingConstraintRule;
5use crate::rules::destructive::{
6    CascadingDropRule, CreateTableAsSelectRule, DropDatabaseRule, DropSchemaCascadeRule,
7    GeneralCascadeRule, ReversibilityRule, SizeAwareAddColumnRule, TypeChangeRewriteRule,
8};
9use crate::rules::drift::DriftDetectionRule;
10use crate::rules::expressions::VolatileDefaultRule;
11use crate::rules::functions::{BrokenComputeRule, FunctionVolatilityRule};
12use crate::rules::idempotency::IdempotencyRule;
13use crate::rules::indexes::ConcurrentIndexRule;
14use crate::rules::opaque::OpaqueDynamicSqlRule;
15use crate::rules::partitions::{PartitionLockRule, PartitionStrategyMismatchRule};
16use crate::rules::policies::RestrictivePolicyRule;
17use crate::rules::security::OverbroadGrantRule;
18use crate::rules::transactions::{
19    AlterTypeAddValueRule, ConcurrentInsideTransactionRule, VacuumFullRule,
20};
21use crate::rules::triggers::DisableTriggerRule;
22use crate::rules::views::MaterializedViewRefreshRule;
23
24/// Stable user-facing metadata and construction for one primary rule.
25///
26/// Keep this registry in evaluation order. It is the canonical source for
27/// rule discovery, configuration validation, documentation checks, and engine
28/// construction; auxiliary findings emitted by a primary rule are not entries.
29pub struct RuleDescriptor {
30    pub id: &'static str,
31    pub title: &'static str,
32    pub summary: &'static str,
33    pub impact: &'static str,
34    factory: fn() -> Box<dyn Rule>,
35}
36
37impl RuleDescriptor {
38    pub fn build(&self) -> Box<dyn Rule> {
39        (self.factory)()
40    }
41
42    pub fn default_tier(&self) -> ViolationTier {
43        self.build().default_tier()
44    }
45
46    pub fn recipe(&self) -> &'static str {
47        self.build().recipe()
48    }
49}
50
51macro_rules! descriptor {
52    ($id:literal, $title:literal, $summary:literal, $impact:literal, $rule:expr) => {
53        RuleDescriptor {
54            id: $id,
55            title: $title,
56            summary: $summary,
57            impact: $impact,
58            factory: || Box::new($rule),
59        }
60    };
61}
62
63// Marker rules are currently zero-sized; their constructors are kept in this
64// registry so future initialized rules can supply a dedicated factory.
65pub static PRIMARY_RULES: &[RuleDescriptor] = &[
66    descriptor!(
67        "irreversible-migration",
68        "Irreversible migration",
69        "Flags destructive operations that cannot be reversed.",
70        "data loss",
71        ReversibilityRule
72    ),
73    descriptor!(
74        "drop-database",
75        "Drop database",
76        "Flags database deletion.",
77        "data loss",
78        DropDatabaseRule
79    ),
80    descriptor!(
81        "drop-schema-cascade",
82        "Drop schema with cascade",
83        "Flags schema-wide cascading deletion.",
84        "data loss",
85        DropSchemaCascadeRule
86    ),
87    descriptor!(
88        "destructive-general-cascade",
89        "Destructive cascade",
90        "Flags cascading non-table drops.",
91        "data loss",
92        GeneralCascadeRule
93    ),
94    descriptor!(
95        "destructive-cascade",
96        "Drop table with cascade",
97        "Flags table drops that remove dependencies.",
98        "data loss",
99        CascadingDropRule
100    ),
101    descriptor!(
102        "create-table-as-select",
103        "Create table as select",
104        "Flags potentially expensive CTAS operations.",
105        "rewrite",
106        CreateTableAsSelectRule
107    ),
108    descriptor!(
109        "size-aware-add-column",
110        "Add column on a large table",
111        "Flags column additions that can rewrite large tables.",
112        "rewrite",
113        SizeAwareAddColumnRule
114    ),
115    descriptor!(
116        "type-change-rewrite",
117        "Type change rewrite",
118        "Flags column type changes that rewrite data.",
119        "rewrite",
120        TypeChangeRewriteRule
121    ),
122    descriptor!(
123        "blocking-constraint",
124        "Blocking constraint",
125        "Flags constraint changes that lock or scan tables.",
126        "locking",
127        BlockingConstraintRule
128    ),
129    descriptor!(
130        "require-concurrent-index",
131        "Require concurrent index",
132        "Flags index changes that should use CONCURRENTLY.",
133        "locking",
134        ConcurrentIndexRule
135    ),
136    descriptor!(
137        "blocking-mat-view-refresh",
138        "Blocking materialized-view refresh",
139        "Flags refreshes that block readers.",
140        "locking",
141        MaterializedViewRefreshRule
142    ),
143    descriptor!(
144        "blocking-partition-mutation",
145        "Blocking partition mutation",
146        "Flags partition attach and detach locks.",
147        "locking",
148        PartitionLockRule
149    ),
150    descriptor!(
151        "partition-strategy-mismatch",
152        "Partition strategy mismatch",
153        "Flags incompatible partition attachment.",
154        "correctness",
155        PartitionStrategyMismatchRule
156    ),
157    descriptor!(
158        "restrictive-policy",
159        "Restrictive policy",
160        "Flags policies that narrow row visibility.",
161        "access control",
162        RestrictivePolicyRule
163    ),
164    descriptor!(
165        "disable-trigger",
166        "Disable trigger",
167        "Flags disabled triggers.",
168        "correctness",
169        DisableTriggerRule
170    ),
171    descriptor!(
172        "broken-compute",
173        "Broken compute dependency",
174        "Flags function changes that break triggers.",
175        "correctness",
176        BrokenComputeRule
177    ),
178    descriptor!(
179        "function-volatility-change",
180        "Function volatility change",
181        "Flags changed function volatility.",
182        "query planning",
183        FunctionVolatilityRule
184    ),
185    descriptor!(
186        "missing-idempotency",
187        "Missing idempotency",
188        "Flags migrations unsafe to rerun.",
189        "operability",
190        IdempotencyRule
191    ),
192    descriptor!(
193        "concurrent-in-transaction",
194        "Concurrent index in transaction",
195        "Flags CONCURRENTLY inside a transaction.",
196        "correctness",
197        ConcurrentInsideTransactionRule
198    ),
199    descriptor!(
200        "alter-type-add-value-txn",
201        "Enum value in transaction",
202        "Flags ALTER TYPE ADD VALUE in a transaction.",
203        "correctness",
204        AlterTypeAddValueRule
205    ),
206    descriptor!(
207        "vacuum-full",
208        "Vacuum full",
209        "Flags VACUUM FULL in migrations.",
210        "locking",
211        VacuumFullRule
212    ),
213    descriptor!(
214        "opaque-dynamic-sql",
215        "Opaque dynamic SQL",
216        "Flags SQL whose schema effects cannot be modeled.",
217        "confidence",
218        OpaqueDynamicSqlRule
219    ),
220    descriptor!(
221        "volatile-default",
222        "Volatile default",
223        "Flags volatile default expressions.",
224        "correctness",
225        VolatileDefaultRule
226    ),
227    descriptor!(
228        "overbroad-grant",
229        "Overbroad grant",
230        "Flags broad public privileges.",
231        "access control",
232        OverbroadGrantRule
233    ),
234    descriptor!(
235        "schema-drift",
236        "Schema drift",
237        "Flags references missing from the baseline.",
238        "correctness",
239        DriftDetectionRule
240    ),
241    descriptor!(
242        "chain-conflict",
243        "Migration chain conflict",
244        "Flags statements that conflict with prior migration state.",
245        "correctness",
246        ConflictRule
247    ),
248];
249
250pub fn primary_rule_ids() -> impl Iterator<Item = &'static str> {
251    PRIMARY_RULES.iter().map(|rule| rule.id)
252}
253
254pub fn find_primary_rule(id: &str) -> Option<&'static RuleDescriptor> {
255    PRIMARY_RULES.iter().find(|rule| rule.id == id)
256}
257pub fn build_primary_rules() -> Vec<Box<dyn Rule>> {
258    PRIMARY_RULES.iter().map(RuleDescriptor::build).collect()
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use std::collections::HashSet;
265
266    #[test]
267    fn descriptors_have_unique_ids_matching_the_rules_they_construct() {
268        let ids: HashSet<_> = PRIMARY_RULES
269            .iter()
270            .map(|descriptor| descriptor.id)
271            .collect();
272        assert_eq!(ids.len(), PRIMARY_RULES.len());
273        for descriptor in PRIMARY_RULES {
274            let rule = descriptor.build();
275            assert_eq!(rule.id(), descriptor.id);
276            assert_eq!(descriptor.default_tier(), rule.default_tier());
277            assert_eq!(descriptor.recipe(), rule.recipe());
278        }
279    }
280}