1use serde::{Deserialize, Serialize};
48use std::path::PathBuf;
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct Config {
77 #[serde(default)]
79 pub storage: StorageConfig,
80 #[serde(default)]
82 pub encryption: EncryptionConfig,
83 #[serde(default)]
85 pub server: ServerConfig,
86 #[serde(default)]
88 pub performance: PerformanceConfig,
89 #[serde(default)]
91 pub audit: crate::audit::AuditConfig,
92 #[serde(default)]
94 pub optimizer: OptimizerConfig,
95 #[serde(default)]
97 pub authentication: AuthenticationConfig,
98 #[serde(default)]
100 pub compression: CompressionConfig,
101 #[serde(default)]
103 pub materialized_views: MaterializedViewConfig,
104 #[serde(default)]
106 pub vector: VectorConfig,
107 #[serde(default)]
109 pub sync: SyncConfig,
110 #[serde(default)]
112 pub session: SessionConfig,
113 #[serde(default)]
115 pub locks: LockConfig,
116 #[serde(default)]
118 pub dump: DumpConfig,
119 #[serde(default)]
121 pub resource_quotas: ResourceQuotaConfig,
122 #[serde(default)]
124 pub api: ApiConfig,
125}
126
127impl Default for Config {
128 fn default() -> Self {
129 Self {
130 storage: StorageConfig::default(),
131 encryption: EncryptionConfig::default(),
132 server: ServerConfig::default(),
133 performance: PerformanceConfig::default(),
134 audit: crate::audit::AuditConfig::default(),
135 optimizer: OptimizerConfig::default(),
136 authentication: AuthenticationConfig::default(),
137 compression: CompressionConfig::default(),
138 materialized_views: MaterializedViewConfig::default(),
139 vector: VectorConfig::default(),
140 sync: SyncConfig::default(),
141 session: SessionConfig::default(),
142 locks: LockConfig::default(),
143 dump: DumpConfig::default(),
144 resource_quotas: ResourceQuotaConfig::default(),
145 api: ApiConfig::default(),
146 }
147 }
148}
149
150impl Config {
151 pub fn in_memory() -> Self {
153 Self {
154 storage: StorageConfig {
155 path: None,
156 memory_only: true,
157 wal_enabled: false, ..Default::default()
159 },
160 audit: crate::audit::AuditConfig::default(),
161 ..Default::default()
162 }
163 }
164
165 pub fn from_file(path: impl AsRef<std::path::Path>) -> crate::Result<Self> {
167 let content = std::fs::read_to_string(path)?;
168 let config: Config = toml::from_str(&content)
169 .map_err(|e| crate::Error::config(format!("Failed to parse config: {}", e)))?;
170 Ok(config)
171 }
172
173 pub fn save_to_file(&self, path: impl AsRef<std::path::Path>) -> crate::Result<()> {
175 let content = toml::to_string_pretty(self)
176 .map_err(|e| crate::Error::config(format!("Failed to serialize config: {}", e)))?;
177 std::fs::write(path, content)?;
178 Ok(())
179 }
180
181 pub fn validate(&self) -> crate::Result<()> {
183 self.session.validate()?;
184 self.locks.validate()?;
185 self.dump.validate()?;
186 self.resource_quotas.validate()?;
187 Ok(())
188 }
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
193#[serde(default)]
194pub struct StorageConfig {
195 pub path: Option<PathBuf>,
197 pub memory_only: bool,
199 pub wal_enabled: bool,
201 pub wal_sync_mode: WalSyncModeConfig,
203 pub cache_size: usize,
205 pub compression: CompressionType,
207 pub time_travel_enabled: bool,
216 pub query_timeout_ms: Option<u64>,
225 pub statement_timeout_ms: Option<u64>,
234 pub transaction_isolation: TransactionIsolation,
239 #[serde(default = "default_slow_query_threshold")]
246 pub slow_query_threshold_ms: Option<u64>,
247}
248
249fn default_slow_query_threshold() -> Option<u64> {
250 Some(1000)
251}
252
253fn default_idle_timeout_secs() -> u64 {
254 300 }
256
257impl Default for StorageConfig {
258 fn default() -> Self {
259 Self {
260 path: Some(PathBuf::from("./heliosdb-data")),
261 memory_only: false,
262 wal_enabled: true,
263 wal_sync_mode: WalSyncModeConfig::Sync, cache_size: 512 * 1024 * 1024, compression: CompressionType::Zstd,
266 time_travel_enabled: true, query_timeout_ms: None, statement_timeout_ms: None, transaction_isolation: TransactionIsolation::ReadCommitted, slow_query_threshold_ms: Some(1000), }
272 }
273}
274
275#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
277#[serde(rename_all = "snake_case")]
278pub enum WalSyncModeConfig {
279 Sync,
281 Async,
283 GroupCommit,
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
289pub enum CompressionType {
290 None,
292 Zstd,
294 Lz4,
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
303#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
304pub enum TransactionIsolation {
305 ReadUncommitted,
310 ReadCommitted,
315 RepeatableRead,
320 Serializable,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329#[serde(default)]
330pub struct EncryptionConfig {
331 pub enabled: bool,
333 pub algorithm: EncryptionAlgorithm,
335 pub key_source: KeySource,
337 pub rotation_interval_days: u32,
339 #[serde(default)]
341 pub zke: ZkeEncryptionConfig,
342}
343
344impl Default for EncryptionConfig {
345 fn default() -> Self {
346 Self {
347 enabled: false,
348 algorithm: EncryptionAlgorithm::Aes256Gcm,
349 key_source: KeySource::Environment("HELIOSDB_ENCRYPTION_KEY".to_string()),
350 rotation_interval_days: 90,
351 zke: ZkeEncryptionConfig::default(),
352 }
353 }
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct ZkeEncryptionConfig {
380 pub enabled: bool,
382 pub mode: ZkeMode,
384 pub require_key_hash: bool,
386 pub replay_protection: bool,
388 pub nonce_window_secs: u64,
390 pub max_cached_nonces: usize,
392}
393
394impl Default for ZkeEncryptionConfig {
395 fn default() -> Self {
396 Self {
397 enabled: false,
398 mode: ZkeMode::PerRequest,
399 require_key_hash: true,
400 replay_protection: true,
401 nonce_window_secs: 300,
402 max_cached_nonces: 10000,
403 }
404 }
405}
406
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
409#[serde(rename_all = "snake_case")]
410pub enum ZkeMode {
411 Full,
415 Hybrid,
419 PerRequest,
423}
424
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
427pub enum EncryptionAlgorithm {
428 Aes256Gcm,
430}
431
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
455pub enum KeySource {
456 Environment(String),
458 File(PathBuf),
460 Kms {
462 provider: String,
464 key_id: String,
466 },
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
471#[serde(default)]
472pub struct ServerConfig {
473 pub listen_addr: String,
475 pub port: u16,
477 pub oracle_port: Option<u16>,
479 pub max_connections: usize,
481 #[serde(default = "default_idle_timeout_secs")]
483 pub idle_timeout_secs: u64,
484 pub tls_enabled: bool,
486 pub tls_cert_path: Option<PathBuf>,
488 pub tls_key_path: Option<PathBuf>,
490}
491
492impl Default for ServerConfig {
493 fn default() -> Self {
494 Self {
495 listen_addr: "127.0.0.1".to_string(),
496 port: 5432,
497 oracle_port: Some(1521), max_connections: 100,
499 idle_timeout_secs: default_idle_timeout_secs(),
500 tls_enabled: false,
501 tls_cert_path: None,
502 tls_key_path: None,
503 }
504 }
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
509#[serde(default)]
510pub struct PerformanceConfig {
511 pub worker_threads: usize,
513 pub query_timeout_secs: u64,
515 pub simd_enabled: bool,
517 pub parallel_query: bool,
519}
520
521impl Default for PerformanceConfig {
522 fn default() -> Self {
523 Self {
524 worker_threads: num_cpus::get(),
525 query_timeout_secs: 300,
526 simd_enabled: true,
527 parallel_query: true,
528 }
529 }
530}
531
532mod num_cpus {
535 pub fn get() -> usize {
536 std::thread::available_parallelism()
537 .map(|n| n.get())
538 .unwrap_or(4)
539 }
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544#[serde(default)]
545pub struct OptimizerConfig {
546 pub enabled: bool,
548 pub enable_seqscan: bool,
550 pub enable_indexscan: bool,
552 pub enable_hashjoin: bool,
554 pub enable_mergejoin: bool,
556 pub enable_nestloop: bool,
558 pub seq_page_cost: f64,
560 pub random_page_cost: f64,
561 pub cpu_tuple_cost: f64,
562 pub cpu_index_tuple_cost: f64,
563}
564
565impl Default for OptimizerConfig {
566 fn default() -> Self {
567 Self {
568 enabled: true,
569 enable_seqscan: true,
570 enable_indexscan: true,
571 enable_hashjoin: true,
572 enable_mergejoin: true,
573 enable_nestloop: true,
574 seq_page_cost: 1.0,
575 random_page_cost: 4.0,
576 cpu_tuple_cost: 0.01,
577 cpu_index_tuple_cost: 0.005,
578 }
579 }
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize)]
584#[serde(default)]
585pub struct AuthenticationConfig {
586 pub enabled: bool,
588 pub method: AuthMethod,
590 pub jwt_secret: Option<String>,
592 pub jwt_expiration_secs: u64,
594 pub password_hash_algorithm: PasswordHashAlgorithm,
596 pub users_file: Option<PathBuf>,
598}
599
600impl Default for AuthenticationConfig {
601 fn default() -> Self {
602 Self {
603 enabled: false,
604 method: AuthMethod::Trust,
605 jwt_secret: None,
606 jwt_expiration_secs: 86400, password_hash_algorithm: PasswordHashAlgorithm::Argon2,
608 users_file: None,
609 }
610 }
611}
612
613#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
615#[serde(rename_all = "lowercase")]
616pub enum AuthMethod {
617 Trust,
619 Password,
621 Jwt,
623 Ldap,
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
629#[serde(rename_all = "lowercase")]
630pub enum PasswordHashAlgorithm {
631 Argon2,
633 Bcrypt,
635 Pbkdf2,
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
641#[serde(default)]
642pub struct CompressionConfig {
643 pub default_type: CompressionType,
645 pub level: i32,
647 pub enable_alp: bool,
649 pub enable_fsst: bool,
651 pub min_size_bytes: usize,
653}
654
655impl Default for CompressionConfig {
656 fn default() -> Self {
657 Self {
658 default_type: CompressionType::Zstd,
659 level: 3,
660 enable_alp: true,
661 enable_fsst: true,
662 min_size_bytes: 1024, }
664 }
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize)]
669#[serde(default)]
670pub struct MaterializedViewConfig {
671 pub auto_refresh_default: bool,
673 pub default_max_cpu_percent: u8,
675 pub refresh_check_interval_secs: u64,
677 pub max_concurrent_refreshes: usize,
679 pub enable_incremental: bool,
681 pub enable_delta_tracking: bool,
683 pub enable_scheduler: bool,
685 pub delta_retention_hours: u64,
687}
688
689impl Default for MaterializedViewConfig {
690 fn default() -> Self {
691 Self {
692 auto_refresh_default: false,
693 default_max_cpu_percent: 15,
694 refresh_check_interval_secs: 60,
695 max_concurrent_refreshes: 2,
696 enable_incremental: false, enable_delta_tracking: false, enable_scheduler: false, delta_retention_hours: 168, }
701 }
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize)]
706#[serde(default)]
707pub struct VectorConfig {
708 pub default_index_type: VectorIndexType,
710 pub hnsw_ef_construction: usize,
712 pub hnsw_m: usize,
714 pub enable_pq: bool,
716 pub pq_subvectors: usize,
718 pub pq_bits: usize,
720}
721
722impl Default for VectorConfig {
723 fn default() -> Self {
724 Self {
725 default_index_type: VectorIndexType::Hnsw,
726 hnsw_ef_construction: 200,
727 hnsw_m: 16,
728 enable_pq: true,
729 pq_subvectors: 8,
730 pq_bits: 8,
731 }
732 }
733}
734
735#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
737#[serde(rename_all = "lowercase")]
738pub enum VectorIndexType {
739 Flat,
741 Hnsw,
743 Ivf,
745}
746
747#[derive(Debug, Clone, Serialize, Deserialize)]
749#[serde(default)]
750pub struct SyncConfig {
751 pub enabled: bool,
753 pub node_id: Option<String>,
755 pub server_url: Option<String>,
757 pub client_id: Option<String>,
759 pub sync_interval_secs: u64,
761 pub change_log_enabled: bool,
763}
764
765impl Default for SyncConfig {
766 fn default() -> Self {
767 Self {
768 enabled: false,
769 node_id: None,
770 server_url: None,
771 client_id: None,
772 sync_interval_secs: 30,
773 change_log_enabled: true,
774 }
775 }
776}
777
778#[derive(Debug, Clone, Serialize, Deserialize)]
780#[serde(default)]
781pub struct SessionConfig {
782 pub timeout_secs: u64,
784 pub max_sessions_per_user: u32,
786 pub cleanup_interval_secs: u64,
788}
789
790impl Default for SessionConfig {
791 fn default() -> Self {
792 Self {
793 timeout_secs: 3600,
794 max_sessions_per_user: 10,
795 cleanup_interval_secs: 300,
796 }
797 }
798}
799
800impl SessionConfig {
801 pub fn validate(&self) -> crate::Result<()> {
803 if self.timeout_secs < 1 {
804 return Err(crate::Error::config(
805 "session.timeout_secs must be at least 1 second",
806 ));
807 }
808 if self.max_sessions_per_user < 1 {
809 return Err(crate::Error::config(
810 "session.max_sessions_per_user must be at least 1",
811 ));
812 }
813 if self.cleanup_interval_secs < 1 {
814 return Err(crate::Error::config(
815 "session.cleanup_interval_secs must be at least 1 second",
816 ));
817 }
818 Ok(())
819 }
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize)]
824#[serde(default)]
825pub struct LockConfig {
826 pub timeout_ms: u32,
828 pub deadlock_check_interval_ms: u32,
830 pub max_lock_holders: u32,
832}
833
834impl Default for LockConfig {
835 fn default() -> Self {
836 Self {
837 timeout_ms: 30000,
838 deadlock_check_interval_ms: 100,
839 max_lock_holders: 10000,
840 }
841 }
842}
843
844impl LockConfig {
845 pub fn validate(&self) -> crate::Result<()> {
847 if self.timeout_ms < 100 {
848 return Err(crate::Error::config(
849 "locks.timeout_ms must be at least 100 milliseconds",
850 ));
851 }
852 if self.deadlock_check_interval_ms < 10 {
853 return Err(crate::Error::config(
854 "locks.deadlock_check_interval_ms must be at least 10 milliseconds",
855 ));
856 }
857 if self.max_lock_holders < 1 {
858 return Err(crate::Error::config(
859 "locks.max_lock_holders must be at least 1",
860 ));
861 }
862 Ok(())
863 }
864}
865
866#[derive(Debug, Clone, Serialize, Deserialize)]
868#[serde(default)]
869pub struct DumpConfig {
870 pub auto_dump_enabled: bool,
872 pub schedule: String,
874 pub compression: String,
876 pub max_dump_size_mb: u64,
878 pub keep_dumps: usize,
880 pub dump_dir: String,
882}
883
884impl Default for DumpConfig {
885 fn default() -> Self {
886 Self {
887 auto_dump_enabled: false,
888 schedule: String::new(),
889 compression: "zstd".to_string(),
890 max_dump_size_mb: 10000,
891 keep_dumps: 10,
892 dump_dir: ".dumps".to_string(),
893 }
894 }
895}
896
897impl DumpConfig {
898 pub fn validate(&self) -> crate::Result<()> {
900 match self.compression.as_str() {
902 "zstd" | "gzip" | "none" => {}
903 _ => {
904 return Err(crate::Error::config(format!(
905 "dump.compression must be 'zstd', 'gzip', or 'none', got '{}'",
906 self.compression
907 )));
908 }
909 }
910
911 if self.max_dump_size_mb < 1 {
913 return Err(crate::Error::config(
914 "dump.max_dump_size_mb must be at least 1 MB",
915 ));
916 }
917
918 if self.auto_dump_enabled && !self.schedule.is_empty() {
920 Self::validate_cron_schedule(&self.schedule)?;
921 }
922
923 Ok(())
924 }
925
926 fn validate_cron_schedule(schedule: &str) -> crate::Result<()> {
928 let parts: Vec<&str> = schedule.split_whitespace().collect();
929 if parts.len() != 5 {
930 return Err(crate::Error::config(format!(
931 "Invalid cron schedule format '{}'. Expected 5 fields: minute hour day month weekday",
932 schedule
933 )));
934 }
935 Ok(())
936 }
937}
938
939#[derive(Debug, Clone, Serialize, Deserialize)]
941#[serde(default)]
942pub struct ResourceQuotaConfig {
943 pub memory_limit_per_user_mb: u64,
945 pub max_concurrent_queries: u32,
947 pub query_timeout_secs: u64,
949}
950
951impl Default for ResourceQuotaConfig {
952 fn default() -> Self {
953 Self {
954 memory_limit_per_user_mb: 1024,
955 max_concurrent_queries: 100,
956 query_timeout_secs: 300,
957 }
958 }
959}
960
961impl ResourceQuotaConfig {
962 pub fn validate(&self) -> crate::Result<()> {
964 if self.memory_limit_per_user_mb < 1 {
965 return Err(crate::Error::config(
966 "resource_quotas.memory_limit_per_user_mb must be at least 1 MB",
967 ));
968 }
969 if self.max_concurrent_queries < 1 {
970 return Err(crate::Error::config(
971 "resource_quotas.max_concurrent_queries must be at least 1",
972 ));
973 }
974 if self.query_timeout_secs < 1 {
975 return Err(crate::Error::config(
976 "resource_quotas.query_timeout_secs must be at least 1 second",
977 ));
978 }
979 Ok(())
980 }
981}
982
983#[derive(Debug, Clone, Serialize, Deserialize)]
997#[serde(default)]
998pub struct ApiConfig {
999 pub jwt_secret: String,
1003 pub anon_key: Option<String>,
1005 pub service_role_key: Option<String>,
1007 #[serde(default)]
1025 pub oauth_providers: Vec<OAuthProviderConfig>,
1026}
1027
1028#[derive(Debug, Clone, Serialize, Deserialize)]
1030pub struct OAuthProviderConfig {
1031 pub name: String,
1033 pub client_id: String,
1035 pub client_secret: String,
1037 pub redirect_uri: String,
1039}
1040
1041impl Default for ApiConfig {
1042 fn default() -> Self {
1043 Self {
1044 jwt_secret: generate_random_secret(),
1045 anon_key: None,
1046 service_role_key: None,
1047 oauth_providers: Vec::new(),
1048 }
1049 }
1050}
1051
1052fn generate_random_secret() -> String {
1054 use std::collections::hash_map::RandomState;
1055 use std::hash::{BuildHasher, Hasher};
1056
1057 let s = RandomState::new();
1059 let h1 = s.build_hasher().finish();
1060 let h2 = RandomState::new().build_hasher().finish();
1061 format!("{h1:016x}{h2:016x}{h1:016x}{h2:016x}")
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066 use super::*;
1067
1068 #[test]
1069 fn test_session_config_default() {
1070 let config = SessionConfig::default();
1071 assert_eq!(config.timeout_secs, 3600);
1072 assert_eq!(config.max_sessions_per_user, 10);
1073 assert_eq!(config.cleanup_interval_secs, 300);
1074 assert!(config.validate().is_ok());
1075 }
1076
1077 #[test]
1078 fn test_session_config_validation() {
1079 let mut config = SessionConfig::default();
1080
1081 assert!(config.validate().is_ok());
1083
1084 config.timeout_secs = 0;
1086 assert!(config.validate().is_err());
1087 config.timeout_secs = 3600;
1088
1089 config.max_sessions_per_user = 0;
1091 assert!(config.validate().is_err());
1092 config.max_sessions_per_user = 10;
1093
1094 config.cleanup_interval_secs = 0;
1096 assert!(config.validate().is_err());
1097 }
1098
1099 #[test]
1100 fn test_lock_config_default() {
1101 let config = LockConfig::default();
1102 assert_eq!(config.timeout_ms, 30000);
1103 assert_eq!(config.deadlock_check_interval_ms, 100);
1104 assert_eq!(config.max_lock_holders, 10000);
1105 assert!(config.validate().is_ok());
1106 }
1107
1108 #[test]
1109 fn test_lock_config_validation() {
1110 let mut config = LockConfig::default();
1111
1112 assert!(config.validate().is_ok());
1114
1115 config.timeout_ms = 50;
1117 assert!(config.validate().is_err());
1118 config.timeout_ms = 30000;
1119
1120 config.deadlock_check_interval_ms = 5;
1122 assert!(config.validate().is_err());
1123 config.deadlock_check_interval_ms = 100;
1124
1125 config.max_lock_holders = 0;
1127 assert!(config.validate().is_err());
1128 }
1129
1130 #[test]
1131 fn test_dump_config_default() {
1132 let config = DumpConfig::default();
1133 assert!(!config.auto_dump_enabled);
1134 assert_eq!(config.schedule, "");
1135 assert_eq!(config.compression, "zstd");
1136 assert_eq!(config.max_dump_size_mb, 10000);
1137 assert_eq!(config.keep_dumps, 10);
1138 assert_eq!(config.dump_dir, ".dumps");
1139 assert!(config.validate().is_ok());
1140 }
1141
1142 #[test]
1143 fn test_dump_config_validation() {
1144 let mut config = DumpConfig::default();
1145
1146 assert!(config.validate().is_ok());
1148
1149 config.compression = "invalid".to_string();
1151 assert!(config.validate().is_err());
1152 config.compression = "zstd".to_string();
1153
1154 config.compression = "gzip".to_string();
1156 assert!(config.validate().is_ok());
1157 config.compression = "none".to_string();
1158 assert!(config.validate().is_ok());
1159 config.compression = "zstd".to_string();
1160
1161 config.max_dump_size_mb = 0;
1163 assert!(config.validate().is_err());
1164 config.max_dump_size_mb = 10000;
1165
1166 config.auto_dump_enabled = true;
1168 config.schedule = "0 */6 * * *".to_string();
1169 assert!(config.validate().is_ok());
1170
1171 config.schedule = "invalid".to_string();
1173 assert!(config.validate().is_err());
1174
1175 config.schedule = "".to_string();
1177 assert!(config.validate().is_ok());
1178 }
1179
1180 #[test]
1181 fn test_resource_quota_config_default() {
1182 let config = ResourceQuotaConfig::default();
1183 assert_eq!(config.memory_limit_per_user_mb, 1024);
1184 assert_eq!(config.max_concurrent_queries, 100);
1185 assert_eq!(config.query_timeout_secs, 300);
1186 assert!(config.validate().is_ok());
1187 }
1188
1189 #[test]
1190 fn test_resource_quota_config_validation() {
1191 let mut config = ResourceQuotaConfig::default();
1192
1193 assert!(config.validate().is_ok());
1195
1196 config.memory_limit_per_user_mb = 0;
1198 assert!(config.validate().is_err());
1199 config.memory_limit_per_user_mb = 1024;
1200
1201 config.max_concurrent_queries = 0;
1203 assert!(config.validate().is_err());
1204 config.max_concurrent_queries = 100;
1205
1206 config.query_timeout_secs = 0;
1208 assert!(config.validate().is_err());
1209 }
1210
1211 #[test]
1212 fn test_config_with_new_sections() {
1213 let config = Config::default();
1214
1215 assert_eq!(config.session.timeout_secs, 3600);
1217 assert_eq!(config.locks.timeout_ms, 30000);
1218 assert_eq!(config.dump.compression, "zstd");
1219 assert_eq!(config.resource_quotas.memory_limit_per_user_mb, 1024);
1220
1221 assert!(config.validate().is_ok());
1223 }
1224
1225 #[test]
1226 fn test_config_serialization() {
1227 let config = Config::default();
1228
1229 let toml_str = toml::to_string(&config).expect("Failed to serialize config");
1231
1232 assert!(toml_str.contains("[session]"));
1234 assert!(toml_str.contains("[locks]"));
1235 assert!(toml_str.contains("[dump]"));
1236 assert!(toml_str.contains("[resource_quotas]"));
1237 }
1238
1239 #[test]
1240 fn test_config_deserialization() {
1241 let toml_str = r#"
1242 [storage]
1243 memory_only = true
1244 wal_enabled = false
1245 wal_sync_mode = "sync"
1246 cache_size = 536870912
1247 compression = "Zstd"
1248 time_travel_enabled = true
1249 transaction_isolation = "READ_COMMITTED"
1250
1251 [session]
1252 timeout_secs = 7200
1253 max_sessions_per_user = 20
1254 cleanup_interval_secs = 600
1255
1256 [locks]
1257 timeout_ms = 60000
1258 deadlock_check_interval_ms = 200
1259 max_lock_holders = 20000
1260
1261 [dump]
1262 auto_dump_enabled = true
1263 schedule = "0 2 * * *"
1264 compression = "gzip"
1265 max_dump_size_mb = 5000
1266 keep_dumps = 5
1267 dump_dir = "/var/dumps"
1268
1269 [resource_quotas]
1270 memory_limit_per_user_mb = 2048
1271 max_concurrent_queries = 200
1272 query_timeout_secs = 600
1273 "#;
1274
1275 let config: Config = toml::from_str(toml_str).expect("Failed to deserialize config");
1276
1277 assert_eq!(config.session.timeout_secs, 7200);
1279 assert_eq!(config.session.max_sessions_per_user, 20);
1280 assert_eq!(config.session.cleanup_interval_secs, 600);
1281
1282 assert_eq!(config.locks.timeout_ms, 60000);
1284 assert_eq!(config.locks.deadlock_check_interval_ms, 200);
1285 assert_eq!(config.locks.max_lock_holders, 20000);
1286
1287 assert!(config.dump.auto_dump_enabled);
1289 assert_eq!(config.dump.schedule, "0 2 * * *");
1290 assert_eq!(config.dump.compression, "gzip");
1291 assert_eq!(config.dump.max_dump_size_mb, 5000);
1292 assert_eq!(config.dump.keep_dumps, 5);
1293 assert_eq!(config.dump.dump_dir, "/var/dumps");
1294
1295 assert_eq!(config.resource_quotas.memory_limit_per_user_mb, 2048);
1297 assert_eq!(config.resource_quotas.max_concurrent_queries, 200);
1298 assert_eq!(config.resource_quotas.query_timeout_secs, 600);
1299
1300 assert!(config.validate().is_ok());
1302 }
1303
1304 #[test]
1305 fn test_config_with_missing_sections() {
1306 let toml_str = r#"
1308 [storage]
1309 memory_only = true
1310 wal_enabled = false
1311 wal_sync_mode = "sync"
1312 cache_size = 536870912
1313 compression = "Zstd"
1314 time_travel_enabled = true
1315 transaction_isolation = "READ_COMMITTED"
1316 "#;
1317
1318 let config: Config = toml::from_str(toml_str).expect("Failed to deserialize config");
1319
1320 assert_eq!(config.session.timeout_secs, 3600);
1322 assert_eq!(config.locks.timeout_ms, 30000);
1323 assert_eq!(config.dump.compression, "zstd");
1324 assert_eq!(config.resource_quotas.memory_limit_per_user_mb, 1024);
1325
1326 assert!(config.validate().is_ok());
1328 }
1329
1330 #[test]
1331 fn test_cron_schedule_validation() {
1332 assert!(DumpConfig::validate_cron_schedule("0 */6 * * *").is_ok());
1334 assert!(DumpConfig::validate_cron_schedule("0 2 * * *").is_ok());
1335 assert!(DumpConfig::validate_cron_schedule("*/15 * * * *").is_ok());
1336 assert!(DumpConfig::validate_cron_schedule("0 0 1 * *").is_ok());
1337 assert!(DumpConfig::validate_cron_schedule("30 3 * * 0").is_ok());
1338
1339 assert!(DumpConfig::validate_cron_schedule("invalid").is_err());
1341 assert!(DumpConfig::validate_cron_schedule("0 * * *").is_err()); assert!(DumpConfig::validate_cron_schedule("0 * * * * *").is_err()); assert!(DumpConfig::validate_cron_schedule("").is_err()); }
1345}