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
232#[derive(Debug, Clone)]
237pub struct AdaptiveConfig {
238 pub enabled: bool,
240
241 pub threshold: f64,
246
247 pub min_rows: u64,
251
252 pub max_reoptimizations: usize,
254}
255
256impl Default for AdaptiveConfig {
257 fn default() -> Self {
258 Self {
259 enabled: true,
260 threshold: 3.0,
261 min_rows: 1000,
262 max_reoptimizations: 3,
263 }
264 }
265}
266
267impl AdaptiveConfig {
268 #[must_use]
270 pub fn disabled() -> Self {
271 Self {
272 enabled: false,
273 ..Default::default()
274 }
275 }
276
277 #[must_use]
279 pub fn with_threshold(mut self, threshold: f64) -> Self {
280 self.threshold = threshold;
281 self
282 }
283
284 #[must_use]
286 pub fn with_min_rows(mut self, min_rows: u64) -> Self {
287 self.min_rows = min_rows;
288 self
289 }
290
291 #[must_use]
293 pub fn with_max_reoptimizations(mut self, max: usize) -> Self {
294 self.max_reoptimizations = max;
295 self
296 }
297}
298
299impl Default for Config {
300 fn default() -> Self {
301 Self {
302 graph_model: GraphModel::default(),
303 path: None,
304 memory_limit: None,
305 spill_path: None,
306 threads: num_cpus::get(),
307 wal_enabled: true,
308 wal_flush_interval_ms: 100,
309 backward_edges: true,
310 query_logging: false,
311 adaptive: AdaptiveConfig::default(),
312 factorized_execution: true,
313 wal_durability: DurabilityMode::default(),
314 storage_format: StorageFormat::default(),
315 schema_constraints: false,
316 query_timeout: None,
317 gc_interval: 100,
318 access_mode: AccessMode::default(),
319 }
320 }
321}
322
323impl Config {
324 #[must_use]
326 pub fn in_memory() -> Self {
327 Self {
328 path: None,
329 wal_enabled: false,
330 ..Default::default()
331 }
332 }
333
334 #[must_use]
336 pub fn persistent(path: impl Into<PathBuf>) -> Self {
337 Self {
338 path: Some(path.into()),
339 wal_enabled: true,
340 ..Default::default()
341 }
342 }
343
344 #[must_use]
346 pub fn with_memory_limit(mut self, limit: usize) -> Self {
347 self.memory_limit = Some(limit);
348 self
349 }
350
351 #[must_use]
353 pub fn with_threads(mut self, threads: usize) -> Self {
354 self.threads = threads;
355 self
356 }
357
358 #[must_use]
360 pub fn without_backward_edges(mut self) -> Self {
361 self.backward_edges = false;
362 self
363 }
364
365 #[must_use]
367 pub fn with_query_logging(mut self) -> Self {
368 self.query_logging = true;
369 self
370 }
371
372 #[must_use]
374 pub fn with_memory_fraction(mut self, fraction: f64) -> Self {
375 use grafeo_common::memory::buffer::BufferManagerConfig;
376 let system_memory = BufferManagerConfig::detect_system_memory();
377 self.memory_limit = Some((system_memory as f64 * fraction) as usize);
378 self
379 }
380
381 #[must_use]
383 pub fn with_spill_path(mut self, path: impl Into<PathBuf>) -> Self {
384 self.spill_path = Some(path.into());
385 self
386 }
387
388 #[must_use]
390 pub fn with_adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
391 self.adaptive = adaptive;
392 self
393 }
394
395 #[must_use]
397 pub fn without_adaptive(mut self) -> Self {
398 self.adaptive.enabled = false;
399 self
400 }
401
402 #[must_use]
408 pub fn without_factorized_execution(mut self) -> Self {
409 self.factorized_execution = false;
410 self
411 }
412
413 #[must_use]
415 pub fn with_graph_model(mut self, model: GraphModel) -> Self {
416 self.graph_model = model;
417 self
418 }
419
420 #[must_use]
422 pub fn with_wal_durability(mut self, mode: DurabilityMode) -> Self {
423 self.wal_durability = mode;
424 self
425 }
426
427 #[must_use]
429 pub fn with_storage_format(mut self, format: StorageFormat) -> Self {
430 self.storage_format = format;
431 self
432 }
433
434 #[must_use]
436 pub fn with_schema_constraints(mut self) -> Self {
437 self.schema_constraints = true;
438 self
439 }
440
441 #[must_use]
443 pub fn with_query_timeout(mut self, timeout: Duration) -> Self {
444 self.query_timeout = Some(timeout);
445 self
446 }
447
448 #[must_use]
452 pub fn with_gc_interval(mut self, interval: usize) -> Self {
453 self.gc_interval = interval;
454 self
455 }
456
457 #[must_use]
459 pub fn with_access_mode(mut self, mode: AccessMode) -> Self {
460 self.access_mode = mode;
461 self
462 }
463
464 #[must_use]
469 pub fn read_only(path: impl Into<PathBuf>) -> Self {
470 Self {
471 path: Some(path.into()),
472 wal_enabled: false,
473 access_mode: AccessMode::ReadOnly,
474 ..Default::default()
475 }
476 }
477
478 pub fn validate(&self) -> std::result::Result<(), ConfigError> {
486 if let Some(limit) = self.memory_limit
487 && limit == 0
488 {
489 return Err(ConfigError::ZeroMemoryLimit);
490 }
491
492 if self.threads == 0 {
493 return Err(ConfigError::ZeroThreads);
494 }
495
496 if self.wal_flush_interval_ms == 0 {
497 return Err(ConfigError::ZeroWalFlushInterval);
498 }
499
500 #[cfg(not(feature = "rdf"))]
501 if self.graph_model == GraphModel::Rdf {
502 return Err(ConfigError::RdfFeatureRequired);
503 }
504
505 Ok(())
506 }
507}
508
509mod num_cpus {
511 #[cfg(not(target_arch = "wasm32"))]
512 pub fn get() -> usize {
513 std::thread::available_parallelism()
514 .map(|n| n.get())
515 .unwrap_or(4)
516 }
517
518 #[cfg(target_arch = "wasm32")]
519 pub fn get() -> usize {
520 1
521 }
522}
523
524#[cfg(test)]
525mod tests {
526 use super::*;
527
528 #[test]
529 fn test_config_default() {
530 let config = Config::default();
531 assert_eq!(config.graph_model, GraphModel::Lpg);
532 assert!(config.path.is_none());
533 assert!(config.memory_limit.is_none());
534 assert!(config.spill_path.is_none());
535 assert!(config.threads > 0);
536 assert!(config.wal_enabled);
537 assert_eq!(config.wal_flush_interval_ms, 100);
538 assert!(config.backward_edges);
539 assert!(!config.query_logging);
540 assert!(config.factorized_execution);
541 assert_eq!(config.wal_durability, DurabilityMode::default());
542 assert!(!config.schema_constraints);
543 assert!(config.query_timeout.is_none());
544 assert_eq!(config.gc_interval, 100);
545 }
546
547 #[test]
548 fn test_config_in_memory() {
549 let config = Config::in_memory();
550 assert!(config.path.is_none());
551 assert!(!config.wal_enabled);
552 assert!(config.backward_edges);
553 }
554
555 #[test]
556 fn test_config_persistent() {
557 let config = Config::persistent("/tmp/test_db");
558 assert_eq!(
559 config.path.as_deref(),
560 Some(std::path::Path::new("/tmp/test_db"))
561 );
562 assert!(config.wal_enabled);
563 }
564
565 #[test]
566 fn test_config_with_memory_limit() {
567 let config = Config::in_memory().with_memory_limit(1024 * 1024);
568 assert_eq!(config.memory_limit, Some(1024 * 1024));
569 }
570
571 #[test]
572 fn test_config_with_threads() {
573 let config = Config::in_memory().with_threads(8);
574 assert_eq!(config.threads, 8);
575 }
576
577 #[test]
578 fn test_config_without_backward_edges() {
579 let config = Config::in_memory().without_backward_edges();
580 assert!(!config.backward_edges);
581 }
582
583 #[test]
584 fn test_config_with_query_logging() {
585 let config = Config::in_memory().with_query_logging();
586 assert!(config.query_logging);
587 }
588
589 #[test]
590 fn test_config_with_spill_path() {
591 let config = Config::in_memory().with_spill_path("/tmp/spill");
592 assert_eq!(
593 config.spill_path.as_deref(),
594 Some(std::path::Path::new("/tmp/spill"))
595 );
596 }
597
598 #[test]
599 fn test_config_with_memory_fraction() {
600 let config = Config::in_memory().with_memory_fraction(0.5);
601 assert!(config.memory_limit.is_some());
602 assert!(config.memory_limit.unwrap() > 0);
603 }
604
605 #[test]
606 fn test_config_with_adaptive() {
607 let adaptive = AdaptiveConfig::default().with_threshold(5.0);
608 let config = Config::in_memory().with_adaptive(adaptive);
609 assert!((config.adaptive.threshold - 5.0).abs() < f64::EPSILON);
610 }
611
612 #[test]
613 fn test_config_without_adaptive() {
614 let config = Config::in_memory().without_adaptive();
615 assert!(!config.adaptive.enabled);
616 }
617
618 #[test]
619 fn test_config_without_factorized_execution() {
620 let config = Config::in_memory().without_factorized_execution();
621 assert!(!config.factorized_execution);
622 }
623
624 #[test]
625 fn test_config_builder_chaining() {
626 let config = Config::persistent("/tmp/db")
627 .with_memory_limit(512 * 1024 * 1024)
628 .with_threads(4)
629 .with_query_logging()
630 .without_backward_edges()
631 .with_spill_path("/tmp/spill");
632
633 assert!(config.path.is_some());
634 assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
635 assert_eq!(config.threads, 4);
636 assert!(config.query_logging);
637 assert!(!config.backward_edges);
638 assert!(config.spill_path.is_some());
639 }
640
641 #[test]
642 fn test_adaptive_config_default() {
643 let config = AdaptiveConfig::default();
644 assert!(config.enabled);
645 assert!((config.threshold - 3.0).abs() < f64::EPSILON);
646 assert_eq!(config.min_rows, 1000);
647 assert_eq!(config.max_reoptimizations, 3);
648 }
649
650 #[test]
651 fn test_adaptive_config_disabled() {
652 let config = AdaptiveConfig::disabled();
653 assert!(!config.enabled);
654 }
655
656 #[test]
657 fn test_adaptive_config_with_threshold() {
658 let config = AdaptiveConfig::default().with_threshold(10.0);
659 assert!((config.threshold - 10.0).abs() < f64::EPSILON);
660 }
661
662 #[test]
663 fn test_adaptive_config_with_min_rows() {
664 let config = AdaptiveConfig::default().with_min_rows(500);
665 assert_eq!(config.min_rows, 500);
666 }
667
668 #[test]
669 fn test_adaptive_config_with_max_reoptimizations() {
670 let config = AdaptiveConfig::default().with_max_reoptimizations(5);
671 assert_eq!(config.max_reoptimizations, 5);
672 }
673
674 #[test]
675 fn test_adaptive_config_builder_chaining() {
676 let config = AdaptiveConfig::default()
677 .with_threshold(2.0)
678 .with_min_rows(100)
679 .with_max_reoptimizations(10);
680 assert!((config.threshold - 2.0).abs() < f64::EPSILON);
681 assert_eq!(config.min_rows, 100);
682 assert_eq!(config.max_reoptimizations, 10);
683 }
684
685 #[test]
688 fn test_graph_model_default_is_lpg() {
689 assert_eq!(GraphModel::default(), GraphModel::Lpg);
690 }
691
692 #[test]
693 fn test_graph_model_display() {
694 assert_eq!(GraphModel::Lpg.to_string(), "LPG");
695 assert_eq!(GraphModel::Rdf.to_string(), "RDF");
696 }
697
698 #[test]
699 fn test_config_with_graph_model() {
700 let config = Config::in_memory().with_graph_model(GraphModel::Rdf);
701 assert_eq!(config.graph_model, GraphModel::Rdf);
702 }
703
704 #[test]
707 fn test_durability_mode_default_is_batch() {
708 let mode = DurabilityMode::default();
709 assert_eq!(
710 mode,
711 DurabilityMode::Batch {
712 max_delay_ms: 100,
713 max_records: 1000
714 }
715 );
716 }
717
718 #[test]
719 fn test_config_with_wal_durability() {
720 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::Sync);
721 assert_eq!(config.wal_durability, DurabilityMode::Sync);
722 }
723
724 #[test]
725 fn test_config_with_wal_durability_nosync() {
726 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::NoSync);
727 assert_eq!(config.wal_durability, DurabilityMode::NoSync);
728 }
729
730 #[test]
731 fn test_config_with_wal_durability_adaptive() {
732 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::Adaptive {
733 target_interval_ms: 50,
734 });
735 assert_eq!(
736 config.wal_durability,
737 DurabilityMode::Adaptive {
738 target_interval_ms: 50
739 }
740 );
741 }
742
743 #[test]
746 fn test_config_with_schema_constraints() {
747 let config = Config::in_memory().with_schema_constraints();
748 assert!(config.schema_constraints);
749 }
750
751 #[test]
754 fn test_config_with_query_timeout() {
755 let config = Config::in_memory().with_query_timeout(Duration::from_secs(30));
756 assert_eq!(config.query_timeout, Some(Duration::from_secs(30)));
757 }
758
759 #[test]
762 fn test_config_with_gc_interval() {
763 let config = Config::in_memory().with_gc_interval(50);
764 assert_eq!(config.gc_interval, 50);
765 }
766
767 #[test]
768 fn test_config_gc_disabled() {
769 let config = Config::in_memory().with_gc_interval(0);
770 assert_eq!(config.gc_interval, 0);
771 }
772
773 #[test]
776 fn test_validate_default_config() {
777 assert!(Config::default().validate().is_ok());
778 }
779
780 #[test]
781 fn test_validate_in_memory_config() {
782 assert!(Config::in_memory().validate().is_ok());
783 }
784
785 #[test]
786 fn test_validate_rejects_zero_memory_limit() {
787 let config = Config::in_memory().with_memory_limit(0);
788 assert_eq!(config.validate(), Err(ConfigError::ZeroMemoryLimit));
789 }
790
791 #[test]
792 fn test_validate_rejects_zero_threads() {
793 let config = Config::in_memory().with_threads(0);
794 assert_eq!(config.validate(), Err(ConfigError::ZeroThreads));
795 }
796
797 #[test]
798 fn test_validate_rejects_zero_wal_flush_interval() {
799 let mut config = Config::in_memory();
800 config.wal_flush_interval_ms = 0;
801 assert_eq!(config.validate(), Err(ConfigError::ZeroWalFlushInterval));
802 }
803
804 #[cfg(not(feature = "rdf"))]
805 #[test]
806 fn test_validate_rejects_rdf_without_feature() {
807 let config = Config::in_memory().with_graph_model(GraphModel::Rdf);
808 assert_eq!(config.validate(), Err(ConfigError::RdfFeatureRequired));
809 }
810
811 #[test]
812 fn test_config_error_display() {
813 assert_eq!(
814 ConfigError::ZeroMemoryLimit.to_string(),
815 "memory_limit must be greater than zero"
816 );
817 assert_eq!(
818 ConfigError::ZeroThreads.to_string(),
819 "threads must be greater than zero"
820 );
821 assert_eq!(
822 ConfigError::ZeroWalFlushInterval.to_string(),
823 "wal_flush_interval_ms must be greater than zero"
824 );
825 assert_eq!(
826 ConfigError::RdfFeatureRequired.to_string(),
827 "RDF graph model requires the `rdf` feature flag to be enabled"
828 );
829 }
830
831 #[test]
834 fn test_config_full_builder_chaining() {
835 let config = Config::persistent("/tmp/db")
836 .with_graph_model(GraphModel::Lpg)
837 .with_memory_limit(512 * 1024 * 1024)
838 .with_threads(4)
839 .with_query_logging()
840 .with_wal_durability(DurabilityMode::Sync)
841 .with_schema_constraints()
842 .without_backward_edges()
843 .with_spill_path("/tmp/spill")
844 .with_query_timeout(Duration::from_secs(60));
845
846 assert_eq!(config.graph_model, GraphModel::Lpg);
847 assert!(config.path.is_some());
848 assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
849 assert_eq!(config.threads, 4);
850 assert!(config.query_logging);
851 assert_eq!(config.wal_durability, DurabilityMode::Sync);
852 assert!(config.schema_constraints);
853 assert!(!config.backward_edges);
854 assert!(config.spill_path.is_some());
855 assert_eq!(config.query_timeout, Some(Duration::from_secs(60)));
856 assert!(config.validate().is_ok());
857 }
858
859 #[test]
862 fn test_access_mode_default_is_read_write() {
863 assert_eq!(AccessMode::default(), AccessMode::ReadWrite);
864 }
865
866 #[test]
867 fn test_access_mode_display() {
868 assert_eq!(AccessMode::ReadWrite.to_string(), "read-write");
869 assert_eq!(AccessMode::ReadOnly.to_string(), "read-only");
870 }
871
872 #[test]
873 fn test_config_with_access_mode() {
874 let config = Config::persistent("/tmp/db").with_access_mode(AccessMode::ReadOnly);
875 assert_eq!(config.access_mode, AccessMode::ReadOnly);
876 }
877
878 #[test]
879 fn test_config_read_only() {
880 let config = Config::read_only("/tmp/db.grafeo");
881 assert_eq!(config.access_mode, AccessMode::ReadOnly);
882 assert!(config.path.is_some());
883 assert!(!config.wal_enabled);
884 }
885
886 #[test]
887 fn test_config_default_is_read_write() {
888 let config = Config::default();
889 assert_eq!(config.access_mode, AccessMode::ReadWrite);
890 }
891}