Skip to main content

pgevolve_core/ir/
reloptions.rs

1//! Storage parameters / reloptions for Table, Index, `MaterializedView`.
2//!
3//! Typed fields for well-known keys + `extra: BTreeMap<String, String>` for
4//! extension-registered or otherwise-unknown options. The `autovacuum_*` key
5//! family is routed through `extra` as raw strings rather than typed fields:
6//! the key set is large and rarely diffed, so the generic map keeps the IR
7//! surface small while still round-tripping every key faithfully.
8
9use std::collections::BTreeMap;
10use std::str::FromStr;
11
12use serde::{Deserialize, Serialize};
13
14/// Storage options for tables. MV reuses via type alias since PG documents
15/// identical reloptions for both.
16#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
17pub struct TableStorageOptions {
18    /// `fillfactor` — target heap page density (10..=100).
19    pub fillfactor: Option<u32>,
20    /// `parallel_workers` — number of parallel workers (0..=1024).
21    pub parallel_workers: Option<u32>,
22    /// `toast_tuple_target` — TOAST compression threshold in bytes (128..=8160).
23    pub toast_tuple_target: Option<u32>,
24    /// `user_catalog_table` — treat as a catalog table for logical replication.
25    pub user_catalog_table: Option<bool>,
26    /// `vacuum_truncate` — allow VACUUM to truncate trailing empty pages (PG 12+).
27    pub vacuum_truncate: Option<bool>,
28    /// Unknown / extension-registered options, plus the `autovacuum_*` key
29    /// family. Stored as raw `key = value` strings, always sorted by key
30    /// (`BTreeMap`).
31    pub extra: BTreeMap<String, String>,
32}
33
34impl TableStorageOptions {
35    /// `true` iff every typed field is `None` and `extra` is empty.
36    #[must_use]
37    pub fn is_empty(&self) -> bool {
38        self.fillfactor.is_none()
39            && self.parallel_workers.is_none()
40            && self.toast_tuple_target.is_none()
41            && self.user_catalog_table.is_none()
42            && self.vacuum_truncate.is_none()
43            && self.extra.is_empty()
44    }
45}
46
47/// MVs share table reloption semantics in PG.
48pub type MaterializedViewStorageOptions = TableStorageOptions;
49
50/// Storage options for indexes. Valid keys depend on access method;
51/// parse-time validation enforces per-AM rules.
52#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
53pub struct IndexStorageOptions {
54    /// `fillfactor` — target index page density; valid range depends on access method.
55    pub fillfactor: Option<u32>,
56    /// `fastupdate` — GIN: defer index updates via a pending list.
57    pub fastupdate: Option<bool>,
58    /// `gin_pending_list_limit` — GIN: max size of pending list before cleanup (bytes).
59    pub gin_pending_list_limit: Option<u64>,
60    /// `buffering` — `GiST` / `SP-GiST`: controls buffered build strategy.
61    pub buffering: Option<BufferingMode>,
62    /// `deduplicate_items` — B-tree (PG 13+): enable deduplication of posting lists.
63    pub deduplicate_items: Option<bool>,
64    /// `pages_per_range` — BRIN: number of heap pages per BRIN range.
65    pub pages_per_range: Option<u32>,
66    /// `autosummarize` — BRIN: auto-summarize when `pages_per_range` fills.
67    pub autosummarize: Option<bool>,
68    /// Unknown / extension-registered options. Always sorted by key (`BTreeMap`).
69    pub extra: BTreeMap<String, String>,
70}
71
72impl IndexStorageOptions {
73    /// `true` iff every typed field is `None` and `extra` is empty.
74    #[must_use]
75    pub fn is_empty(&self) -> bool {
76        self.fillfactor.is_none()
77            && self.fastupdate.is_none()
78            && self.gin_pending_list_limit.is_none()
79            && self.buffering.is_none()
80            && self.deduplicate_items.is_none()
81            && self.pages_per_range.is_none()
82            && self.autosummarize.is_none()
83            && self.extra.is_empty()
84    }
85}
86
87/// `buffering` setting for GiST/SP-GiST index builds.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
89#[serde(rename_all = "snake_case")]
90pub enum BufferingMode {
91    /// Enable buffered index build.
92    On,
93    /// Disable buffered index build.
94    Off,
95    /// Let PG decide based on available memory (default).
96    Auto,
97}
98
99impl BufferingMode {
100    /// Returns the lowercase SQL keyword for this mode.
101    #[must_use]
102    pub const fn sql_keyword(self) -> &'static str {
103        match self {
104            Self::On => "on",
105            Self::Off => "off",
106            Self::Auto => "auto",
107        }
108    }
109}
110
111impl FromStr for BufferingMode {
112    type Err = ();
113
114    /// Parse from `pg_class.reloptions` text or source SQL value.
115    fn from_str(s: &str) -> Result<Self, Self::Err> {
116        match s.to_ascii_lowercase().as_str() {
117            "on" => Ok(Self::On),
118            "off" => Ok(Self::Off),
119            "auto" => Ok(Self::Auto),
120            _ => Err(()),
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn default_storage_is_empty() {
131        assert!(TableStorageOptions::default().is_empty());
132        assert!(IndexStorageOptions::default().is_empty());
133    }
134
135    #[test]
136    fn autovacuum_extra_key_makes_non_empty() {
137        let mut s = TableStorageOptions::default();
138        s.extra.insert("autovacuum_enabled".into(), "false".into());
139        assert!(!s.is_empty());
140    }
141
142    #[test]
143    fn non_empty_storage_detected() {
144        let s = TableStorageOptions {
145            fillfactor: Some(80),
146            ..Default::default()
147        };
148        assert!(!s.is_empty());
149    }
150
151    #[test]
152    fn buffering_mode_roundtrips() {
153        for m in [BufferingMode::On, BufferingMode::Off, BufferingMode::Auto] {
154            assert_eq!(m.sql_keyword().parse::<BufferingMode>(), Ok(m));
155        }
156        assert!("bogus".parse::<BufferingMode>().is_err());
157    }
158
159    #[test]
160    fn extra_is_sorted_via_btreemap() {
161        let mut s = TableStorageOptions::default();
162        s.extra.insert("zebra".into(), "1".into());
163        s.extra.insert("alpha".into(), "2".into());
164        let keys: Vec<_> = s.extra.keys().cloned().collect();
165        assert_eq!(keys, vec!["alpha", "zebra"]);
166    }
167}