Skip to main content

spg_engine/testkit/
env_config.rs

1//! Test-mode GUC snapshot — frozen at engine init.
2//!
3//! Hot paths in the engine read fields directly via `engine.env_cfg().<field>`.
4//! In production `EnvConfig` is `Default::default()` so every field is `false /
5//! None / Auto` — the optimiser can const-fold every gate.
6//!
7//! Adding a new GUC:
8//! 1. Add a field here + default + builder method.
9//! 2. Add the read site (`engine.env_cfg().<field>`) at one acceptor in the engine.
10//! 3. Add a `tests/env_cfg_<name>.rs` unit pin.
11//! 4. Record all three in `xtests/sigil/test-mode-gucs.md`.
12
13/// Frozen snapshot of `SPG_TEST_*` env vars + programmatic overrides.
14///
15/// Every field defaults to "production behaviour"; only flips when explicitly
16/// set via env var (`EnvConfig::from_env`) or builder (`EnvConfig::builder`).
17#[derive(Debug, Clone, PartialEq, Eq)]
18#[allow(clippy::struct_excessive_bools)] // 5 deliberate orthogonal test knobs
19pub struct EnvConfig {
20    /// `SPG_TEST_COMPUTE_QUERY_ID=regress` — strip query-id annotations
21    /// from EXPLAIN output (regression-test mode).
22    pub compute_query_id: ComputeQueryId,
23
24    /// `SPG_TEST_EXPLAIN_NO_COSTS=1` — suppress nondeterministic cost /
25    /// elapsed annotations in EXPLAIN output. Used so EXPLAIN diffs are
26    /// byte-equal across runs and machines.
27    pub explain_no_costs: bool,
28
29    /// `SPG_TEST_STATS_FROZEN=1` — freeze the ANALYZE-derived statistics
30    /// snapshot. ANALYZE becomes a no-op; INSERT/UPDATE auto-stats refresh
31    /// stops firing. Pins cost-model inputs across runs.
32    pub stats_frozen: bool,
33
34    /// `SPG_TEST_PLAN_DETERMINISTIC=1` — disable cost-based plan-cache and
35    /// join-order decisions; fall back to a lexical / signature-hash tie
36    /// break so the chosen plan is invariant w.r.t. stats jitter.
37    pub plan_deterministic: bool,
38
39    /// `SPG_TEST_DISABLE_TOPK=1` — disable the aggregate top-K LIMIT
40    /// fast path (`select_nth_unstable_by`). Forces the full-sort
41    /// fallback so two plans can be diffed for the same ORDER BY +
42    /// LIMIT query.
43    pub disable_topk: bool,
44
45    /// `SPG_TEST_DISABLE_JOINFOLD=1` — disable the v7.37 joinfold rewrite.
46    /// Lets oracle / regression harnesses compare un-folded plans against
47    /// the folded baseline.
48    pub disable_joinfold: bool,
49
50    /// `SPG_TEST_RANDOM_SEED=N` — seed every nondeterministic source
51    /// (hash maps that need a seed, randomised tie-breakers, …) to a
52    /// deterministic value. `None` means "production: derive from clock".
53    pub random_seed: Option<u64>,
54
55    /// `SPG_TEST_FIXED_CLOCK_MICROS=N` — pin the engine clock to a
56    /// fixed instant (microseconds since the Unix epoch). Makes
57    /// `now()` / `CURRENT_DATE` / `CURRENT_TIMESTAMP` deterministic in
58    /// hosts that can't inject `with_clock` programmatically (the
59    /// server permutations of the corpus runner). `None` = wall clock.
60    pub fixed_clock_micros: Option<i64>,
61}
62
63/// Semantics of the `compute_query_id` knob.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
65pub enum ComputeQueryId {
66    /// Production: hash + emit query id in EXPLAIN output.
67    #[default]
68    Auto,
69    /// Regression-test mode: elide query id from EXPLAIN output so diffs
70    /// are byte-equal across runs.
71    Regress,
72}
73
74impl Default for EnvConfig {
75    fn default() -> Self {
76        Self {
77            compute_query_id: ComputeQueryId::Auto,
78            explain_no_costs: false,
79            stats_frozen: false,
80            plan_deterministic: false,
81            disable_topk: false,
82            disable_joinfold: false,
83            random_seed: None,
84            fixed_clock_micros: None,
85        }
86    }
87}
88
89impl EnvConfig {
90    /// Read every `SPG_TEST_*` env var exactly once. Called by hosts
91    /// (spg-server, spg-embedded, tests) at engine init; the engine
92    /// itself is `#![no_std]` and never touches env directly on its hot
93    /// path.
94    ///
95    /// Only available when the `std` feature (default) is enabled.
96    #[cfg(feature = "std")]
97    pub fn from_env() -> Self {
98        extern crate std;
99        use std::env;
100
101        let mut cfg = Self::default();
102        if let Ok(v) = env::var("SPG_TEST_COMPUTE_QUERY_ID") {
103            cfg.compute_query_id = if v == "regress" {
104                ComputeQueryId::Regress
105            } else {
106                ComputeQueryId::Auto
107            };
108        }
109        if env_flag("SPG_TEST_EXPLAIN_NO_COSTS") {
110            cfg.explain_no_costs = true;
111        }
112        if env_flag("SPG_TEST_STATS_FROZEN") {
113            cfg.stats_frozen = true;
114        }
115        if env_flag("SPG_TEST_PLAN_DETERMINISTIC") {
116            cfg.plan_deterministic = true;
117        }
118        if env_flag("SPG_TEST_DISABLE_TOPK") {
119            cfg.disable_topk = true;
120        }
121        if env_flag("SPG_TEST_DISABLE_JOINFOLD") {
122            cfg.disable_joinfold = true;
123        }
124        if let Ok(v) = env::var("SPG_TEST_RANDOM_SEED") {
125            cfg.random_seed = v.parse().ok();
126        }
127        if let Ok(v) = env::var("SPG_TEST_FIXED_CLOCK_MICROS") {
128            cfg.fixed_clock_micros = v.parse().ok();
129        }
130        cfg
131    }
132
133    /// Builder-style override for programmatic GUC injection (used by
134    /// permutation runner / oracle runner / unit tests that build an
135    /// engine without env vars).
136    #[must_use]
137    pub fn builder() -> EnvConfigBuilder {
138        EnvConfigBuilder {
139            cfg: Self::default(),
140        }
141    }
142}
143
144#[cfg(feature = "std")]
145fn env_flag(name: &str) -> bool {
146    extern crate std;
147    std::env::var(name).map(|v| v == "1").unwrap_or(false)
148}
149
150/// Builder for `EnvConfig`. One method per field; symmetric with the env
151/// vars in `from_env`.
152#[derive(Debug, Clone, Default)]
153pub struct EnvConfigBuilder {
154    cfg: EnvConfig,
155}
156
157impl EnvConfigBuilder {
158    pub fn compute_query_id(mut self, v: ComputeQueryId) -> Self {
159        self.cfg.compute_query_id = v;
160        self
161    }
162    pub fn explain_no_costs(mut self, v: bool) -> Self {
163        self.cfg.explain_no_costs = v;
164        self
165    }
166    pub fn stats_frozen(mut self, v: bool) -> Self {
167        self.cfg.stats_frozen = v;
168        self
169    }
170    pub fn plan_deterministic(mut self, v: bool) -> Self {
171        self.cfg.plan_deterministic = v;
172        self
173    }
174    pub fn disable_topk(mut self, v: bool) -> Self {
175        self.cfg.disable_topk = v;
176        self
177    }
178    pub fn disable_joinfold(mut self, v: bool) -> Self {
179        self.cfg.disable_joinfold = v;
180        self
181    }
182    pub fn fixed_clock_micros(mut self, v: i64) -> Self {
183        self.cfg.fixed_clock_micros = Some(v);
184        self
185    }
186    pub fn random_seed(mut self, v: u64) -> Self {
187        self.cfg.random_seed = Some(v);
188        self
189    }
190    pub fn build(self) -> EnvConfig {
191        self.cfg
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    #[test]
200    fn default_is_production() {
201        let cfg = EnvConfig::default();
202        assert_eq!(cfg.compute_query_id, ComputeQueryId::Auto);
203        assert!(!cfg.explain_no_costs);
204        assert!(!cfg.stats_frozen);
205        assert!(!cfg.plan_deterministic);
206        assert!(!cfg.disable_topk);
207        assert!(!cfg.disable_joinfold);
208        assert_eq!(cfg.random_seed, None);
209    }
210
211    #[test]
212    fn builder_roundtrip() {
213        let cfg = EnvConfig::builder()
214            .explain_no_costs(true)
215            .disable_topk(true)
216            .random_seed(42)
217            .build();
218        assert!(cfg.explain_no_costs);
219        assert!(cfg.disable_topk);
220        assert_eq!(cfg.random_seed, Some(42));
221        // Untouched fields stay at default.
222        assert!(!cfg.disable_joinfold);
223        assert_eq!(cfg.compute_query_id, ComputeQueryId::Auto);
224    }
225}