1use std::fmt;
4use std::path::PathBuf;
5use std::time::Duration;
6
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
15pub enum GraphModel {
16 #[default]
18 Lpg,
19 Rdf,
21}
22
23impl fmt::Display for GraphModel {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::Lpg => write!(f, "LPG"),
27 Self::Rdf => write!(f, "RDF"),
28 }
29 }
30}
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
38pub enum StorageFormat {
39 #[default]
42 Auto,
43 WalDirectory,
45 SingleFile,
48}
49
50impl fmt::Display for StorageFormat {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 match self {
53 Self::Auto => write!(f, "auto"),
54 Self::WalDirectory => write!(f, "wal-directory"),
55 Self::SingleFile => write!(f, "single-file"),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum DurabilityMode {
67 Sync,
69 Batch {
71 max_delay_ms: u64,
73 max_records: u64,
75 },
76 Adaptive {
78 target_interval_ms: u64,
80 },
81 NoSync,
83}
84
85impl Default for DurabilityMode {
86 fn default() -> Self {
87 Self::Batch {
88 max_delay_ms: 100,
89 max_records: 1000,
90 }
91 }
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum ConfigError {
97 ZeroMemoryLimit,
99 ZeroThreads,
101 ZeroWalFlushInterval,
103 RdfFeatureRequired,
105}
106
107impl fmt::Display for ConfigError {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 match self {
110 Self::ZeroMemoryLimit => write!(f, "memory_limit must be greater than zero"),
111 Self::ZeroThreads => write!(f, "threads must be greater than zero"),
112 Self::ZeroWalFlushInterval => {
113 write!(f, "wal_flush_interval_ms must be greater than zero")
114 }
115 Self::RdfFeatureRequired => {
116 write!(
117 f,
118 "RDF graph model requires the `rdf` feature flag to be enabled"
119 )
120 }
121 }
122 }
123}
124
125impl std::error::Error for ConfigError {}
126
127#[derive(Debug, Clone)]
129#[allow(clippy::struct_excessive_bools)] pub struct Config {
131 pub graph_model: GraphModel,
133 pub path: Option<PathBuf>,
135
136 pub memory_limit: Option<usize>,
138
139 pub spill_path: Option<PathBuf>,
141
142 pub threads: usize,
144
145 pub wal_enabled: bool,
147
148 pub wal_flush_interval_ms: u64,
150
151 pub backward_edges: bool,
153
154 pub query_logging: bool,
156
157 pub adaptive: AdaptiveConfig,
159
160 pub factorized_execution: bool,
168
169 pub wal_durability: DurabilityMode,
171
172 pub storage_format: StorageFormat,
177
178 pub schema_constraints: bool,
184
185 pub query_timeout: Option<Duration>,
191
192 pub gc_interval: usize,
197}
198
199#[derive(Debug, Clone)]
204pub struct AdaptiveConfig {
205 pub enabled: bool,
207
208 pub threshold: f64,
213
214 pub min_rows: u64,
218
219 pub max_reoptimizations: usize,
221}
222
223impl Default for AdaptiveConfig {
224 fn default() -> Self {
225 Self {
226 enabled: true,
227 threshold: 3.0,
228 min_rows: 1000,
229 max_reoptimizations: 3,
230 }
231 }
232}
233
234impl AdaptiveConfig {
235 #[must_use]
237 pub fn disabled() -> Self {
238 Self {
239 enabled: false,
240 ..Default::default()
241 }
242 }
243
244 #[must_use]
246 pub fn with_threshold(mut self, threshold: f64) -> Self {
247 self.threshold = threshold;
248 self
249 }
250
251 #[must_use]
253 pub fn with_min_rows(mut self, min_rows: u64) -> Self {
254 self.min_rows = min_rows;
255 self
256 }
257
258 #[must_use]
260 pub fn with_max_reoptimizations(mut self, max: usize) -> Self {
261 self.max_reoptimizations = max;
262 self
263 }
264}
265
266impl Default for Config {
267 fn default() -> Self {
268 Self {
269 graph_model: GraphModel::default(),
270 path: None,
271 memory_limit: None,
272 spill_path: None,
273 threads: num_cpus::get(),
274 wal_enabled: true,
275 wal_flush_interval_ms: 100,
276 backward_edges: true,
277 query_logging: false,
278 adaptive: AdaptiveConfig::default(),
279 factorized_execution: true,
280 wal_durability: DurabilityMode::default(),
281 storage_format: StorageFormat::default(),
282 schema_constraints: false,
283 query_timeout: None,
284 gc_interval: 100,
285 }
286 }
287}
288
289impl Config {
290 #[must_use]
292 pub fn in_memory() -> Self {
293 Self {
294 path: None,
295 wal_enabled: false,
296 ..Default::default()
297 }
298 }
299
300 #[must_use]
302 pub fn persistent(path: impl Into<PathBuf>) -> Self {
303 Self {
304 path: Some(path.into()),
305 wal_enabled: true,
306 ..Default::default()
307 }
308 }
309
310 #[must_use]
312 pub fn with_memory_limit(mut self, limit: usize) -> Self {
313 self.memory_limit = Some(limit);
314 self
315 }
316
317 #[must_use]
319 pub fn with_threads(mut self, threads: usize) -> Self {
320 self.threads = threads;
321 self
322 }
323
324 #[must_use]
326 pub fn without_backward_edges(mut self) -> Self {
327 self.backward_edges = false;
328 self
329 }
330
331 #[must_use]
333 pub fn with_query_logging(mut self) -> Self {
334 self.query_logging = true;
335 self
336 }
337
338 #[must_use]
340 pub fn with_memory_fraction(mut self, fraction: f64) -> Self {
341 use grafeo_common::memory::buffer::BufferManagerConfig;
342 let system_memory = BufferManagerConfig::detect_system_memory();
343 self.memory_limit = Some((system_memory as f64 * fraction) as usize);
344 self
345 }
346
347 #[must_use]
349 pub fn with_spill_path(mut self, path: impl Into<PathBuf>) -> Self {
350 self.spill_path = Some(path.into());
351 self
352 }
353
354 #[must_use]
356 pub fn with_adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
357 self.adaptive = adaptive;
358 self
359 }
360
361 #[must_use]
363 pub fn without_adaptive(mut self) -> Self {
364 self.adaptive.enabled = false;
365 self
366 }
367
368 #[must_use]
374 pub fn without_factorized_execution(mut self) -> Self {
375 self.factorized_execution = false;
376 self
377 }
378
379 #[must_use]
381 pub fn with_graph_model(mut self, model: GraphModel) -> Self {
382 self.graph_model = model;
383 self
384 }
385
386 #[must_use]
388 pub fn with_wal_durability(mut self, mode: DurabilityMode) -> Self {
389 self.wal_durability = mode;
390 self
391 }
392
393 #[must_use]
395 pub fn with_storage_format(mut self, format: StorageFormat) -> Self {
396 self.storage_format = format;
397 self
398 }
399
400 #[must_use]
402 pub fn with_schema_constraints(mut self) -> Self {
403 self.schema_constraints = true;
404 self
405 }
406
407 #[must_use]
409 pub fn with_query_timeout(mut self, timeout: Duration) -> Self {
410 self.query_timeout = Some(timeout);
411 self
412 }
413
414 #[must_use]
418 pub fn with_gc_interval(mut self, interval: usize) -> Self {
419 self.gc_interval = interval;
420 self
421 }
422
423 pub fn validate(&self) -> std::result::Result<(), ConfigError> {
431 if let Some(limit) = self.memory_limit
432 && limit == 0
433 {
434 return Err(ConfigError::ZeroMemoryLimit);
435 }
436
437 if self.threads == 0 {
438 return Err(ConfigError::ZeroThreads);
439 }
440
441 if self.wal_flush_interval_ms == 0 {
442 return Err(ConfigError::ZeroWalFlushInterval);
443 }
444
445 #[cfg(not(feature = "rdf"))]
446 if self.graph_model == GraphModel::Rdf {
447 return Err(ConfigError::RdfFeatureRequired);
448 }
449
450 Ok(())
451 }
452}
453
454mod num_cpus {
456 #[cfg(not(target_arch = "wasm32"))]
457 pub fn get() -> usize {
458 std::thread::available_parallelism()
459 .map(|n| n.get())
460 .unwrap_or(4)
461 }
462
463 #[cfg(target_arch = "wasm32")]
464 pub fn get() -> usize {
465 1
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn test_config_default() {
475 let config = Config::default();
476 assert_eq!(config.graph_model, GraphModel::Lpg);
477 assert!(config.path.is_none());
478 assert!(config.memory_limit.is_none());
479 assert!(config.spill_path.is_none());
480 assert!(config.threads > 0);
481 assert!(config.wal_enabled);
482 assert_eq!(config.wal_flush_interval_ms, 100);
483 assert!(config.backward_edges);
484 assert!(!config.query_logging);
485 assert!(config.factorized_execution);
486 assert_eq!(config.wal_durability, DurabilityMode::default());
487 assert!(!config.schema_constraints);
488 assert!(config.query_timeout.is_none());
489 assert_eq!(config.gc_interval, 100);
490 }
491
492 #[test]
493 fn test_config_in_memory() {
494 let config = Config::in_memory();
495 assert!(config.path.is_none());
496 assert!(!config.wal_enabled);
497 assert!(config.backward_edges);
498 }
499
500 #[test]
501 fn test_config_persistent() {
502 let config = Config::persistent("/tmp/test_db");
503 assert_eq!(
504 config.path.as_deref(),
505 Some(std::path::Path::new("/tmp/test_db"))
506 );
507 assert!(config.wal_enabled);
508 }
509
510 #[test]
511 fn test_config_with_memory_limit() {
512 let config = Config::in_memory().with_memory_limit(1024 * 1024);
513 assert_eq!(config.memory_limit, Some(1024 * 1024));
514 }
515
516 #[test]
517 fn test_config_with_threads() {
518 let config = Config::in_memory().with_threads(8);
519 assert_eq!(config.threads, 8);
520 }
521
522 #[test]
523 fn test_config_without_backward_edges() {
524 let config = Config::in_memory().without_backward_edges();
525 assert!(!config.backward_edges);
526 }
527
528 #[test]
529 fn test_config_with_query_logging() {
530 let config = Config::in_memory().with_query_logging();
531 assert!(config.query_logging);
532 }
533
534 #[test]
535 fn test_config_with_spill_path() {
536 let config = Config::in_memory().with_spill_path("/tmp/spill");
537 assert_eq!(
538 config.spill_path.as_deref(),
539 Some(std::path::Path::new("/tmp/spill"))
540 );
541 }
542
543 #[test]
544 fn test_config_with_memory_fraction() {
545 let config = Config::in_memory().with_memory_fraction(0.5);
546 assert!(config.memory_limit.is_some());
547 assert!(config.memory_limit.unwrap() > 0);
548 }
549
550 #[test]
551 fn test_config_with_adaptive() {
552 let adaptive = AdaptiveConfig::default().with_threshold(5.0);
553 let config = Config::in_memory().with_adaptive(adaptive);
554 assert!((config.adaptive.threshold - 5.0).abs() < f64::EPSILON);
555 }
556
557 #[test]
558 fn test_config_without_adaptive() {
559 let config = Config::in_memory().without_adaptive();
560 assert!(!config.adaptive.enabled);
561 }
562
563 #[test]
564 fn test_config_without_factorized_execution() {
565 let config = Config::in_memory().without_factorized_execution();
566 assert!(!config.factorized_execution);
567 }
568
569 #[test]
570 fn test_config_builder_chaining() {
571 let config = Config::persistent("/tmp/db")
572 .with_memory_limit(512 * 1024 * 1024)
573 .with_threads(4)
574 .with_query_logging()
575 .without_backward_edges()
576 .with_spill_path("/tmp/spill");
577
578 assert!(config.path.is_some());
579 assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
580 assert_eq!(config.threads, 4);
581 assert!(config.query_logging);
582 assert!(!config.backward_edges);
583 assert!(config.spill_path.is_some());
584 }
585
586 #[test]
587 fn test_adaptive_config_default() {
588 let config = AdaptiveConfig::default();
589 assert!(config.enabled);
590 assert!((config.threshold - 3.0).abs() < f64::EPSILON);
591 assert_eq!(config.min_rows, 1000);
592 assert_eq!(config.max_reoptimizations, 3);
593 }
594
595 #[test]
596 fn test_adaptive_config_disabled() {
597 let config = AdaptiveConfig::disabled();
598 assert!(!config.enabled);
599 }
600
601 #[test]
602 fn test_adaptive_config_with_threshold() {
603 let config = AdaptiveConfig::default().with_threshold(10.0);
604 assert!((config.threshold - 10.0).abs() < f64::EPSILON);
605 }
606
607 #[test]
608 fn test_adaptive_config_with_min_rows() {
609 let config = AdaptiveConfig::default().with_min_rows(500);
610 assert_eq!(config.min_rows, 500);
611 }
612
613 #[test]
614 fn test_adaptive_config_with_max_reoptimizations() {
615 let config = AdaptiveConfig::default().with_max_reoptimizations(5);
616 assert_eq!(config.max_reoptimizations, 5);
617 }
618
619 #[test]
620 fn test_adaptive_config_builder_chaining() {
621 let config = AdaptiveConfig::default()
622 .with_threshold(2.0)
623 .with_min_rows(100)
624 .with_max_reoptimizations(10);
625 assert!((config.threshold - 2.0).abs() < f64::EPSILON);
626 assert_eq!(config.min_rows, 100);
627 assert_eq!(config.max_reoptimizations, 10);
628 }
629
630 #[test]
633 fn test_graph_model_default_is_lpg() {
634 assert_eq!(GraphModel::default(), GraphModel::Lpg);
635 }
636
637 #[test]
638 fn test_graph_model_display() {
639 assert_eq!(GraphModel::Lpg.to_string(), "LPG");
640 assert_eq!(GraphModel::Rdf.to_string(), "RDF");
641 }
642
643 #[test]
644 fn test_config_with_graph_model() {
645 let config = Config::in_memory().with_graph_model(GraphModel::Rdf);
646 assert_eq!(config.graph_model, GraphModel::Rdf);
647 }
648
649 #[test]
652 fn test_durability_mode_default_is_batch() {
653 let mode = DurabilityMode::default();
654 assert_eq!(
655 mode,
656 DurabilityMode::Batch {
657 max_delay_ms: 100,
658 max_records: 1000
659 }
660 );
661 }
662
663 #[test]
664 fn test_config_with_wal_durability() {
665 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::Sync);
666 assert_eq!(config.wal_durability, DurabilityMode::Sync);
667 }
668
669 #[test]
670 fn test_config_with_wal_durability_nosync() {
671 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::NoSync);
672 assert_eq!(config.wal_durability, DurabilityMode::NoSync);
673 }
674
675 #[test]
676 fn test_config_with_wal_durability_adaptive() {
677 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::Adaptive {
678 target_interval_ms: 50,
679 });
680 assert_eq!(
681 config.wal_durability,
682 DurabilityMode::Adaptive {
683 target_interval_ms: 50
684 }
685 );
686 }
687
688 #[test]
691 fn test_config_with_schema_constraints() {
692 let config = Config::in_memory().with_schema_constraints();
693 assert!(config.schema_constraints);
694 }
695
696 #[test]
699 fn test_config_with_query_timeout() {
700 let config = Config::in_memory().with_query_timeout(Duration::from_secs(30));
701 assert_eq!(config.query_timeout, Some(Duration::from_secs(30)));
702 }
703
704 #[test]
707 fn test_config_with_gc_interval() {
708 let config = Config::in_memory().with_gc_interval(50);
709 assert_eq!(config.gc_interval, 50);
710 }
711
712 #[test]
713 fn test_config_gc_disabled() {
714 let config = Config::in_memory().with_gc_interval(0);
715 assert_eq!(config.gc_interval, 0);
716 }
717
718 #[test]
721 fn test_validate_default_config() {
722 assert!(Config::default().validate().is_ok());
723 }
724
725 #[test]
726 fn test_validate_in_memory_config() {
727 assert!(Config::in_memory().validate().is_ok());
728 }
729
730 #[test]
731 fn test_validate_rejects_zero_memory_limit() {
732 let config = Config::in_memory().with_memory_limit(0);
733 assert_eq!(config.validate(), Err(ConfigError::ZeroMemoryLimit));
734 }
735
736 #[test]
737 fn test_validate_rejects_zero_threads() {
738 let config = Config::in_memory().with_threads(0);
739 assert_eq!(config.validate(), Err(ConfigError::ZeroThreads));
740 }
741
742 #[test]
743 fn test_validate_rejects_zero_wal_flush_interval() {
744 let mut config = Config::in_memory();
745 config.wal_flush_interval_ms = 0;
746 assert_eq!(config.validate(), Err(ConfigError::ZeroWalFlushInterval));
747 }
748
749 #[cfg(not(feature = "rdf"))]
750 #[test]
751 fn test_validate_rejects_rdf_without_feature() {
752 let config = Config::in_memory().with_graph_model(GraphModel::Rdf);
753 assert_eq!(config.validate(), Err(ConfigError::RdfFeatureRequired));
754 }
755
756 #[test]
757 fn test_config_error_display() {
758 assert_eq!(
759 ConfigError::ZeroMemoryLimit.to_string(),
760 "memory_limit must be greater than zero"
761 );
762 assert_eq!(
763 ConfigError::ZeroThreads.to_string(),
764 "threads must be greater than zero"
765 );
766 assert_eq!(
767 ConfigError::ZeroWalFlushInterval.to_string(),
768 "wal_flush_interval_ms must be greater than zero"
769 );
770 assert_eq!(
771 ConfigError::RdfFeatureRequired.to_string(),
772 "RDF graph model requires the `rdf` feature flag to be enabled"
773 );
774 }
775
776 #[test]
779 fn test_config_full_builder_chaining() {
780 let config = Config::persistent("/tmp/db")
781 .with_graph_model(GraphModel::Lpg)
782 .with_memory_limit(512 * 1024 * 1024)
783 .with_threads(4)
784 .with_query_logging()
785 .with_wal_durability(DurabilityMode::Sync)
786 .with_schema_constraints()
787 .without_backward_edges()
788 .with_spill_path("/tmp/spill")
789 .with_query_timeout(Duration::from_secs(60));
790
791 assert_eq!(config.graph_model, GraphModel::Lpg);
792 assert!(config.path.is_some());
793 assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
794 assert_eq!(config.threads, 4);
795 assert!(config.query_logging);
796 assert_eq!(config.wal_durability, DurabilityMode::Sync);
797 assert!(config.schema_constraints);
798 assert!(!config.backward_edges);
799 assert!(config.spill_path.is_some());
800 assert_eq!(config.query_timeout, Some(Duration::from_secs(60)));
801 assert!(config.validate().is_ok());
802 }
803}