1use std::fmt;
4use std::path::PathBuf;
5use std::time::Duration;
6
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum GraphModel {
17 #[default]
19 Lpg,
20 Rdf,
22}
23
24impl fmt::Display for GraphModel {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Self::Lpg => write!(f, "LPG"),
28 Self::Rdf => write!(f, "RDF"),
29 }
30 }
31}
32
33#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
40#[non_exhaustive]
41pub enum AccessMode {
42 #[default]
44 ReadWrite,
45 ReadOnly,
49}
50
51impl fmt::Display for AccessMode {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 match self {
54 Self::ReadWrite => write!(f, "read-write"),
55 Self::ReadOnly => write!(f, "read-only"),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
66#[non_exhaustive]
67pub enum StorageFormat {
68 #[default]
71 Auto,
72 WalDirectory,
74 SingleFile,
77}
78
79impl fmt::Display for StorageFormat {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match self {
82 Self::Auto => write!(f, "auto"),
83 Self::WalDirectory => write!(f, "wal-directory"),
84 Self::SingleFile => write!(f, "single-file"),
85 }
86 }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95#[non_exhaustive]
96pub enum DurabilityMode {
97 Sync,
99 Batch {
101 max_delay_ms: u64,
103 max_records: u64,
105 },
106 Adaptive {
108 target_interval_ms: u64,
110 },
111 NoSync,
113}
114
115impl Default for DurabilityMode {
116 fn default() -> Self {
117 Self::Batch {
118 max_delay_ms: 100,
119 max_records: 1000,
120 }
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
126pub enum ConfigError {
127 ZeroMemoryLimit,
129 ZeroThreads,
131 ZeroWalFlushInterval,
133 RdfFeatureRequired,
135}
136
137impl fmt::Display for ConfigError {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 match self {
140 Self::ZeroMemoryLimit => write!(f, "memory_limit must be greater than zero"),
141 Self::ZeroThreads => write!(f, "threads must be greater than zero"),
142 Self::ZeroWalFlushInterval => {
143 write!(f, "wal_flush_interval_ms must be greater than zero")
144 }
145 Self::RdfFeatureRequired => {
146 write!(
147 f,
148 "RDF graph model requires the `rdf` feature flag to be enabled"
149 )
150 }
151 }
152 }
153}
154
155impl std::error::Error for ConfigError {}
156
157#[derive(Debug, Clone)]
159#[allow(clippy::struct_excessive_bools)] pub struct Config {
161 pub graph_model: GraphModel,
163 pub path: Option<PathBuf>,
165
166 pub memory_limit: Option<usize>,
168
169 pub spill_path: Option<PathBuf>,
171
172 pub threads: usize,
174
175 pub wal_enabled: bool,
177
178 pub wal_flush_interval_ms: u64,
180
181 pub backward_edges: bool,
183
184 pub query_logging: bool,
186
187 pub adaptive: AdaptiveConfig,
189
190 pub factorized_execution: bool,
198
199 pub wal_durability: DurabilityMode,
201
202 pub storage_format: StorageFormat,
207
208 pub schema_constraints: bool,
214
215 pub query_timeout: Option<Duration>,
221
222 pub gc_interval: usize,
227
228 pub access_mode: AccessMode,
234
235 pub cdc_enabled: bool,
246}
247
248#[derive(Debug, Clone)]
253pub struct AdaptiveConfig {
254 pub enabled: bool,
256
257 pub threshold: f64,
262
263 pub min_rows: u64,
267
268 pub max_reoptimizations: usize,
270}
271
272impl Default for AdaptiveConfig {
273 fn default() -> Self {
274 Self {
275 enabled: true,
276 threshold: 3.0,
277 min_rows: 1000,
278 max_reoptimizations: 3,
279 }
280 }
281}
282
283impl AdaptiveConfig {
284 #[must_use]
286 pub fn disabled() -> Self {
287 Self {
288 enabled: false,
289 ..Default::default()
290 }
291 }
292
293 #[must_use]
295 pub fn with_threshold(mut self, threshold: f64) -> Self {
296 self.threshold = threshold;
297 self
298 }
299
300 #[must_use]
302 pub fn with_min_rows(mut self, min_rows: u64) -> Self {
303 self.min_rows = min_rows;
304 self
305 }
306
307 #[must_use]
309 pub fn with_max_reoptimizations(mut self, max: usize) -> Self {
310 self.max_reoptimizations = max;
311 self
312 }
313}
314
315impl Default for Config {
316 fn default() -> Self {
317 Self {
318 graph_model: GraphModel::default(),
319 path: None,
320 memory_limit: None,
321 spill_path: None,
322 threads: num_cpus::get(),
323 wal_enabled: true,
324 wal_flush_interval_ms: 100,
325 backward_edges: true,
326 query_logging: false,
327 adaptive: AdaptiveConfig::default(),
328 factorized_execution: true,
329 wal_durability: DurabilityMode::default(),
330 storage_format: StorageFormat::default(),
331 schema_constraints: false,
332 query_timeout: None,
333 gc_interval: 100,
334 access_mode: AccessMode::default(),
335 cdc_enabled: false,
336 }
337 }
338}
339
340impl Config {
341 #[must_use]
343 pub fn in_memory() -> Self {
344 Self {
345 path: None,
346 wal_enabled: false,
347 ..Default::default()
348 }
349 }
350
351 #[must_use]
353 pub fn persistent(path: impl Into<PathBuf>) -> Self {
354 Self {
355 path: Some(path.into()),
356 wal_enabled: true,
357 ..Default::default()
358 }
359 }
360
361 #[must_use]
363 pub fn with_memory_limit(mut self, limit: usize) -> Self {
364 self.memory_limit = Some(limit);
365 self
366 }
367
368 #[must_use]
370 pub fn with_threads(mut self, threads: usize) -> Self {
371 self.threads = threads;
372 self
373 }
374
375 #[must_use]
377 pub fn without_backward_edges(mut self) -> Self {
378 self.backward_edges = false;
379 self
380 }
381
382 #[must_use]
384 pub fn with_query_logging(mut self) -> Self {
385 self.query_logging = true;
386 self
387 }
388
389 #[must_use]
391 pub fn with_memory_fraction(mut self, fraction: f64) -> Self {
392 use grafeo_common::memory::buffer::BufferManagerConfig;
393 let system_memory = BufferManagerConfig::detect_system_memory();
394 self.memory_limit = Some((system_memory as f64 * fraction) as usize);
395 self
396 }
397
398 #[must_use]
400 pub fn with_spill_path(mut self, path: impl Into<PathBuf>) -> Self {
401 self.spill_path = Some(path.into());
402 self
403 }
404
405 #[must_use]
407 pub fn with_adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
408 self.adaptive = adaptive;
409 self
410 }
411
412 #[must_use]
414 pub fn without_adaptive(mut self) -> Self {
415 self.adaptive.enabled = false;
416 self
417 }
418
419 #[must_use]
425 pub fn without_factorized_execution(mut self) -> Self {
426 self.factorized_execution = false;
427 self
428 }
429
430 #[must_use]
432 pub fn with_graph_model(mut self, model: GraphModel) -> Self {
433 self.graph_model = model;
434 self
435 }
436
437 #[must_use]
439 pub fn with_wal_durability(mut self, mode: DurabilityMode) -> Self {
440 self.wal_durability = mode;
441 self
442 }
443
444 #[must_use]
446 pub fn with_storage_format(mut self, format: StorageFormat) -> Self {
447 self.storage_format = format;
448 self
449 }
450
451 #[must_use]
453 pub fn with_schema_constraints(mut self) -> Self {
454 self.schema_constraints = true;
455 self
456 }
457
458 #[must_use]
460 pub fn with_query_timeout(mut self, timeout: Duration) -> Self {
461 self.query_timeout = Some(timeout);
462 self
463 }
464
465 #[must_use]
469 pub fn with_gc_interval(mut self, interval: usize) -> Self {
470 self.gc_interval = interval;
471 self
472 }
473
474 #[must_use]
476 pub fn with_access_mode(mut self, mode: AccessMode) -> Self {
477 self.access_mode = mode;
478 self
479 }
480
481 #[must_use]
486 pub fn read_only(path: impl Into<PathBuf>) -> Self {
487 Self {
488 path: Some(path.into()),
489 wal_enabled: false,
490 access_mode: AccessMode::ReadOnly,
491 ..Default::default()
492 }
493 }
494
495 #[must_use]
503 pub fn with_cdc(mut self) -> Self {
504 self.cdc_enabled = true;
505 self
506 }
507
508 pub fn validate(&self) -> std::result::Result<(), ConfigError> {
516 if let Some(limit) = self.memory_limit
517 && limit == 0
518 {
519 return Err(ConfigError::ZeroMemoryLimit);
520 }
521
522 if self.threads == 0 {
523 return Err(ConfigError::ZeroThreads);
524 }
525
526 if self.wal_flush_interval_ms == 0 {
527 return Err(ConfigError::ZeroWalFlushInterval);
528 }
529
530 #[cfg(not(feature = "rdf"))]
531 if self.graph_model == GraphModel::Rdf {
532 return Err(ConfigError::RdfFeatureRequired);
533 }
534
535 Ok(())
536 }
537}
538
539mod num_cpus {
541 #[cfg(not(target_arch = "wasm32"))]
542 pub fn get() -> usize {
543 std::thread::available_parallelism()
544 .map(|n| n.get())
545 .unwrap_or(4)
546 }
547
548 #[cfg(target_arch = "wasm32")]
549 pub fn get() -> usize {
550 1
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557
558 #[test]
559 fn test_config_default() {
560 let config = Config::default();
561 assert_eq!(config.graph_model, GraphModel::Lpg);
562 assert!(config.path.is_none());
563 assert!(config.memory_limit.is_none());
564 assert!(config.spill_path.is_none());
565 assert!(config.threads > 0);
566 assert!(config.wal_enabled);
567 assert_eq!(config.wal_flush_interval_ms, 100);
568 assert!(config.backward_edges);
569 assert!(!config.query_logging);
570 assert!(config.factorized_execution);
571 assert_eq!(config.wal_durability, DurabilityMode::default());
572 assert!(!config.schema_constraints);
573 assert!(config.query_timeout.is_none());
574 assert_eq!(config.gc_interval, 100);
575 }
576
577 #[test]
578 fn test_config_in_memory() {
579 let config = Config::in_memory();
580 assert!(config.path.is_none());
581 assert!(!config.wal_enabled);
582 assert!(config.backward_edges);
583 }
584
585 #[test]
586 fn test_config_persistent() {
587 let config = Config::persistent("/tmp/test_db");
588 assert_eq!(
589 config.path.as_deref(),
590 Some(std::path::Path::new("/tmp/test_db"))
591 );
592 assert!(config.wal_enabled);
593 }
594
595 #[test]
596 fn test_config_with_memory_limit() {
597 let config = Config::in_memory().with_memory_limit(1024 * 1024);
598 assert_eq!(config.memory_limit, Some(1024 * 1024));
599 }
600
601 #[test]
602 fn test_config_with_threads() {
603 let config = Config::in_memory().with_threads(8);
604 assert_eq!(config.threads, 8);
605 }
606
607 #[test]
608 fn test_config_without_backward_edges() {
609 let config = Config::in_memory().without_backward_edges();
610 assert!(!config.backward_edges);
611 }
612
613 #[test]
614 fn test_config_with_query_logging() {
615 let config = Config::in_memory().with_query_logging();
616 assert!(config.query_logging);
617 }
618
619 #[test]
620 fn test_config_with_spill_path() {
621 let config = Config::in_memory().with_spill_path("/tmp/spill");
622 assert_eq!(
623 config.spill_path.as_deref(),
624 Some(std::path::Path::new("/tmp/spill"))
625 );
626 }
627
628 #[test]
629 fn test_config_with_memory_fraction() {
630 let config = Config::in_memory().with_memory_fraction(0.5);
631 assert!(config.memory_limit.is_some());
632 assert!(config.memory_limit.unwrap() > 0);
633 }
634
635 #[test]
636 fn test_config_with_adaptive() {
637 let adaptive = AdaptiveConfig::default().with_threshold(5.0);
638 let config = Config::in_memory().with_adaptive(adaptive);
639 assert!((config.adaptive.threshold - 5.0).abs() < f64::EPSILON);
640 }
641
642 #[test]
643 fn test_config_without_adaptive() {
644 let config = Config::in_memory().without_adaptive();
645 assert!(!config.adaptive.enabled);
646 }
647
648 #[test]
649 fn test_config_without_factorized_execution() {
650 let config = Config::in_memory().without_factorized_execution();
651 assert!(!config.factorized_execution);
652 }
653
654 #[test]
655 fn test_config_builder_chaining() {
656 let config = Config::persistent("/tmp/db")
657 .with_memory_limit(512 * 1024 * 1024)
658 .with_threads(4)
659 .with_query_logging()
660 .without_backward_edges()
661 .with_spill_path("/tmp/spill");
662
663 assert!(config.path.is_some());
664 assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
665 assert_eq!(config.threads, 4);
666 assert!(config.query_logging);
667 assert!(!config.backward_edges);
668 assert!(config.spill_path.is_some());
669 }
670
671 #[test]
672 fn test_adaptive_config_default() {
673 let config = AdaptiveConfig::default();
674 assert!(config.enabled);
675 assert!((config.threshold - 3.0).abs() < f64::EPSILON);
676 assert_eq!(config.min_rows, 1000);
677 assert_eq!(config.max_reoptimizations, 3);
678 }
679
680 #[test]
681 fn test_adaptive_config_disabled() {
682 let config = AdaptiveConfig::disabled();
683 assert!(!config.enabled);
684 }
685
686 #[test]
687 fn test_adaptive_config_with_threshold() {
688 let config = AdaptiveConfig::default().with_threshold(10.0);
689 assert!((config.threshold - 10.0).abs() < f64::EPSILON);
690 }
691
692 #[test]
693 fn test_adaptive_config_with_min_rows() {
694 let config = AdaptiveConfig::default().with_min_rows(500);
695 assert_eq!(config.min_rows, 500);
696 }
697
698 #[test]
699 fn test_adaptive_config_with_max_reoptimizations() {
700 let config = AdaptiveConfig::default().with_max_reoptimizations(5);
701 assert_eq!(config.max_reoptimizations, 5);
702 }
703
704 #[test]
705 fn test_adaptive_config_builder_chaining() {
706 let config = AdaptiveConfig::default()
707 .with_threshold(2.0)
708 .with_min_rows(100)
709 .with_max_reoptimizations(10);
710 assert!((config.threshold - 2.0).abs() < f64::EPSILON);
711 assert_eq!(config.min_rows, 100);
712 assert_eq!(config.max_reoptimizations, 10);
713 }
714
715 #[test]
718 fn test_graph_model_default_is_lpg() {
719 assert_eq!(GraphModel::default(), GraphModel::Lpg);
720 }
721
722 #[test]
723 fn test_graph_model_display() {
724 assert_eq!(GraphModel::Lpg.to_string(), "LPG");
725 assert_eq!(GraphModel::Rdf.to_string(), "RDF");
726 }
727
728 #[test]
729 fn test_config_with_graph_model() {
730 let config = Config::in_memory().with_graph_model(GraphModel::Rdf);
731 assert_eq!(config.graph_model, GraphModel::Rdf);
732 }
733
734 #[test]
737 fn test_durability_mode_default_is_batch() {
738 let mode = DurabilityMode::default();
739 assert_eq!(
740 mode,
741 DurabilityMode::Batch {
742 max_delay_ms: 100,
743 max_records: 1000
744 }
745 );
746 }
747
748 #[test]
749 fn test_config_with_wal_durability() {
750 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::Sync);
751 assert_eq!(config.wal_durability, DurabilityMode::Sync);
752 }
753
754 #[test]
755 fn test_config_with_wal_durability_nosync() {
756 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::NoSync);
757 assert_eq!(config.wal_durability, DurabilityMode::NoSync);
758 }
759
760 #[test]
761 fn test_config_with_wal_durability_adaptive() {
762 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::Adaptive {
763 target_interval_ms: 50,
764 });
765 assert_eq!(
766 config.wal_durability,
767 DurabilityMode::Adaptive {
768 target_interval_ms: 50
769 }
770 );
771 }
772
773 #[test]
776 fn test_config_with_schema_constraints() {
777 let config = Config::in_memory().with_schema_constraints();
778 assert!(config.schema_constraints);
779 }
780
781 #[test]
784 fn test_config_with_query_timeout() {
785 let config = Config::in_memory().with_query_timeout(Duration::from_secs(30));
786 assert_eq!(config.query_timeout, Some(Duration::from_secs(30)));
787 }
788
789 #[test]
792 fn test_config_with_gc_interval() {
793 let config = Config::in_memory().with_gc_interval(50);
794 assert_eq!(config.gc_interval, 50);
795 }
796
797 #[test]
798 fn test_config_gc_disabled() {
799 let config = Config::in_memory().with_gc_interval(0);
800 assert_eq!(config.gc_interval, 0);
801 }
802
803 #[test]
806 fn test_validate_default_config() {
807 assert!(Config::default().validate().is_ok());
808 }
809
810 #[test]
811 fn test_validate_in_memory_config() {
812 assert!(Config::in_memory().validate().is_ok());
813 }
814
815 #[test]
816 fn test_validate_rejects_zero_memory_limit() {
817 let config = Config::in_memory().with_memory_limit(0);
818 assert_eq!(config.validate(), Err(ConfigError::ZeroMemoryLimit));
819 }
820
821 #[test]
822 fn test_validate_rejects_zero_threads() {
823 let config = Config::in_memory().with_threads(0);
824 assert_eq!(config.validate(), Err(ConfigError::ZeroThreads));
825 }
826
827 #[test]
828 fn test_validate_rejects_zero_wal_flush_interval() {
829 let mut config = Config::in_memory();
830 config.wal_flush_interval_ms = 0;
831 assert_eq!(config.validate(), Err(ConfigError::ZeroWalFlushInterval));
832 }
833
834 #[cfg(not(feature = "rdf"))]
835 #[test]
836 fn test_validate_rejects_rdf_without_feature() {
837 let config = Config::in_memory().with_graph_model(GraphModel::Rdf);
838 assert_eq!(config.validate(), Err(ConfigError::RdfFeatureRequired));
839 }
840
841 #[test]
842 fn test_config_error_display() {
843 assert_eq!(
844 ConfigError::ZeroMemoryLimit.to_string(),
845 "memory_limit must be greater than zero"
846 );
847 assert_eq!(
848 ConfigError::ZeroThreads.to_string(),
849 "threads must be greater than zero"
850 );
851 assert_eq!(
852 ConfigError::ZeroWalFlushInterval.to_string(),
853 "wal_flush_interval_ms must be greater than zero"
854 );
855 assert_eq!(
856 ConfigError::RdfFeatureRequired.to_string(),
857 "RDF graph model requires the `rdf` feature flag to be enabled"
858 );
859 }
860
861 #[test]
864 fn test_config_full_builder_chaining() {
865 let config = Config::persistent("/tmp/db")
866 .with_graph_model(GraphModel::Lpg)
867 .with_memory_limit(512 * 1024 * 1024)
868 .with_threads(4)
869 .with_query_logging()
870 .with_wal_durability(DurabilityMode::Sync)
871 .with_schema_constraints()
872 .without_backward_edges()
873 .with_spill_path("/tmp/spill")
874 .with_query_timeout(Duration::from_secs(60));
875
876 assert_eq!(config.graph_model, GraphModel::Lpg);
877 assert!(config.path.is_some());
878 assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
879 assert_eq!(config.threads, 4);
880 assert!(config.query_logging);
881 assert_eq!(config.wal_durability, DurabilityMode::Sync);
882 assert!(config.schema_constraints);
883 assert!(!config.backward_edges);
884 assert!(config.spill_path.is_some());
885 assert_eq!(config.query_timeout, Some(Duration::from_secs(60)));
886 assert!(config.validate().is_ok());
887 }
888
889 #[test]
892 fn test_access_mode_default_is_read_write() {
893 assert_eq!(AccessMode::default(), AccessMode::ReadWrite);
894 }
895
896 #[test]
897 fn test_access_mode_display() {
898 assert_eq!(AccessMode::ReadWrite.to_string(), "read-write");
899 assert_eq!(AccessMode::ReadOnly.to_string(), "read-only");
900 }
901
902 #[test]
903 fn test_config_with_access_mode() {
904 let config = Config::persistent("/tmp/db").with_access_mode(AccessMode::ReadOnly);
905 assert_eq!(config.access_mode, AccessMode::ReadOnly);
906 }
907
908 #[test]
909 fn test_config_read_only() {
910 let config = Config::read_only("/tmp/db.grafeo");
911 assert_eq!(config.access_mode, AccessMode::ReadOnly);
912 assert!(config.path.is_some());
913 assert!(!config.wal_enabled);
914 }
915
916 #[test]
917 fn test_config_default_is_read_write() {
918 let config = Config::default();
919 assert_eq!(config.access_mode, AccessMode::ReadWrite);
920 }
921}