1use std::fmt;
4use std::path::PathBuf;
5use std::time::Duration;
6
7#[cfg(feature = "encryption")]
16#[derive(Clone)]
17pub struct EncryptionConfig {
18 pub key_chain: std::sync::Arc<grafeo_common::encryption::KeyChain>,
21}
22
23#[cfg(feature = "encryption")]
24impl fmt::Debug for EncryptionConfig {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 f.debug_struct("EncryptionConfig")
27 .field("key_chain", &"[redacted]")
28 .finish()
29 }
30}
31
32#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
40#[non_exhaustive]
41pub enum GraphModel {
42 #[default]
44 Lpg,
45 Rdf,
47}
48
49impl fmt::Display for GraphModel {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 Self::Lpg => write!(f, "LPG"),
53 Self::Rdf => write!(f, "RDF"),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
65#[non_exhaustive]
66pub enum AccessMode {
67 #[default]
69 ReadWrite,
70 ReadOnly,
74}
75
76impl fmt::Display for AccessMode {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::ReadWrite => write!(f, "read-write"),
80 Self::ReadOnly => write!(f, "read-only"),
81 }
82 }
83}
84
85#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
91#[non_exhaustive]
92pub enum StorageFormat {
93 #[default]
96 Auto,
97 WalDirectory,
99 SingleFile,
102}
103
104impl fmt::Display for StorageFormat {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 Self::Auto => write!(f, "auto"),
108 Self::WalDirectory => write!(f, "wal-directory"),
109 Self::SingleFile => write!(f, "single-file"),
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120#[non_exhaustive]
121pub enum DurabilityMode {
122 Sync,
124 Batch {
126 max_delay_ms: u64,
128 max_records: u64,
130 },
131 Adaptive {
133 target_interval_ms: u64,
135 },
136 NoSync,
138}
139
140impl Default for DurabilityMode {
141 fn default() -> Self {
142 Self::Batch {
143 max_delay_ms: 100,
144 max_records: 1000,
145 }
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq)]
151#[non_exhaustive]
152pub enum ConfigError {
153 ZeroMemoryLimit,
155 ZeroThreads,
157 ZeroWalFlushInterval,
159 RdfFeatureRequired,
161}
162
163impl fmt::Display for ConfigError {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 match self {
166 Self::ZeroMemoryLimit => write!(f, "memory_limit must be greater than zero"),
167 Self::ZeroThreads => write!(f, "threads must be greater than zero"),
168 Self::ZeroWalFlushInterval => {
169 write!(f, "wal_flush_interval_ms must be greater than zero")
170 }
171 Self::RdfFeatureRequired => {
172 write!(
173 f,
174 "RDF graph model requires the `rdf` feature flag to be enabled"
175 )
176 }
177 }
178 }
179}
180
181impl std::error::Error for ConfigError {}
182
183#[derive(Debug, Clone)]
185#[allow(clippy::struct_excessive_bools)] pub struct Config {
187 pub graph_model: GraphModel,
189 pub path: Option<PathBuf>,
191
192 pub memory_limit: Option<usize>,
194
195 pub spill_path: Option<PathBuf>,
197
198 pub threads: usize,
200
201 pub wal_enabled: bool,
203
204 pub wal_flush_interval_ms: u64,
206
207 pub backward_edges: bool,
209
210 pub query_logging: bool,
212
213 pub adaptive: AdaptiveConfig,
215
216 pub factorized_execution: bool,
224
225 pub wal_durability: DurabilityMode,
227
228 pub storage_format: StorageFormat,
233
234 pub schema_constraints: bool,
240
241 pub query_timeout: Option<Duration>,
250
251 pub max_property_size: Option<usize>,
260
261 pub gc_interval: usize,
266
267 pub access_mode: AccessMode,
273
274 pub cdc_enabled: bool,
285
286 #[cfg(feature = "cdc")]
293 pub cdc_retention: crate::cdc::CdcRetentionConfig,
294
295 pub section_configs: hashbrown::HashMap<
301 grafeo_common::storage::SectionType,
302 grafeo_common::storage::SectionMemoryConfig,
303 >,
304
305 pub checkpoint_interval: Option<Duration>,
311
312 #[cfg(feature = "encryption")]
320 pub encryption: Option<EncryptionConfig>,
321}
322
323#[derive(Debug, Clone)]
328pub struct AdaptiveConfig {
329 pub enabled: bool,
331
332 pub threshold: f64,
337
338 pub min_rows: u64,
342
343 pub max_reoptimizations: usize,
345}
346
347impl Default for AdaptiveConfig {
348 fn default() -> Self {
349 Self {
350 enabled: true,
351 threshold: 3.0,
352 min_rows: 1000,
353 max_reoptimizations: 3,
354 }
355 }
356}
357
358impl AdaptiveConfig {
359 #[must_use]
361 pub fn disabled() -> Self {
362 Self {
363 enabled: false,
364 ..Default::default()
365 }
366 }
367
368 #[must_use]
370 pub fn with_threshold(mut self, threshold: f64) -> Self {
371 self.threshold = threshold;
372 self
373 }
374
375 #[must_use]
377 pub fn with_min_rows(mut self, min_rows: u64) -> Self {
378 self.min_rows = min_rows;
379 self
380 }
381
382 #[must_use]
384 pub fn with_max_reoptimizations(mut self, max: usize) -> Self {
385 self.max_reoptimizations = max;
386 self
387 }
388}
389
390impl Default for Config {
391 fn default() -> Self {
392 Self {
393 graph_model: GraphModel::default(),
394 path: None,
395 memory_limit: None,
396 spill_path: None,
397 threads: num_cpus::get(),
398 wal_enabled: true,
399 wal_flush_interval_ms: 100,
400 backward_edges: true,
401 query_logging: false,
402 adaptive: AdaptiveConfig::default(),
403 factorized_execution: true,
404 wal_durability: DurabilityMode::default(),
405 storage_format: StorageFormat::default(),
406 schema_constraints: false,
407 query_timeout: Some(Duration::from_secs(30)),
408 max_property_size: Some(16 * 1024 * 1024), gc_interval: 100,
410 access_mode: AccessMode::default(),
411 cdc_enabled: false,
412 #[cfg(feature = "cdc")]
413 cdc_retention: crate::cdc::CdcRetentionConfig::default(),
414 section_configs: hashbrown::HashMap::new(),
415 checkpoint_interval: None,
416 #[cfg(feature = "encryption")]
417 encryption: None,
418 }
419 }
420}
421
422impl Config {
423 #[must_use]
425 pub fn in_memory() -> Self {
426 Self {
427 path: None,
428 wal_enabled: false,
429 ..Default::default()
430 }
431 }
432
433 #[must_use]
435 pub fn persistent(path: impl Into<PathBuf>) -> Self {
436 Self {
437 path: Some(path.into()),
438 wal_enabled: true,
439 ..Default::default()
440 }
441 }
442
443 #[must_use]
445 pub fn with_memory_limit(mut self, limit: usize) -> Self {
446 self.memory_limit = Some(limit);
447 self
448 }
449
450 #[must_use]
452 pub fn with_threads(mut self, threads: usize) -> Self {
453 self.threads = threads;
454 self
455 }
456
457 #[must_use]
459 pub fn without_backward_edges(mut self) -> Self {
460 self.backward_edges = false;
461 self
462 }
463
464 #[must_use]
466 pub fn with_query_logging(mut self) -> Self {
467 self.query_logging = true;
468 self
469 }
470
471 #[must_use]
473 pub fn with_memory_fraction(mut self, fraction: f64) -> Self {
474 use grafeo_common::memory::buffer::BufferManagerConfig;
475 let system_memory = BufferManagerConfig::detect_system_memory();
476 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
478 let budget = (system_memory as f64 * fraction) as usize;
479 self.memory_limit = Some(budget);
480 self
481 }
482
483 #[must_use]
485 pub fn with_spill_path(mut self, path: impl Into<PathBuf>) -> Self {
486 self.spill_path = Some(path.into());
487 self
488 }
489
490 #[must_use]
492 pub fn with_adaptive(mut self, adaptive: AdaptiveConfig) -> Self {
493 self.adaptive = adaptive;
494 self
495 }
496
497 #[must_use]
499 pub fn without_adaptive(mut self) -> Self {
500 self.adaptive.enabled = false;
501 self
502 }
503
504 #[must_use]
510 pub fn without_factorized_execution(mut self) -> Self {
511 self.factorized_execution = false;
512 self
513 }
514
515 #[must_use]
517 pub fn with_graph_model(mut self, model: GraphModel) -> Self {
518 self.graph_model = model;
519 self
520 }
521
522 #[must_use]
524 pub fn with_wal_durability(mut self, mode: DurabilityMode) -> Self {
525 self.wal_durability = mode;
526 self
527 }
528
529 #[must_use]
531 pub fn with_storage_format(mut self, format: StorageFormat) -> Self {
532 self.storage_format = format;
533 self
534 }
535
536 #[must_use]
538 pub fn with_schema_constraints(mut self) -> Self {
539 self.schema_constraints = true;
540 self
541 }
542
543 #[must_use]
545 pub fn with_query_timeout(mut self, timeout: Duration) -> Self {
546 self.query_timeout = Some(timeout);
547 self
548 }
549
550 #[must_use]
552 pub fn without_query_timeout(mut self) -> Self {
553 self.query_timeout = None;
554 self
555 }
556
557 #[must_use]
559 pub fn with_max_property_size(mut self, size: usize) -> Self {
560 self.max_property_size = Some(size);
561 self
562 }
563
564 #[must_use]
566 pub fn without_max_property_size(mut self) -> Self {
567 self.max_property_size = None;
568 self
569 }
570
571 #[must_use]
575 pub fn with_gc_interval(mut self, interval: usize) -> Self {
576 self.gc_interval = interval;
577 self
578 }
579
580 #[must_use]
582 pub fn with_access_mode(mut self, mode: AccessMode) -> Self {
583 self.access_mode = mode;
584 self
585 }
586
587 #[must_use]
592 pub fn read_only(path: impl Into<PathBuf>) -> Self {
593 Self {
594 path: Some(path.into()),
595 wal_enabled: false,
596 access_mode: AccessMode::ReadOnly,
597 ..Default::default()
598 }
599 }
600
601 #[must_use]
609 pub fn with_cdc(mut self) -> Self {
610 self.cdc_enabled = true;
611 self
612 }
613
614 #[must_use]
632 pub fn with_section_config(
633 mut self,
634 section_type: grafeo_common::storage::SectionType,
635 config: grafeo_common::storage::SectionMemoryConfig,
636 ) -> Self {
637 self.section_configs.insert(section_type, config);
638 self
639 }
640
641 #[must_use]
646 pub fn with_checkpoint_interval(mut self, interval: Duration) -> Self {
647 self.checkpoint_interval = Some(interval);
648 self
649 }
650
651 pub fn validate(&self) -> std::result::Result<(), ConfigError> {
659 if let Some(limit) = self.memory_limit
660 && limit == 0
661 {
662 return Err(ConfigError::ZeroMemoryLimit);
663 }
664
665 if self.threads == 0 {
666 return Err(ConfigError::ZeroThreads);
667 }
668
669 if self.wal_flush_interval_ms == 0 {
670 return Err(ConfigError::ZeroWalFlushInterval);
671 }
672
673 #[cfg(not(feature = "triple-store"))]
674 if self.graph_model == GraphModel::Rdf {
675 return Err(ConfigError::RdfFeatureRequired);
676 }
677
678 Ok(())
679 }
680}
681
682mod num_cpus {
684 #[cfg(not(target_arch = "wasm32"))]
685 pub fn get() -> usize {
686 std::thread::available_parallelism()
687 .map(|n| n.get())
688 .unwrap_or(4)
689 }
690
691 #[cfg(target_arch = "wasm32")]
692 pub fn get() -> usize {
693 1
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700
701 #[test]
702 fn test_config_default() {
703 let config = Config::default();
704 assert_eq!(config.graph_model, GraphModel::Lpg);
705 assert!(config.path.is_none());
706 assert!(config.memory_limit.is_none());
707 assert!(config.spill_path.is_none());
708 assert!(config.threads > 0);
709 assert!(config.wal_enabled);
710 assert_eq!(config.wal_flush_interval_ms, 100);
711 assert!(config.backward_edges);
712 assert!(!config.query_logging);
713 assert!(config.factorized_execution);
714 assert_eq!(config.wal_durability, DurabilityMode::default());
715 assert!(!config.schema_constraints);
716 assert_eq!(config.query_timeout, Some(Duration::from_secs(30)));
717 assert_eq!(config.gc_interval, 100);
718 }
719
720 #[test]
721 fn test_config_in_memory() {
722 let config = Config::in_memory();
723 assert!(config.path.is_none());
724 assert!(!config.wal_enabled);
725 assert!(config.backward_edges);
726 }
727
728 #[test]
729 fn test_config_persistent() {
730 let config = Config::persistent("/tmp/test_db");
731 assert_eq!(
732 config.path.as_deref(),
733 Some(std::path::Path::new("/tmp/test_db"))
734 );
735 assert!(config.wal_enabled);
736 }
737
738 #[test]
739 fn test_config_with_memory_limit() {
740 let config = Config::in_memory().with_memory_limit(1024 * 1024);
741 assert_eq!(config.memory_limit, Some(1024 * 1024));
742 }
743
744 #[test]
745 fn test_config_with_threads() {
746 let config = Config::in_memory().with_threads(8);
747 assert_eq!(config.threads, 8);
748 }
749
750 #[test]
751 fn test_config_without_backward_edges() {
752 let config = Config::in_memory().without_backward_edges();
753 assert!(!config.backward_edges);
754 }
755
756 #[test]
757 fn test_config_with_query_logging() {
758 let config = Config::in_memory().with_query_logging();
759 assert!(config.query_logging);
760 }
761
762 #[test]
763 fn test_config_with_spill_path() {
764 let config = Config::in_memory().with_spill_path("/tmp/spill");
765 assert_eq!(
766 config.spill_path.as_deref(),
767 Some(std::path::Path::new("/tmp/spill"))
768 );
769 }
770
771 #[test]
772 fn test_config_with_memory_fraction() {
773 let config = Config::in_memory().with_memory_fraction(0.5);
774 assert!(config.memory_limit.is_some());
775 assert!(config.memory_limit.unwrap() > 0);
776 }
777
778 #[test]
779 fn test_config_with_adaptive() {
780 let adaptive = AdaptiveConfig::default().with_threshold(5.0);
781 let config = Config::in_memory().with_adaptive(adaptive);
782 assert!((config.adaptive.threshold - 5.0).abs() < f64::EPSILON);
783 }
784
785 #[test]
786 fn test_config_without_adaptive() {
787 let config = Config::in_memory().without_adaptive();
788 assert!(!config.adaptive.enabled);
789 }
790
791 #[test]
792 fn test_config_without_factorized_execution() {
793 let config = Config::in_memory().without_factorized_execution();
794 assert!(!config.factorized_execution);
795 }
796
797 #[test]
798 fn test_config_builder_chaining() {
799 let config = Config::persistent("/tmp/db")
800 .with_memory_limit(512 * 1024 * 1024)
801 .with_threads(4)
802 .with_query_logging()
803 .without_backward_edges()
804 .with_spill_path("/tmp/spill");
805
806 assert!(config.path.is_some());
807 assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
808 assert_eq!(config.threads, 4);
809 assert!(config.query_logging);
810 assert!(!config.backward_edges);
811 assert!(config.spill_path.is_some());
812 }
813
814 #[test]
815 fn test_adaptive_config_default() {
816 let config = AdaptiveConfig::default();
817 assert!(config.enabled);
818 assert!((config.threshold - 3.0).abs() < f64::EPSILON);
819 assert_eq!(config.min_rows, 1000);
820 assert_eq!(config.max_reoptimizations, 3);
821 }
822
823 #[test]
824 fn test_adaptive_config_disabled() {
825 let config = AdaptiveConfig::disabled();
826 assert!(!config.enabled);
827 }
828
829 #[test]
830 fn test_adaptive_config_with_threshold() {
831 let config = AdaptiveConfig::default().with_threshold(10.0);
832 assert!((config.threshold - 10.0).abs() < f64::EPSILON);
833 }
834
835 #[test]
836 fn test_adaptive_config_with_min_rows() {
837 let config = AdaptiveConfig::default().with_min_rows(500);
838 assert_eq!(config.min_rows, 500);
839 }
840
841 #[test]
842 fn test_adaptive_config_with_max_reoptimizations() {
843 let config = AdaptiveConfig::default().with_max_reoptimizations(5);
844 assert_eq!(config.max_reoptimizations, 5);
845 }
846
847 #[test]
848 fn test_adaptive_config_builder_chaining() {
849 let config = AdaptiveConfig::default()
850 .with_threshold(2.0)
851 .with_min_rows(100)
852 .with_max_reoptimizations(10);
853 assert!((config.threshold - 2.0).abs() < f64::EPSILON);
854 assert_eq!(config.min_rows, 100);
855 assert_eq!(config.max_reoptimizations, 10);
856 }
857
858 #[test]
861 fn test_graph_model_default_is_lpg() {
862 assert_eq!(GraphModel::default(), GraphModel::Lpg);
863 }
864
865 #[test]
866 fn test_graph_model_display() {
867 assert_eq!(GraphModel::Lpg.to_string(), "LPG");
868 assert_eq!(GraphModel::Rdf.to_string(), "RDF");
869 }
870
871 #[test]
872 fn test_config_with_graph_model() {
873 let config = Config::in_memory().with_graph_model(GraphModel::Rdf);
874 assert_eq!(config.graph_model, GraphModel::Rdf);
875 }
876
877 #[test]
880 fn test_durability_mode_default_is_batch() {
881 let mode = DurabilityMode::default();
882 assert_eq!(
883 mode,
884 DurabilityMode::Batch {
885 max_delay_ms: 100,
886 max_records: 1000
887 }
888 );
889 }
890
891 #[test]
892 fn test_config_with_wal_durability() {
893 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::Sync);
894 assert_eq!(config.wal_durability, DurabilityMode::Sync);
895 }
896
897 #[test]
898 fn test_config_with_wal_durability_nosync() {
899 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::NoSync);
900 assert_eq!(config.wal_durability, DurabilityMode::NoSync);
901 }
902
903 #[test]
904 fn test_config_with_wal_durability_adaptive() {
905 let config = Config::persistent("/tmp/db").with_wal_durability(DurabilityMode::Adaptive {
906 target_interval_ms: 50,
907 });
908 assert_eq!(
909 config.wal_durability,
910 DurabilityMode::Adaptive {
911 target_interval_ms: 50
912 }
913 );
914 }
915
916 #[test]
919 fn test_config_default_max_property_size() {
920 let config = Config::in_memory();
921 assert_eq!(config.max_property_size, Some(16 * 1024 * 1024));
922 }
923
924 #[test]
925 fn test_config_with_max_property_size() {
926 let config = Config::in_memory().with_max_property_size(1024);
927 assert_eq!(config.max_property_size, Some(1024));
928 }
929
930 #[test]
931 fn test_config_without_max_property_size() {
932 let config = Config::in_memory().without_max_property_size();
933 assert!(config.max_property_size.is_none());
934 }
935
936 #[test]
939 fn test_config_with_schema_constraints() {
940 let config = Config::in_memory().with_schema_constraints();
941 assert!(config.schema_constraints);
942 }
943
944 #[test]
947 fn test_config_with_query_timeout() {
948 let config = Config::in_memory().with_query_timeout(Duration::from_secs(60));
949 assert_eq!(config.query_timeout, Some(Duration::from_secs(60)));
950 }
951
952 #[test]
953 fn test_config_without_query_timeout() {
954 let config = Config::in_memory().without_query_timeout();
955 assert!(config.query_timeout.is_none());
956 }
957
958 #[test]
959 fn test_config_default_query_timeout() {
960 let config = Config::in_memory();
961 assert_eq!(config.query_timeout, Some(Duration::from_secs(30)));
962 }
963
964 #[test]
967 fn test_config_with_gc_interval() {
968 let config = Config::in_memory().with_gc_interval(50);
969 assert_eq!(config.gc_interval, 50);
970 }
971
972 #[test]
973 fn test_config_gc_disabled() {
974 let config = Config::in_memory().with_gc_interval(0);
975 assert_eq!(config.gc_interval, 0);
976 }
977
978 #[test]
981 fn test_validate_default_config() {
982 assert!(Config::default().validate().is_ok());
983 }
984
985 #[test]
986 fn test_validate_in_memory_config() {
987 assert!(Config::in_memory().validate().is_ok());
988 }
989
990 #[test]
991 fn test_validate_rejects_zero_memory_limit() {
992 let config = Config::in_memory().with_memory_limit(0);
993 assert_eq!(config.validate(), Err(ConfigError::ZeroMemoryLimit));
994 }
995
996 #[test]
997 fn test_validate_rejects_zero_threads() {
998 let config = Config::in_memory().with_threads(0);
999 assert_eq!(config.validate(), Err(ConfigError::ZeroThreads));
1000 }
1001
1002 #[test]
1003 fn test_validate_rejects_zero_wal_flush_interval() {
1004 let mut config = Config::in_memory();
1005 config.wal_flush_interval_ms = 0;
1006 assert_eq!(config.validate(), Err(ConfigError::ZeroWalFlushInterval));
1007 }
1008
1009 #[cfg(not(feature = "triple-store"))]
1010 #[test]
1011 fn test_validate_rejects_rdf_without_feature() {
1012 let config = Config::in_memory().with_graph_model(GraphModel::Rdf);
1013 assert_eq!(config.validate(), Err(ConfigError::RdfFeatureRequired));
1014 }
1015
1016 #[test]
1017 fn test_config_error_display() {
1018 assert_eq!(
1019 ConfigError::ZeroMemoryLimit.to_string(),
1020 "memory_limit must be greater than zero"
1021 );
1022 assert_eq!(
1023 ConfigError::ZeroThreads.to_string(),
1024 "threads must be greater than zero"
1025 );
1026 assert_eq!(
1027 ConfigError::ZeroWalFlushInterval.to_string(),
1028 "wal_flush_interval_ms must be greater than zero"
1029 );
1030 assert_eq!(
1031 ConfigError::RdfFeatureRequired.to_string(),
1032 "RDF graph model requires the `rdf` feature flag to be enabled"
1033 );
1034 }
1035
1036 #[test]
1039 fn test_config_full_builder_chaining() {
1040 let config = Config::persistent("/tmp/db")
1041 .with_graph_model(GraphModel::Lpg)
1042 .with_memory_limit(512 * 1024 * 1024)
1043 .with_threads(4)
1044 .with_query_logging()
1045 .with_wal_durability(DurabilityMode::Sync)
1046 .with_schema_constraints()
1047 .without_backward_edges()
1048 .with_spill_path("/tmp/spill")
1049 .with_query_timeout(Duration::from_secs(60));
1050
1051 assert_eq!(config.graph_model, GraphModel::Lpg);
1052 assert!(config.path.is_some());
1053 assert_eq!(config.memory_limit, Some(512 * 1024 * 1024));
1054 assert_eq!(config.threads, 4);
1055 assert!(config.query_logging);
1056 assert_eq!(config.wal_durability, DurabilityMode::Sync);
1057 assert!(config.schema_constraints);
1058 assert!(!config.backward_edges);
1059 assert!(config.spill_path.is_some());
1060 assert_eq!(config.query_timeout, Some(Duration::from_secs(60)));
1061 assert!(config.validate().is_ok());
1062 }
1063
1064 #[test]
1067 fn test_access_mode_default_is_read_write() {
1068 assert_eq!(AccessMode::default(), AccessMode::ReadWrite);
1069 }
1070
1071 #[test]
1072 fn test_access_mode_display() {
1073 assert_eq!(AccessMode::ReadWrite.to_string(), "read-write");
1074 assert_eq!(AccessMode::ReadOnly.to_string(), "read-only");
1075 }
1076
1077 #[test]
1078 fn test_config_with_access_mode() {
1079 let config = Config::persistent("/tmp/db").with_access_mode(AccessMode::ReadOnly);
1080 assert_eq!(config.access_mode, AccessMode::ReadOnly);
1081 }
1082
1083 #[test]
1084 fn test_config_read_only() {
1085 let config = Config::read_only("/tmp/db.grafeo");
1086 assert_eq!(config.access_mode, AccessMode::ReadOnly);
1087 assert!(config.path.is_some());
1088 assert!(!config.wal_enabled);
1089 }
1090
1091 #[test]
1092 fn test_config_default_is_read_write() {
1093 let config = Config::default();
1094 assert_eq!(config.access_mode, AccessMode::ReadWrite);
1095 }
1096
1097 #[test]
1100 fn test_storage_format_default_is_auto() {
1101 assert_eq!(StorageFormat::default(), StorageFormat::Auto);
1102 }
1103
1104 #[test]
1105 fn test_storage_format_display() {
1106 assert_eq!(StorageFormat::Auto.to_string(), "auto");
1107 assert_eq!(StorageFormat::WalDirectory.to_string(), "wal-directory");
1108 assert_eq!(StorageFormat::SingleFile.to_string(), "single-file");
1109 }
1110
1111 #[test]
1112 fn test_config_with_storage_format() {
1113 let config = Config::in_memory().with_storage_format(StorageFormat::SingleFile);
1114 assert_eq!(config.storage_format, StorageFormat::SingleFile);
1115
1116 let config2 = Config::in_memory().with_storage_format(StorageFormat::WalDirectory);
1117 assert_eq!(config2.storage_format, StorageFormat::WalDirectory);
1118 }
1119
1120 #[test]
1123 fn test_config_with_cdc() {
1124 let config = Config::in_memory().with_cdc();
1125 assert!(config.cdc_enabled);
1126 }
1127
1128 #[test]
1129 fn test_config_cdc_default_false() {
1130 let config = Config::default();
1131 assert!(!config.cdc_enabled);
1132 }
1133
1134 #[test]
1137 fn test_config_error_is_std_error() {
1138 let err = ConfigError::ZeroMemoryLimit;
1139 let dyn_err: &dyn std::error::Error = &err;
1141 assert!(dyn_err.source().is_none());
1142 assert!(!dyn_err.to_string().is_empty());
1143 }
1144
1145 #[test]
1148 fn test_validate_accepts_nonzero_memory_limit() {
1149 let config = Config::in_memory().with_memory_limit(1);
1150 assert!(config.validate().is_ok());
1151 }
1152
1153 #[test]
1154 fn test_validate_accepts_none_memory_limit() {
1155 let config = Config::in_memory();
1156 assert!(config.memory_limit.is_none());
1157 assert!(config.validate().is_ok());
1158 }
1159
1160 #[test]
1163 fn test_durability_mode_debug() {
1164 let sync = DurabilityMode::Sync;
1165 let debug = format!("{sync:?}");
1166 assert_eq!(debug, "Sync");
1167
1168 let no_sync = DurabilityMode::NoSync;
1169 let debug = format!("{no_sync:?}");
1170 assert_eq!(debug, "NoSync");
1171 }
1172
1173 #[test]
1176 fn test_read_only_config_full() {
1177 let config = Config::read_only("/tmp/data.grafeo");
1178 assert_eq!(config.access_mode, AccessMode::ReadOnly);
1179 assert!(!config.wal_enabled);
1180 assert!(config.path.is_some());
1181 assert!(config.backward_edges);
1183 assert_eq!(config.graph_model, GraphModel::Lpg);
1184 }
1185}