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
56/// Semantics of the `compute_query_id` knob.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum ComputeQueryId {
59 /// Production: hash + emit query id in EXPLAIN output.
60 #[default]
61 Auto,
62 /// Regression-test mode: elide query id from EXPLAIN output so diffs
63 /// are byte-equal across runs.
64 Regress,
65}
66
67impl Default for EnvConfig {
68 fn default() -> Self {
69 Self {
70 compute_query_id: ComputeQueryId::Auto,
71 explain_no_costs: false,
72 stats_frozen: false,
73 plan_deterministic: false,
74 disable_topk: false,
75 disable_joinfold: false,
76 random_seed: None,
77 }
78 }
79}
80
81impl EnvConfig {
82 /// Read every `SPG_TEST_*` env var exactly once. Called by hosts
83 /// (spg-server, spg-embedded, tests) at engine init; the engine
84 /// itself is `#![no_std]` and never touches env directly on its hot
85 /// path.
86 ///
87 /// Only available when the `std` feature (default) is enabled.
88 #[cfg(feature = "std")]
89 pub fn from_env() -> Self {
90 extern crate std;
91 use std::env;
92
93 let mut cfg = Self::default();
94 if let Ok(v) = env::var("SPG_TEST_COMPUTE_QUERY_ID") {
95 cfg.compute_query_id = if v == "regress" {
96 ComputeQueryId::Regress
97 } else {
98 ComputeQueryId::Auto
99 };
100 }
101 if env_flag("SPG_TEST_EXPLAIN_NO_COSTS") {
102 cfg.explain_no_costs = true;
103 }
104 if env_flag("SPG_TEST_STATS_FROZEN") {
105 cfg.stats_frozen = true;
106 }
107 if env_flag("SPG_TEST_PLAN_DETERMINISTIC") {
108 cfg.plan_deterministic = true;
109 }
110 if env_flag("SPG_TEST_DISABLE_TOPK") {
111 cfg.disable_topk = true;
112 }
113 if env_flag("SPG_TEST_DISABLE_JOINFOLD") {
114 cfg.disable_joinfold = true;
115 }
116 if let Ok(v) = env::var("SPG_TEST_RANDOM_SEED") {
117 cfg.random_seed = v.parse().ok();
118 }
119 cfg
120 }
121
122 /// Builder-style override for programmatic GUC injection (used by
123 /// permutation runner / oracle runner / unit tests that build an
124 /// engine without env vars).
125 #[must_use]
126 pub fn builder() -> EnvConfigBuilder {
127 EnvConfigBuilder {
128 cfg: Self::default(),
129 }
130 }
131}
132
133#[cfg(feature = "std")]
134fn env_flag(name: &str) -> bool {
135 extern crate std;
136 std::env::var(name).map(|v| v == "1").unwrap_or(false)
137}
138
139/// Builder for `EnvConfig`. One method per field; symmetric with the env
140/// vars in `from_env`.
141#[derive(Debug, Clone, Default)]
142pub struct EnvConfigBuilder {
143 cfg: EnvConfig,
144}
145
146impl EnvConfigBuilder {
147 pub fn compute_query_id(mut self, v: ComputeQueryId) -> Self {
148 self.cfg.compute_query_id = v;
149 self
150 }
151 pub fn explain_no_costs(mut self, v: bool) -> Self {
152 self.cfg.explain_no_costs = v;
153 self
154 }
155 pub fn stats_frozen(mut self, v: bool) -> Self {
156 self.cfg.stats_frozen = v;
157 self
158 }
159 pub fn plan_deterministic(mut self, v: bool) -> Self {
160 self.cfg.plan_deterministic = v;
161 self
162 }
163 pub fn disable_topk(mut self, v: bool) -> Self {
164 self.cfg.disable_topk = v;
165 self
166 }
167 pub fn disable_joinfold(mut self, v: bool) -> Self {
168 self.cfg.disable_joinfold = v;
169 self
170 }
171 pub fn random_seed(mut self, v: u64) -> Self {
172 self.cfg.random_seed = Some(v);
173 self
174 }
175 pub fn build(self) -> EnvConfig {
176 self.cfg
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn default_is_production() {
186 let cfg = EnvConfig::default();
187 assert_eq!(cfg.compute_query_id, ComputeQueryId::Auto);
188 assert!(!cfg.explain_no_costs);
189 assert!(!cfg.stats_frozen);
190 assert!(!cfg.plan_deterministic);
191 assert!(!cfg.disable_topk);
192 assert!(!cfg.disable_joinfold);
193 assert_eq!(cfg.random_seed, None);
194 }
195
196 #[test]
197 fn builder_roundtrip() {
198 let cfg = EnvConfig::builder()
199 .explain_no_costs(true)
200 .disable_topk(true)
201 .random_seed(42)
202 .build();
203 assert!(cfg.explain_no_costs);
204 assert!(cfg.disable_topk);
205 assert_eq!(cfg.random_seed, Some(42));
206 // Untouched fields stay at default.
207 assert!(!cfg.disable_joinfold);
208 assert_eq!(cfg.compute_query_id, ComputeQueryId::Auto);
209 }
210}