Skip to main content

grafeo_engine/
config.rs

1//! Database configuration.
2
3use std::path::PathBuf;
4
5/// Database configuration.
6#[derive(Debug, Clone)]
7#[allow(clippy::struct_excessive_bools)] // Config structs naturally have many boolean flags
8pub struct Config {
9    /// Path to the database directory (None for in-memory only).
10    pub path: Option<PathBuf>,
11
12    /// Memory limit in bytes (None for unlimited).
13    pub memory_limit: Option<usize>,
14
15    /// Path for spilling data to disk under memory pressure.
16    pub spill_path: Option<PathBuf>,
17
18    /// Number of worker threads for query execution.
19    pub threads: usize,
20
21    /// Whether to enable WAL for durability.
22    pub wal_enabled: bool,
23
24    /// WAL flush interval in milliseconds.
25    pub wal_flush_interval_ms: u64,
26
27    /// Whether to maintain backward edges.
28    pub backward_edges: bool,
29
30    /// Whether to enable query logging.
31    pub query_logging: bool,
32
33    /// Adaptive execution configuration.
34    pub adaptive: AdaptiveConfig,
35
36    /// Whether to use factorized execution for multi-hop queries.
37    ///
38    /// When enabled, consecutive MATCH expansions are executed using factorized
39    /// representation which avoids Cartesian product materialization. This provides
40    /// 5-100x speedup for multi-hop queries with high fan-out.
41    ///
42    /// Enabled by default.
43    pub factorized_execution: bool,
44}
45
46/// Configuration for adaptive query execution.
47///
48/// Adaptive execution monitors actual row counts during query processing and
49/// can trigger re-optimization when estimates are significantly wrong.
50#[derive(Debug, Clone)]
51pub struct AdaptiveConfig {
52    /// Whether adaptive execution is enabled.
53    pub enabled: bool,
54
55    /// Deviation threshold that triggers re-optimization.
56    ///
57    /// A value of 3.0 means re-optimization is triggered when actual cardinality
58    /// is more than 3x or less than 1/3x the estimated value.
59    pub threshold: f64,
60
61    /// Minimum number of rows before considering re-optimization.
62    ///
63    /// Helps avoid thrashing on small result sets.
64    pub min_rows: u64,
65
66    /// Maximum number of re-optimizations allowed per query.
67    pub max_reoptimizations: usize,
68}
69
70impl Default for AdaptiveConfig {
71    fn default() -> Self {
72        Self {
73            enabled: true,
74            threshold: 3.0,
75            min_rows: 1000,
76            max_reoptimizations: 3,
77        }
78    }
79}
80
81impl AdaptiveConfig {
82    /// Creates a disabled adaptive config.
83    #[must_use]
84    pub fn disabled() -> Self {
85        Self {
86            enabled: false,
87            ..Default::default()
88        }
89    }
90
91    /// Sets the deviation threshold.
92    #[must_use]
93    pub fn with_threshold(mut self, threshold: f64) -> Self {
94        self.threshold = threshold;
95        self
96    }
97
98    /// Sets the minimum rows before re-optimization.
99    #[must_use]
100    pub fn with_min_rows(mut self, min_rows: u64) -> Self {
101        self.min_rows = min_rows;
102        self
103    }
104
105    /// Sets the maximum number of re-optimizations.
106    #[must_use]
107    pub fn with_max_reoptimizations(mut self, max: usize) -> Self {
108        self.max_reoptimizations = max;
109        self
110    }
111}
112
113impl Default for Config {
114    fn default() -> Self {
115        Self {
116            path: None,
117            memory_limit: None,
118            spill_path: None,
119            threads: num_cpus::get(),
120            wal_enabled: true,
121            wal_flush_interval_ms: 100,
122            backward_edges: true,
123            query_logging: false,
124            adaptive: AdaptiveConfig::default(),
125            factorized_execution: true,
126        }
127    }
128}
129
130impl Config {
131    /// Creates a new configuration for an in-memory database.
132    #[must_use]
133    pub fn in_memory() -> Self {
134        Self {
135            path: None,
136            wal_enabled: false,
137            ..Default::default()
138        }
139    }
140
141    /// Creates a new configuration for a persistent database.
142    #[must_use]
143    pub fn persistent(path: impl Into<PathBuf>) -> Self {
144        Self {
145            path: Some(path.into()),
146            wal_enabled: true,
147            ..Default::default()
148        }
149    }
150
151    /// Sets the memory limit.
152    #[must_use]
153    pub fn with_memory_limit(mut self, limit: usize) -> Self {
154        self.memory_limit = Some(limit);
155        self
156    }
157
158    /// Sets the number of worker threads.
159    #[must_use]
160    pub fn with_threads(mut self, threads: usize) -> Self {
161        self.threads = threads;
162        self
163    }
164
165    /// Disables backward edges.
166    #[must_use]
167    pub fn without_backward_edges(mut self) -> Self {
168        self.backward_edges = false;
169        self
170    }
171
172    /// Enables query logging.
173    #[must_use]
174    pub fn with_query_logging(mut self) -> Self {
175        self.query_logging = true;
176        self
177    }
178
179    /// Sets the memory budget as a fraction of system RAM.
180    #[must_use]
181    pub fn with_memory_fraction(mut self, fraction: f64) -> Self {
182        use grafeo_common::memory::buffer::BufferManagerConfig;
183        let system_memory = BufferManagerConfig::detect_system_memory();
184        self.memory_limit = Some((system_memory as f64 * fraction) as usize);
185        self
186    }
187
188    /// Sets the spill directory for out-of-core processing.
189    #[must_use]
190    pub fn with_spill_path(mut self, path: impl Into<PathBuf>) -> Self {
191        self.spill_path = Some(path.into());
192        self
193    }
194
195    /// Sets the adaptive execution configuration.
196    #[must_use]
197    pub fn with_adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
198        self.adaptive = adaptive;
199        self
200    }
201
202    /// Disables adaptive execution.
203    #[must_use]
204    pub fn without_adaptive(mut self) -> Self {
205        self.adaptive.enabled = false;
206        self
207    }
208
209    /// Disables factorized execution for multi-hop queries.
210    ///
211    /// This reverts to the traditional flat execution model where each expansion
212    /// creates a full Cartesian product. Only use this if you encounter issues
213    /// with factorized execution.
214    #[must_use]
215    pub fn without_factorized_execution(mut self) -> Self {
216        self.factorized_execution = false;
217        self
218    }
219}
220
221/// Helper function to get CPU count (fallback implementation).
222mod num_cpus {
223    #[cfg(not(target_arch = "wasm32"))]
224    pub fn get() -> usize {
225        std::thread::available_parallelism()
226            .map(|n| n.get())
227            .unwrap_or(4)
228    }
229
230    #[cfg(target_arch = "wasm32")]
231    pub fn get() -> usize {
232        1
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    #[test]
241    fn test_config_default() {
242        let config = Config::default();
243        assert!(config.path.is_none());
244        assert!(config.memory_limit.is_none());
245        assert!(config.spill_path.is_none());
246        assert!(config.threads > 0);
247        assert!(config.wal_enabled);
248        assert_eq!(config.wal_flush_interval_ms, 100);
249        assert!(config.backward_edges);
250        assert!(!config.query_logging);
251        assert!(config.factorized_execution);
252    }
253
254    #[test]
255    fn test_config_in_memory() {
256        let config = Config::in_memory();
257        assert!(config.path.is_none());
258        assert!(!config.wal_enabled);
259        assert!(config.backward_edges);
260    }
261
262    #[test]
263    fn test_config_persistent() {
264        let config = Config::persistent("/tmp/test_db");
265        assert_eq!(
266            config.path.as_deref(),
267            Some(std::path::Path::new("/tmp/test_db"))
268        );
269        assert!(config.wal_enabled);
270    }
271
272    #[test]
273    fn test_config_with_memory_limit() {
274        let config = Config::in_memory().with_memory_limit(1024 * 1024);
275        assert_eq!(config.memory_limit, Some(1024 * 1024));
276    }
277
278    #[test]
279    fn test_config_with_threads() {
280        let config = Config::in_memory().with_threads(8);
281        assert_eq!(config.threads, 8);
282    }
283
284    #[test]
285    fn test_config_without_backward_edges() {
286        let config = Config::in_memory().without_backward_edges();
287        assert!(!config.backward_edges);
288    }
289
290    #[test]
291    fn test_config_with_query_logging() {
292        let config = Config::in_memory().with_query_logging();
293        assert!(config.query_logging);
294    }
295
296    #[test]
297    fn test_config_with_spill_path() {
298        let config = Config::in_memory().with_spill_path("/tmp/spill");
299        assert_eq!(
300            config.spill_path.as_deref(),
301            Some(std::path::Path::new("/tmp/spill"))
302        );
303    }
304
305    #[test]
306    fn test_config_with_memory_fraction() {
307        let config = Config::in_memory().with_memory_fraction(0.5);
308        assert!(config.memory_limit.is_some());
309        assert!(config.memory_limit.unwrap() > 0);
310    }
311
312    #[test]
313    fn test_config_with_adaptive() {
314        let adaptive = AdaptiveConfig::default().with_threshold(5.0);
315        let config = Config::in_memory().with_adaptive(adaptive);
316        assert!((config.adaptive.threshold - 5.0).abs() < f64::EPSILON);
317    }
318
319    #[test]
320    fn test_config_without_adaptive() {
321        let config = Config::in_memory().without_adaptive();
322        assert!(!config.adaptive.enabled);
323    }
324
325    #[test]
326    fn test_config_without_factorized_execution() {
327        let config = Config::in_memory().without_factorized_execution();
328        assert!(!config.factorized_execution);
329    }
330
331    #[test]
332    fn test_config_builder_chaining() {
333        let config = Config::persistent("/tmp/db")
334            .with_memory_limit(512 * 1024 * 1024)
335            .with_threads(4)
336            .with_query_logging()
337            .without_backward_edges()
338            .with_spill_path("/tmp/spill");
339
340        assert!(config.path.is_some());
341        assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
342        assert_eq!(config.threads, 4);
343        assert!(config.query_logging);
344        assert!(!config.backward_edges);
345        assert!(config.spill_path.is_some());
346    }
347
348    #[test]
349    fn test_adaptive_config_default() {
350        let config = AdaptiveConfig::default();
351        assert!(config.enabled);
352        assert!((config.threshold - 3.0).abs() < f64::EPSILON);
353        assert_eq!(config.min_rows, 1000);
354        assert_eq!(config.max_reoptimizations, 3);
355    }
356
357    #[test]
358    fn test_adaptive_config_disabled() {
359        let config = AdaptiveConfig::disabled();
360        assert!(!config.enabled);
361    }
362
363    #[test]
364    fn test_adaptive_config_with_threshold() {
365        let config = AdaptiveConfig::default().with_threshold(10.0);
366        assert!((config.threshold - 10.0).abs() < f64::EPSILON);
367    }
368
369    #[test]
370    fn test_adaptive_config_with_min_rows() {
371        let config = AdaptiveConfig::default().with_min_rows(500);
372        assert_eq!(config.min_rows, 500);
373    }
374
375    #[test]
376    fn test_adaptive_config_with_max_reoptimizations() {
377        let config = AdaptiveConfig::default().with_max_reoptimizations(5);
378        assert_eq!(config.max_reoptimizations, 5);
379    }
380
381    #[test]
382    fn test_adaptive_config_builder_chaining() {
383        let config = AdaptiveConfig::default()
384            .with_threshold(2.0)
385            .with_min_rows(100)
386            .with_max_reoptimizations(10);
387        assert!((config.threshold - 2.0).abs() < f64::EPSILON);
388        assert_eq!(config.min_rows, 100);
389        assert_eq!(config.max_reoptimizations, 10);
390    }
391}