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::{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    // Extra bag (includes autovacuum_* keys): only keys present in source and
36    // (missing-or-different) in catalog.
37    for (k, src_v) in &source.extra {
38        if target.extra.get(k) != Some(src_v) {
39            out.extra.insert(k.clone(), src_v.clone());
40        }
41    }
42
43    out
44}
45
46/// Build the sparse delta for index storage options.
47///
48/// Same lenient-policy semantics as [`table_delta`].
49#[must_use]
50pub fn index_delta(
51    target: &IndexStorageOptions,
52    source: &IndexStorageOptions,
53) -> IndexStorageOptions {
54    let mut out = IndexStorageOptions::default();
55
56    macro_rules! diff_field {
57        ($field:ident) => {
58            if let Some(src) = &source.$field {
59                if target.$field.as_ref() != Some(src) {
60                    out.$field = Some(src.clone());
61                }
62            }
63        };
64    }
65
66    diff_field!(fillfactor);
67    diff_field!(fastupdate);
68    diff_field!(gin_pending_list_limit);
69    diff_field!(buffering);
70    diff_field!(deduplicate_items);
71    diff_field!(pages_per_range);
72    diff_field!(autosummarize);
73
74    for (k, src_v) in &source.extra {
75        if target.extra.get(k) != Some(src_v) {
76            out.extra.insert(k.clone(), src_v.clone());
77        }
78    }
79
80    out
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn empty_source_yields_empty_delta() {
89        let t = TableStorageOptions {
90            fillfactor: Some(80),
91            ..Default::default()
92        };
93        let s = TableStorageOptions::default();
94        let delta = table_delta(&t, &s);
95        assert!(delta.is_empty(), "lenient: source None means skip");
96    }
97
98    #[test]
99    fn source_only_fillfactor_emits_one_field() {
100        let t = TableStorageOptions::default();
101        let s = TableStorageOptions {
102            fillfactor: Some(80),
103            ..Default::default()
104        };
105        let delta = table_delta(&t, &s);
106        assert_eq!(delta.fillfactor, Some(80));
107        assert!(delta.extra.is_empty());
108    }
109
110    #[test]
111    fn matching_source_and_target_yields_empty_delta() {
112        let t = TableStorageOptions {
113            fillfactor: Some(80),
114            ..Default::default()
115        };
116        let s = TableStorageOptions {
117            fillfactor: Some(80),
118            ..Default::default()
119        };
120        let delta = table_delta(&t, &s);
121        assert!(delta.is_empty());
122    }
123
124    #[test]
125    fn source_extra_key_not_in_target_emits() {
126        let t = TableStorageOptions::default();
127        let mut s = TableStorageOptions::default();
128        s.extra.insert("pg_partman.foo".into(), "value".into());
129        let delta = table_delta(&t, &s);
130        assert_eq!(
131            delta.extra.get("pg_partman.foo").map(String::as_str),
132            Some("value")
133        );
134    }
135
136    #[test]
137    fn target_extra_key_not_in_source_does_not_emit() {
138        let mut t = TableStorageOptions::default();
139        t.extra.insert("pg_partman.foo".into(), "value".into());
140        let s = TableStorageOptions::default();
141        let delta = table_delta(&t, &s);
142        assert!(
143            delta.is_empty(),
144            "lenient: unmanaged extra-bag keys ignored"
145        );
146    }
147
148    #[test]
149    fn source_autovacuum_change_emits_via_extra() {
150        // autovacuum_* keys now diff through the generic `extra` map.
151        let mut t = TableStorageOptions::default();
152        t.extra.insert("autovacuum_enabled".into(), "true".into());
153        let mut s = TableStorageOptions::default();
154        s.extra.insert("autovacuum_enabled".into(), "false".into());
155        let delta = table_delta(&t, &s);
156        assert_eq!(
157            delta.extra.get("autovacuum_enabled").map(String::as_str),
158            Some("false")
159        );
160    }
161
162    #[test]
163    fn index_delta_fillfactor_change() {
164        let t = IndexStorageOptions {
165            fillfactor: Some(70),
166            ..Default::default()
167        };
168        let s = IndexStorageOptions {
169            fillfactor: Some(80),
170            ..Default::default()
171        };
172        let delta = index_delta(&t, &s);
173        assert_eq!(delta.fillfactor, Some(80));
174    }
175}