Skip to main content

pgevolve_core/plan/
policy.rs

1//! [`PlannerPolicy`] — feature switches that gate the rewrite pass.
2//!
3//! Spec §6.5: each rewrite is gated on a policy switch so per-environment
4//! overrides plug in cleanly later. v0.1 ships two strategies:
5//!
6//! - [`Strategy::Online`] — apply every enabled rewrite (default).
7//! - [`Strategy::Atomic`] — short-circuit every online switch to `false`,
8//!   producing one in-transaction step per change with no online rewrites.
9//!
10//! Atomic mode is "single transaction, no rewrites." Use [`PlannerPolicy::is_online`]
11//! to read the effective switch values; do not read [`OnlineRewrites`] directly,
12//! because `Atomic` makes the individual switches inert.
13
14/// Top-level rewrite strategy.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub enum Strategy {
17    /// All operations run inside one transaction; no online rewrites apply.
18    Atomic,
19    /// Apply each enabled rewrite from [`OnlineRewrites`].
20    Online,
21}
22
23/// Per-rewrite enable switches. Only consulted when [`Strategy::Online`].
24//
25// Each bool is an independent on/off switch addressing a distinct rewrite —
26// they are not a hidden state machine, so `struct_excessive_bools` doesn't apply.
27#[allow(clippy::struct_excessive_bools)]
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct OnlineRewrites {
30    /// `CreateIndex` (non-unique, on existing table) → `CREATE INDEX CONCURRENTLY`.
31    pub create_index_concurrent: bool,
32    /// `AddConstraint(ForeignKey)` on existing table → `NOT VALID` + `VALIDATE`.
33    pub fk_not_valid_then_validate: bool,
34    /// `AddConstraint(Check)` on existing table → `NOT VALID` + `VALIDATE`.
35    pub check_not_valid_then_validate: bool,
36    /// `SetColumnNullable { nullable: false }` on a populated column →
37    /// `ADD CHECK NOT VALID` + `VALIDATE` + `SET NOT NULL` + `DROP CONSTRAINT`.
38    pub not_null_via_check_pattern: bool,
39    /// Upgrade `REFRESH MATERIALIZED VIEW` to `REFRESH MATERIALIZED VIEW
40    /// CONCURRENTLY` when the MV has at least one unique index. Emits a lint
41    /// warning when the MV has no unique index. Default `true`.
42    pub refresh_mv_concurrently: bool,
43    /// Walk transitively-affected views and emit explicit DROP + CREATE steps
44    /// for them instead of relying on `CASCADE`. When `false`, the planner
45    /// errors (naming every affected view) if any change would cascade
46    /// dependent recreations. Default `true`.
47    pub view_drop_create_dependents: bool,
48}
49
50impl OnlineRewrites {
51    /// All rewrites enabled — default.
52    pub const fn all_enabled() -> Self {
53        Self {
54            create_index_concurrent: true,
55            fk_not_valid_then_validate: true,
56            check_not_valid_then_validate: true,
57            not_null_via_check_pattern: true,
58            refresh_mv_concurrently: true,
59            view_drop_create_dependents: true,
60        }
61    }
62
63    /// All rewrites disabled.
64    pub const fn all_disabled() -> Self {
65        Self {
66            create_index_concurrent: false,
67            fk_not_valid_then_validate: false,
68            check_not_valid_then_validate: false,
69            not_null_via_check_pattern: false,
70            refresh_mv_concurrently: false,
71            view_drop_create_dependents: false,
72        }
73    }
74}
75
76/// Top-level planner policy: strategy + per-rewrite switches + ruleset version.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct PlannerPolicy {
79    /// Strategy. [`Strategy::Atomic`] forces every online switch to `false`.
80    pub strategy: Strategy,
81    /// Per-rewrite switches; honored iff `strategy == Online`.
82    pub online: OnlineRewrites,
83    /// Ruleset version included in the plan id hash (spec §6.6).
84    pub planner_ruleset_version: u32,
85}
86
87impl PlannerPolicy {
88    /// Is the `create_index_concurrent` rewrite effectively enabled?
89    pub const fn create_index_concurrent(&self) -> bool {
90        matches!(self.strategy, Strategy::Online) && self.online.create_index_concurrent
91    }
92
93    /// Is the FK `NOT VALID` + `VALIDATE` rewrite effectively enabled?
94    pub const fn fk_not_valid_then_validate(&self) -> bool {
95        matches!(self.strategy, Strategy::Online) && self.online.fk_not_valid_then_validate
96    }
97
98    /// Is the CHECK `NOT VALID` + `VALIDATE` rewrite effectively enabled?
99    pub const fn check_not_valid_then_validate(&self) -> bool {
100        matches!(self.strategy, Strategy::Online) && self.online.check_not_valid_then_validate
101    }
102
103    /// Is the `SET NOT NULL` via CHECK pattern effectively enabled?
104    pub const fn not_null_via_check_pattern(&self) -> bool {
105        matches!(self.strategy, Strategy::Online) && self.online.not_null_via_check_pattern
106    }
107
108    /// Is the `REFRESH MATERIALIZED VIEW CONCURRENTLY` upgrade effectively enabled?
109    pub const fn refresh_mv_concurrently(&self) -> bool {
110        matches!(self.strategy, Strategy::Online) && self.online.refresh_mv_concurrently
111    }
112
113    /// Is the dependent-view DROP + CREATE walk effectively enabled?
114    ///
115    /// When `false`, the planner errors when any change would force dependent
116    /// view recreations (instead of walking and emitting them silently).
117    pub const fn view_drop_create_dependents(&self) -> bool {
118        // This switch is consulted even in atomic mode because it controls
119        // error-vs-walk behavior (not a pure online-only optimization).
120        self.online.view_drop_create_dependents
121    }
122
123    /// True iff the strategy is `Online` (i.e., online rewrites may run).
124    pub const fn is_online(&self) -> bool {
125        matches!(self.strategy, Strategy::Online)
126    }
127}
128
129impl Default for PlannerPolicy {
130    fn default() -> Self {
131        Self {
132            strategy: Strategy::Online,
133            online: OnlineRewrites::all_enabled(),
134            planner_ruleset_version: 1,
135        }
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn default_is_online_with_every_rewrite_enabled() {
145        let p = PlannerPolicy::default();
146        assert!(p.is_online());
147        assert!(p.create_index_concurrent());
148        assert!(p.fk_not_valid_then_validate());
149        assert!(p.check_not_valid_then_validate());
150        assert!(p.not_null_via_check_pattern());
151        assert!(p.refresh_mv_concurrently());
152        assert!(p.view_drop_create_dependents());
153        assert_eq!(p.planner_ruleset_version, 1);
154    }
155
156    #[test]
157    fn atomic_strategy_disables_every_rewrite_regardless_of_switches() {
158        let p = PlannerPolicy {
159            strategy: Strategy::Atomic,
160            online: OnlineRewrites::all_enabled(),
161            planner_ruleset_version: 1,
162        };
163        assert!(!p.is_online());
164        assert!(!p.create_index_concurrent());
165        assert!(!p.fk_not_valid_then_validate());
166        assert!(!p.check_not_valid_then_validate());
167        assert!(!p.not_null_via_check_pattern());
168        assert!(!p.refresh_mv_concurrently());
169        // view_drop_create_dependents is consulted regardless of strategy.
170        assert!(p.view_drop_create_dependents());
171    }
172
173    #[test]
174    fn online_strategy_respects_individual_switches() {
175        let p = PlannerPolicy {
176            strategy: Strategy::Online,
177            online: OnlineRewrites {
178                create_index_concurrent: false,
179                fk_not_valid_then_validate: true,
180                check_not_valid_then_validate: false,
181                not_null_via_check_pattern: true,
182                refresh_mv_concurrently: false,
183                view_drop_create_dependents: true,
184            },
185            planner_ruleset_version: 1,
186        };
187        assert!(!p.create_index_concurrent());
188        assert!(p.fk_not_valid_then_validate());
189        assert!(!p.check_not_valid_then_validate());
190        assert!(p.not_null_via_check_pattern());
191        assert!(!p.refresh_mv_concurrently());
192        assert!(p.view_drop_create_dependents());
193    }
194
195    #[test]
196    fn online_with_all_disabled_disables_every_rewrite() {
197        let p = PlannerPolicy {
198            strategy: Strategy::Online,
199            online: OnlineRewrites::all_disabled(),
200            planner_ruleset_version: 1,
201        };
202        assert!(p.is_online());
203        assert!(!p.create_index_concurrent());
204        assert!(!p.fk_not_valid_then_validate());
205        assert!(!p.check_not_valid_then_validate());
206        assert!(!p.not_null_via_check_pattern());
207        assert!(!p.refresh_mv_concurrently());
208        assert!(!p.view_drop_create_dependents());
209    }
210}