Skip to main content

pgevolve_core/diff/
reloptions.rs

1//! Sparse-delta diffing for storage reloptions. Lenient policy:
2//! source `None` always means "skip"; catalog values not in source surface
3//! as `unmanaged-reloption` lint, never RESET.
4
5use crate::ir::reloptions::{AutovacuumOptions, IndexStorageOptions, TableStorageOptions};
6
7/// Build the sparse delta for table (or materialized-view) storage options.
8///
9/// Only fields where source is `Some(_)` **and** target disagrees flow into
10/// the returned value. Fields absent from source (`None`) are left as `None`
11/// in the delta — the lenient policy treats them as unmanaged.
12#[must_use]
13pub fn table_delta(
14    target: &TableStorageOptions,
15    source: &TableStorageOptions,
16) -> TableStorageOptions {
17    let mut out = TableStorageOptions::default();
18
19    macro_rules! diff_field {
20        ($field:ident) => {
21            if let Some(src) = &source.$field {
22                if target.$field.as_ref() != Some(src) {
23                    out.$field = Some(src.clone());
24                }
25            }
26        };
27    }
28
29    diff_field!(fillfactor);
30    diff_field!(parallel_workers);
31    diff_field!(toast_tuple_target);
32    diff_field!(user_catalog_table);
33    diff_field!(vacuum_truncate);
34
35    out.autovacuum = autovacuum_delta(&target.autovacuum, &source.autovacuum);
36
37    // Extra bag: only keys present in source and (missing-or-different) in catalog.
38    for (k, src_v) in &source.extra {
39        if target.extra.get(k) != Some(src_v) {
40            out.extra.insert(k.clone(), src_v.clone());
41        }
42    }
43
44    out
45}
46
47/// Build the sparse delta for index storage options.
48///
49/// Same lenient-policy semantics as [`table_delta`].
50#[must_use]
51pub fn index_delta(
52    target: &IndexStorageOptions,
53    source: &IndexStorageOptions,
54) -> IndexStorageOptions {
55    let mut out = IndexStorageOptions::default();
56
57    macro_rules! diff_field {
58        ($field:ident) => {
59            if let Some(src) = &source.$field {
60                if target.$field.as_ref() != Some(src) {
61                    out.$field = Some(src.clone());
62                }
63            }
64        };
65    }
66
67    diff_field!(fillfactor);
68    diff_field!(fastupdate);
69    diff_field!(gin_pending_list_limit);
70    diff_field!(buffering);
71    diff_field!(deduplicate_items);
72    diff_field!(pages_per_range);
73    diff_field!(autosummarize);
74
75    for (k, src_v) in &source.extra {
76        if target.extra.get(k) != Some(src_v) {
77            out.extra.insert(k.clone(), src_v.clone());
78        }
79    }
80
81    out
82}
83
84fn autovacuum_delta(target: &AutovacuumOptions, source: &AutovacuumOptions) -> AutovacuumOptions {
85    let mut out = AutovacuumOptions::default();
86
87    macro_rules! diff_field {
88        ($field:ident) => {
89            if let Some(src) = &source.$field {
90                if target.$field.as_ref() != Some(src) {
91                    out.$field = Some(src.clone());
92                }
93            }
94        };
95    }
96
97    diff_field!(enabled);
98    diff_field!(vacuum_threshold);
99    diff_field!(vacuum_scale_factor);
100    diff_field!(vacuum_cost_delay);
101    diff_field!(vacuum_cost_limit);
102    diff_field!(analyze_threshold);
103    diff_field!(analyze_scale_factor);
104    diff_field!(freeze_max_age);
105    diff_field!(freeze_min_age);
106    diff_field!(freeze_table_age);
107    diff_field!(multixact_freeze_max_age);
108    diff_field!(multixact_freeze_min_age);
109    diff_field!(multixact_freeze_table_age);
110    diff_field!(vacuum_insert_threshold);
111    diff_field!(vacuum_insert_scale_factor);
112    diff_field!(log_min_duration);
113
114    out
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn empty_source_yields_empty_delta() {
123        let t = TableStorageOptions {
124            fillfactor: Some(80),
125            ..Default::default()
126        };
127        let s = TableStorageOptions::default();
128        let delta = table_delta(&t, &s);
129        assert!(delta.is_empty(), "lenient: source None means skip");
130    }
131
132    #[test]
133    fn source_only_fillfactor_emits_one_field() {
134        let t = TableStorageOptions::default();
135        let s = TableStorageOptions {
136            fillfactor: Some(80),
137            ..Default::default()
138        };
139        let delta = table_delta(&t, &s);
140        assert_eq!(delta.fillfactor, Some(80));
141        assert!(delta.autovacuum.is_empty());
142    }
143
144    #[test]
145    fn matching_source_and_target_yields_empty_delta() {
146        let t = TableStorageOptions {
147            fillfactor: Some(80),
148            ..Default::default()
149        };
150        let s = TableStorageOptions {
151            fillfactor: Some(80),
152            ..Default::default()
153        };
154        let delta = table_delta(&t, &s);
155        assert!(delta.is_empty());
156    }
157
158    #[test]
159    fn source_extra_key_not_in_target_emits() {
160        let t = TableStorageOptions::default();
161        let mut s = TableStorageOptions::default();
162        s.extra.insert("pg_partman.foo".into(), "value".into());
163        let delta = table_delta(&t, &s);
164        assert_eq!(
165            delta.extra.get("pg_partman.foo").map(String::as_str),
166            Some("value")
167        );
168    }
169
170    #[test]
171    fn target_extra_key_not_in_source_does_not_emit() {
172        let mut t = TableStorageOptions::default();
173        t.extra.insert("pg_partman.foo".into(), "value".into());
174        let s = TableStorageOptions::default();
175        let delta = table_delta(&t, &s);
176        assert!(
177            delta.is_empty(),
178            "lenient: unmanaged extra-bag keys ignored"
179        );
180    }
181
182    #[test]
183    fn index_delta_fillfactor_change() {
184        let t = IndexStorageOptions {
185            fillfactor: Some(70),
186            ..Default::default()
187        };
188        let s = IndexStorageOptions {
189            fillfactor: Some(80),
190            ..Default::default()
191        };
192        let delta = index_delta(&t, &s);
193        assert_eq!(delta.fillfactor, Some(80));
194    }
195}