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