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)]
39pub enum AccessMode {
40 #[default]
42 ReadWrite,
43 ReadOnly,
47}
48
49impl fmt::Display for AccessMode {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::ReadWrite => write!(f, "read-write"),
53 Self::ReadOnly => write!(f, "read-only"),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
64pub enum StorageFormat {
65 #[default]
68 Auto,
69 WalDirectory,
71 SingleFile,
74}
75
76impl fmt::Display for StorageFormat {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::Auto => write!(f, "auto"),
80 Self::WalDirectory => write!(f, "wal-directory"),
81 Self::SingleFile => write!(f, "single-file"),
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum DurabilityMode {
93 Sync,
95 Batch {
97 max_delay_ms: u64,
99 max_records: u64,
101 },
102 Adaptive {
104 target_interval_ms: u64,
106 },
107 NoSync,
109}
110
111impl Default for DurabilityMode {
112 fn default() -> Self {
113 Self::Batch {
114 max_delay_ms: 100,
115 max_records: 1000,
116 }
117 }
118}
119
120#[derive(Debug, Clone, PartialEq, Eq)]
122pub enum ConfigError {
123 ZeroMemoryLimit,
125 ZeroThreads,
127 ZeroWalFlushInterval,
129 RdfFeatureRequired,
131}
132
133impl fmt::Display for ConfigError {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match self {
136 Self::ZeroMemoryLimit => write!(f, "memory_limit must be greater than zero"),
137 Self::ZeroThreads => write!(f, "threads must be greater than zero"),
138 Self::ZeroWalFlushInterval => {
139 write!(f, "wal_flush_interval_ms must be greater than zero")
140 }
141 Self::RdfFeatureRequired => {
142 write!(
143 f,
144 "RDF graph model requires the `rdf` feature flag to be enabled"
145 )
146 }
147 }
148 }
149}
150
151impl std::error::Error for ConfigError {}
152
153#[derive(Debug, Clone)]
155#[allow(clippy::struct_excessive_bools)] pub struct Config {
157 pub graph_model: GraphModel,
159 pub path: Option<PathBuf>,
161
162 pub memory_limit: Option<usize>,
164
165 pub spill_path: Option<PathBuf>,
167
168 pub threads: usize,
170
171 pub wal_enabled: bool,
173
174 pub wal_flush_interval_ms: u64,
176
177 pub backward_edges: bool,
179
180 pub query_logging: bool,
182
183 pub adaptive: AdaptiveConfig,
185
186 pub factorized_execution: bool,
194
195 pub wal_durability: DurabilityMode,
197
198 pub storage_format: StorageFormat,
203
204 pub schema_constraints: bool,
210
211 pub query_timeout: Option<Duration>,
217
218 pub gc_interval: usize,
223
224 pub access_mode: AccessMode,
230
231 pub cdc_enabled: bool,
242}
243
244#[derive(Debug, Clone)]
249pub struct AdaptiveConfig {
250 pub enabled: bool,
252
253 pub threshold: f64,
258
259 pub min_rows: u64,
263
264 pub max_reoptimizations: usize,
266}
267
268impl Default for AdaptiveConfig {
269 fn default() -> Self {
270 Self {
271 enabled: true,
272 threshold: 3.0,
273 min_rows: 1000,
274 max_reoptimizations: 3,
275 }
276 }
277}
278
279impl AdaptiveConfig {
280 #[must_use]
282 pub fn disabled() -> Self {
283 Self {
284 enabled: false,
285 ..Default::default()
286 }
287 }
288
289 #[must_use]
291 pub fn with_threshold(mut self, threshold: f64) -> Self {
292 self.threshold = threshold;
293 self
294 }
295
296 #[must_use]
298 pub fn with_min_rows(mut self, min_rows: u64) -> Self {
299 self.min_rows = min_rows;
300 self
301 }
302
303 #[must_use]
305 pub fn with_max_reoptimizations(mut self, max: usize) -> Self {
306 self.max_reoptimizations = max;
307 self
308 }
309}
310
311impl Default for Config {
312 fn default() -> Self {
313 Self {
314 graph_model: GraphModel::default(),
315 path: None,
316 memory_limit: None,
317 spill_path: None,
318 threads: num_cpus::get(),
319 wal_enabled: true,
320 wal_flush_interval_ms: 100,
321 backward_edges: true,
322 query_logging: false,
323 adaptive: AdaptiveConfig::default(),
324 factorized_execution: true,
325 wal_durability: DurabilityMode::default(),
326 storage_format: StorageFormat::default(),
327 schema_constraints: false,
328 query_timeout: None,
329 gc_interval: 100,
330 access_mode: AccessMode::default(),
331 cdc_enabled: false,
332 }
333 }
334}
335
336impl Config {
337 #[must_use]
339 pub fn in_memory() -> Self {
340 Self {
341 path: None,
342 wal_enabled: false,
343 ..Default::default()
344 }
345 }
346
347 #[must_use]
349 pub fn persistent(path: impl Into<PathBuf>) -> Self {
350 Self {
351 path: Some(path.into()),
352 wal_enabled: true,
353 ..Default::default()
354 }
355 }
356
357 #[must_use]
359 pub fn with_memory_limit(mut self, limit: usize) -> Self {
360 self.memory_limit = Some(limit);
361 self
362 }
363
364 #[must_use]
366 pub fn with_threads(mut self, threads: usize) -> Self {
367 self.threads = threads;
368 self
369 }
370
371 #[must_use]
373 pub fn without_backward_edges(mut self) -> Self {
374 self.backward_edges = false;
375 self
376 }
377
378 #[must_use]
380 pub fn with_query_logging(mut self) -> Self {
381 self.query_logging = true;
382 self
383 }
384
385 #[must_use]
387 pub fn with_memory_fraction(mut self, fraction: f64) -> Self {
388 use grafeo_common::memory::buffer::BufferManagerConfig;
389 let system_memory = BufferManagerConfig::detect_system_memory();
390 self.memory_limit = Some((system_memory as f64 * fraction) as usize);
391 self
392 }
393
394 #[must_use]
396 pub fn with_spill_path(mut self, path: impl Into<PathBuf>) -> Self {
397 self.spill_path = Some(path.into());
398 self
399 }
400
401 #[must_use]
403 pub fn with_adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
404 self.adaptive = adaptive;
405 self
406 }
407
408 #[must_use]
410 pub fn without_adaptive(mut self) -> Self {
411 self.adaptive.enabled = false;
412 self
413 }
414
415 #[must_use]
421 pub fn without_factorized_execution(mut self) -> Self {
422 self.factorized_execution = false;
423 self
424 }
425
426 #[must_use]
428 pub fn with_graph_model(mut self, model: GraphModel) -> Self {
429 self.graph_model = model;
430 self
431 }
432
433 #[must_use]
435 pub fn with_wal_durability(mut self, mode: DurabilityMode) -> Self {
436 self.wal_durability = mode;
437 self
438 }
439
440 #[must_use]
442 pub fn with_storage_format(mut self, format: StorageFormat) -> Self {
443 self.storage_format = format;
444 self
445 }
446
447 #[must_use]
449 pub fn with_schema_constraints(mut self) -> Self {
450 self.schema_constraints = true;
451 self
452 }
453
454 #[must_use]
456 pub fn with_query_timeout(mut self, timeout: Duration) -> Self {
457 self.query_timeout = Some(timeout);
458 self
459 }
460
461 #[must_use]
465 pub fn with_gc_interval(mut self, interval: usize) -> Self {
466 self.gc_interval = interval;
467 self
468 }
469
470 #[must_use]
472 pub fn with_access_mode(mut self, mode: AccessMode) -> Self {
473 self.access_mode = mode;
474 self
475 }
476
477 #[must_use]
482 pub fn read_only(path: impl Into<PathBuf>) -> Self {
483 Self {
484 path: Some(path.into()),
485 wal_enabled: false,
486 access_mode: AccessMode::ReadOnly,
487 ..Default::default()
488 }
489 }
490
491 #[must_use]
499 pub fn with_cdc(mut self) -> Self {
500 self.cdc_enabled = true;
501 self
502 }
503
504 pub fn validate(&self) -> std::result::Result<(), ConfigError> {
512 if let Some(limit) = self.memory_limit
513 && limit == 0
514 {
515 return Err(ConfigError::ZeroMemoryLimit);
516 }
517
518 if self.threads == 0 {
519 return Err(ConfigError::ZeroThreads);
520 }
521
522 if self.wal_flush_interval_ms == 0 {
523 return Err(ConfigError::ZeroWalFlushInterval);
524 }
525
526 #[cfg(not(feature = "rdf"))]
527 if self.graph_model == GraphModel::Rdf {
528 return Err(ConfigError::RdfFeatureRequired);
529 }
530
531 Ok(())
532 }
533}
534
535mod num_cpus {
537 #[cfg(not(target_arch = "wasm32"))]
538 pub fn get() -> usize {
539 std::thread::available_parallelism()
540 .map(|n| n.get())
541 .unwrap_or(4)
542 }
543
544 #[cfg(target_arch = "wasm32")]
545 pub fn get() -> usize {
546 1
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
555 fn test_config_default() {
556 let config = Config::default();
557 assert_eq!(config.graph_model, GraphModel::Lpg);
558 assert!(config.path.is_none());
559 assert!(config.memory_limit.is_none());
560 assert!(config.spill_path.is_none());
561 assert!(config.threads > 0);
562 assert!(config.wal_enabled);
563 assert_eq!(config.wal_flush_interval_ms, 100);
564 assert!(config.backward_edges);
565 assert!(!config.query_logging);
566 assert!(config.factorized_execution);
567 assert_eq!(config.wal_durability, DurabilityMode::default());
568 assert!(!config.schema_constraints);
569 assert!(config.query_timeout.is_none());
570 assert_eq!(config.gc_interval, 100);
571 }
572
573 #[test]
574 fn test_config_in_memory() {
575 let config = Config::in_memory();
576 assert!(config.path.is_none());
577 assert!(!config.wal_enabled);
578 assert!(config.backward_edges);
579 }
580
581 #[test]
582 fn test_config_persistent() {
583 let config = Config::persistent("/tmp/test_db");
584 assert_eq!(
585 config.path.as_deref(),
586 Some(std::path::Path::new("/tmp/test_db"))
587 );
588 assert!(config.wal_enabled);
589 }
590
591 #[test]
592 fn test_config_with_memory_limit() {
593 let config = Config::in_memory().with_memory_limit(1024 * 1024);
594 assert_eq!(config.memory_limit, Some(1024 * 1024));
595 }
596
597 #[test]
598 fn test_config_with_threads() {
599 let config = Config::in_memory().with_threads(8);
600 assert_eq!(config.threads, 8);
601 }
602
603 #[test]
604 fn test_config_without_backward_edges() {
605 let config = Config::in_memory().without_backward_edges();
606 assert!(!config.backward_edges);
607 }
608
609 #[test]
610 fn test_config_with_query_logging() {
611 let config = Config::in_memory().with_query_logging();
612 assert!(config.query_logging);
613 }
614
615 #[test]
616 fn test_config_with_spill_path() {
617 let config = Config::in_memory().with_spill_path("/tmp/spill");
618 assert_eq!(
619 config.spill_path.as_deref(),
620 Some(std::path::Path::new("/tmp/spill"))
621 );
622 }
623
624 #[test]
625 fn test_config_with_memory_fraction() {
626 let config = Config::in_memory().with_memory_fraction(0.5);
627 assert!(config.memory_limit.is_some());
628 assert!(config.memory_limit.unwrap() > 0);
629 }
630
631 #[test]
632 fn test_config_with_adaptive() {
633 let adaptive = AdaptiveConfig::default().with_threshold(5.0);
634 let config = Config::in_memory().with_adaptive(adaptive);
635 assert!((config.adaptive.threshold - 5.0).abs() < f64::EPSILON);
636 }
637
638 #[test]
639 fn test_config_without_adaptive() {
640 let config = Config::in_memory().without_adaptive();
641 assert!(!config.adaptive.enabled);
642 }
643
644 #[test]
645 fn test_config_without_factorized_execution() {
646 let config = Config::in_memory().without_factorized_execution();
647 assert!(!config.factorized_execution);
648 }
649
650 #[test]
651 fn test_config_builder_chaining() {
652 let config = Config::persistent("/tmp/db")
653 .with_memory_limit(512 * 1024 * 1024)
654 .with_threads(4)
655 .with_query_logging()
656 .without_backward_edges()
657 .with_spill_path("/tmp/spill");
658
659 assert!(config.path.is_some());
660 assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
661 assert_eq!(config.threads, 4);
662 assert!(config.query_logging);
663 assert!(!config.backward_edges);
664 assert!(config.spill_path.is_some());
665 }
666
667 #[test]
668 fn test_adaptive_config_default() {
669 let config = AdaptiveConfig::default();
670 assert!(config.enabled);
671 assert!((config.threshold - 3.0).abs() < f64::EPSILON);
672 assert_eq!(config.min_rows, 1000);
673 assert_eq!(config.max_reoptimizations, 3);
674 }
675
676 #[test]
677 fn test_adaptive_config_disabled() {
678 let config = AdaptiveConfig::disabled();
679 assert!(!config.enabled);
680 }
681
682 #[test]
683 fn test_adaptive_config_with_threshold() {
684 let config = AdaptiveConfig::default().with_threshold(10.0);
685 assert!((config.threshold - 10.0).abs() < f64::EPSILON);
686 }
687
688 #[test]
689 fn test_adaptive_config_with_min_rows() {
690 let config = AdaptiveConfig::default().with_min_rows(500);
691 assert_eq!(config.min_rows, 500);
692 }
693
694 #[test]
695 fn test_adaptive_config_with_max_reoptimizations() {
696 let config = AdaptiveConfig::default().with_max_reoptimizations(5);
697 assert_eq!(config.max_reoptimizations, 5);
698 }
699
700 #[test]
701 fn test_adaptive_config_builder_chaining() {
702 let config = AdaptiveConfig::default()
703 .with_threshold(2.0)
704 .with_min_rows(100)
705 .with_max_reoptimizations(10);
706 assert!((config.threshold - 2.0).abs() < f64::EPSILON);
707 assert_eq!(config.min_rows, 100);
708 assert_eq!(config.max_reoptimizations, 10);
709 }
710
711 #[test]
714 fn test_graph_model_default_is_lpg() {
715 assert_eq!(GraphModel::default(), GraphModel::Lpg);
716 }
717
718 #[test]
719 fn test_graph_model_display() {
720 assert_eq!(GraphModel::Lpg.to_string(), "LPG");
721 assert_eq!(GraphModel::Rdf.to_string(), "RDF");
722 }
723
724 #[test]
725 fn test_config_with_graph_model() {
726 let config = Config::in_memory().with_graph_model(GraphModel::Rdf);
727 assert_eq!(config.graph_model, GraphModel::Rdf);
728 }
729
730 #[test]
733 fn test_durability_mode_default_is_batch() {
734 let mode = DurabilityMode::default();
735 assert_eq!(
736 mode,
737 DurabilityMode::Batch {
738 max_delay_ms: 100,
739 max_records: 1000
740 }
741 );
742 }
743
744 #[test]
745 fn test_config_with_wal_durability() {
746 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::Sync);
747 assert_eq!(config.wal_durability, DurabilityMode::Sync);
748 }
749
750 #[test]
751 fn test_config_with_wal_durability_nosync() {
752 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::NoSync);
753 assert_eq!(config.wal_durability, DurabilityMode::NoSync);
754 }
755
756 #[test]
757 fn test_config_with_wal_durability_adaptive() {
758 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::Adaptive {
759 target_interval_ms: 50,
760 });
761 assert_eq!(
762 config.wal_durability,
763 DurabilityMode::Adaptive {
764 target_interval_ms: 50
765 }
766 );
767 }
768
769 #[test]
772 fn test_config_with_schema_constraints() {
773 let config = Config::in_memory().with_schema_constraints();
774 assert!(config.schema_constraints);
775 }
776
777 #[test]
780 fn test_config_with_query_timeout() {
781 let config = Config::in_memory().with_query_timeout(Duration::from_secs(30));
782 assert_eq!(config.query_timeout, Some(Duration::from_secs(30)));
783 }
784
785 #[test]
788 fn test_config_with_gc_interval() {
789 let config = Config::in_memory().with_gc_interval(50);
790 assert_eq!(config.gc_interval, 50);
791 }
792
793 #[test]
794 fn test_config_gc_disabled() {
795 let config = Config::in_memory().with_gc_interval(0);
796 assert_eq!(config.gc_interval, 0);
797 }
798
799 #[test]
802 fn test_validate_default_config() {
803 assert!(Config::default().validate().is_ok());
804 }
805
806 #[test]
807 fn test_validate_in_memory_config() {
808 assert!(Config::in_memory().validate().is_ok());
809 }
810
811 #[test]
812 fn test_validate_rejects_zero_memory_limit() {
813 let config = Config::in_memory().with_memory_limit(0);
814 assert_eq!(config.validate(), Err(ConfigError::ZeroMemoryLimit));
815 }
816
817 #[test]
818 fn test_validate_rejects_zero_threads() {
819 let config = Config::in_memory().with_threads(0);
820 assert_eq!(config.validate(), Err(ConfigError::ZeroThreads));
821 }
822
823 #[test]
824 fn test_validate_rejects_zero_wal_flush_interval() {
825 let mut config = Config::in_memory();
826 config.wal_flush_interval_ms = 0;
827 assert_eq!(config.validate(), Err(ConfigError::ZeroWalFlushInterval));
828 }
829
830 #[cfg(not(feature = "rdf"))]
831 #[test]
832 fn test_validate_rejects_rdf_without_feature() {
833 let config = Config::in_memory().with_graph_model(GraphModel::Rdf);
834 assert_eq!(config.validate(), Err(ConfigError::RdfFeatureRequired));
835 }
836
837 #[test]
838 fn test_config_error_display() {
839 assert_eq!(
840 ConfigError::ZeroMemoryLimit.to_string(),
841 "memory_limit must be greater than zero"
842 );
843 assert_eq!(
844 ConfigError::ZeroThreads.to_string(),
845 "threads must be greater than zero"
846 );
847 assert_eq!(
848 ConfigError::ZeroWalFlushInterval.to_string(),
849 "wal_flush_interval_ms must be greater than zero"
850 );
851 assert_eq!(
852 ConfigError::RdfFeatureRequired.to_string(),
853 "RDF graph model requires the `rdf` feature flag to be enabled"
854 );
855 }
856
857 #[test]
860 fn test_config_full_builder_chaining() {
861 let config = Config::persistent("/tmp/db")
862 .with_graph_model(GraphModel::Lpg)
863 .with_memory_limit(512 * 1024 * 1024)
864 .with_threads(4)
865 .with_query_logging()
866 .with_wal_durability(DurabilityMode::Sync)
867 .with_schema_constraints()
868 .without_backward_edges()
869 .with_spill_path("/tmp/spill")
870 .with_query_timeout(Duration::from_secs(60));
871
872 assert_eq!(config.graph_model, GraphModel::Lpg);
873 assert!(config.path.is_some());
874 assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
875 assert_eq!(config.threads, 4);
876 assert!(config.query_logging);
877 assert_eq!(config.wal_durability, DurabilityMode::Sync);
878 assert!(config.schema_constraints);
879 assert!(!config.backward_edges);
880 assert!(config.spill_path.is_some());
881 assert_eq!(config.query_timeout, Some(Duration::from_secs(60)));
882 assert!(config.validate().is_ok());
883 }
884
885 #[test]
888 fn test_access_mode_default_is_read_write() {
889 assert_eq!(AccessMode::default(), AccessMode::ReadWrite);
890 }
891
892 #[test]
893 fn test_access_mode_display() {
894 assert_eq!(AccessMode::ReadWrite.to_string(), "read-write");
895 assert_eq!(AccessMode::ReadOnly.to_string(), "read-only");
896 }
897
898 #[test]
899 fn test_config_with_access_mode() {
900 let config = Config::persistent("/tmp/db").with_access_mode(AccessMode::ReadOnly);
901 assert_eq!(config.access_mode, AccessMode::ReadOnly);
902 }
903
904 #[test]
905 fn test_config_read_only() {
906 let config = Config::read_only("/tmp/db.grafeo");
907 assert_eq!(config.access_mode, AccessMode::ReadOnly);
908 assert!(config.path.is_some());
909 assert!(!config.wal_enabled);
910 }
911
912 #[test]
913 fn test_config_default_is_read_write() {
914 let config = Config::default();
915 assert_eq!(config.access_mode, AccessMode::ReadWrite);
916 }
917}