1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Config {
8 pub default_database: Option<PathBuf>,
9 #[serde(default)]
10 pub connection: ConnectionConfig,
11 #[serde(default)]
12 pub output: OutputSettings,
13 #[serde(default)]
14 pub performance: PerformanceConfig,
15 #[serde(default)]
16 pub logging: LoggingConfig,
17 #[serde(default)]
18 pub repl: ReplConfig,
19 pub data_directory: Option<PathBuf>,
20 pub default_keyspace: Option<String>,
21
22 #[serde(skip_serializing_if = "Option::is_none")]
24 pub enable_history: Option<bool>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub enable_completion: Option<bool>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub show_timing: Option<bool>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub page_size: Option<usize>,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 pub enable_paging: Option<bool>,
33 #[serde(default)]
34 pub no_color: bool,
35
36 #[serde(default)]
39 pub schema_paths: Vec<PathBuf>,
40
41 #[serde(skip)]
43 pub execution_query: Option<String>,
44
45 #[serde(skip)]
47 pub execution_file: Option<PathBuf>,
48
49 pub output_mode: Option<String>,
51
52 pub query_limit: Option<usize>,
54
55 #[serde(skip)]
58 pub cassandra_version: Option<String>,
59
60 #[serde(skip)]
63 #[allow(dead_code)]
64 pub resolved_version: Option<cqlite_core::version_hints::ResolvedVersion>,
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ConnectionConfig {
69 pub timeout_ms: u64,
70 pub retry_attempts: u32,
71 pub pool_size: u32,
72}
73
74impl Default for ConnectionConfig {
75 fn default() -> Self {
76 Self {
77 timeout_ms: 30000,
78 retry_attempts: 3,
79 pool_size: 10,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct OutputSettings {
86 pub max_rows: Option<usize>,
87 pub pager: Option<String>,
88 pub colors: bool,
89 pub timestamp_format: String,
90}
91
92impl Default for OutputSettings {
93 fn default() -> Self {
94 Self {
95 max_rows: Some(1000),
96 pager: None,
97 colors: true,
98 timestamp_format: "%Y-%m-%d %H:%M:%S".to_string(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct PerformanceConfig {
105 pub query_timeout_ms: u64,
106 pub memory_limit_mb: Option<u64>,
107 pub cache_size_mb: u64,
108}
109
110impl Default for PerformanceConfig {
111 fn default() -> Self {
112 Self {
113 query_timeout_ms: 30000,
114 memory_limit_mb: None,
115 cache_size_mb: 64,
116 }
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct LoggingConfig {
122 pub level: String,
123 pub file: Option<PathBuf>,
124 pub format: LogFormat,
125}
126
127impl Default for LoggingConfig {
128 fn default() -> Self {
129 Self {
130 level: "info".to_string(),
131 file: None,
132 format: LogFormat::Pretty,
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, Default)]
138pub enum LogFormat {
139 Plain,
140 Json,
141 #[default]
142 Pretty,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ReplConfig {
147 pub enable_history: bool,
148 pub enable_completion: bool,
149 pub enable_colors: bool,
150 pub show_timing: bool,
151 pub page_size: usize,
152 pub enable_paging: bool,
153 pub max_history_size: usize,
154 pub prompt: String,
155 pub prompt_continuation: String,
156 pub history_file: Option<PathBuf>,
157}
158
159impl Default for Config {
160 fn default() -> Self {
161 Self {
162 default_database: None,
163 connection: ConnectionConfig::default(),
164 output: OutputSettings::default(),
165 performance: PerformanceConfig::default(),
166 logging: LoggingConfig::default(),
167 repl: ReplConfig::default(),
168 data_directory: None,
169 default_keyspace: None,
170 enable_history: None,
171 enable_completion: None,
172 show_timing: None,
173 page_size: None,
174 enable_paging: None,
175 no_color: false,
176 schema_paths: Vec::new(),
177 execution_query: None,
178 execution_file: None,
179 output_mode: None,
180 query_limit: None,
181 cassandra_version: None,
182 resolved_version: None,
183 }
184 }
185}
186
187impl Config {
188 pub fn load(config_path: Option<PathBuf>, cli: &crate::cli_types::Cli) -> Result<Self> {
189 let mut builder = ConfigBuilder::from_defaults()
190 .with_user_config()? .with_project_config()?; if let Some(path) = config_path {
195 builder = builder.with_explicit_config(path)?;
196 }
197
198 Ok(builder.with_env()?.with_flags(cli).build())
201 }
202
203 #[allow(dead_code)]
221 pub async fn resolve_version(
222 &mut self,
223 platform: std::sync::Arc<cqlite_core::Platform>,
224 ) -> Result<()> {
225 use cqlite_core::version_hints::VersionHintResolver;
226 use std::path::PathBuf;
227
228 let default_path = PathBuf::from(".");
230 let data_dir = self
231 .data_directory
232 .as_deref()
233 .unwrap_or(default_path.as_path());
234
235 self.resolved_version = Some(
236 VersionHintResolver::resolve(self.cassandra_version.clone(), data_dir, platform)
237 .await?,
238 );
239
240 Ok(())
241 }
242
243 #[allow(dead_code)]
250 pub fn version_info(&self) -> Option<&cqlite_core::version_hints::ResolvedVersion> {
251 self.resolved_version.as_ref()
252 }
253
254 #[allow(dead_code)]
258 pub fn version_string(&self) -> String {
259 self.resolved_version
260 .as_ref()
261 .map(|rv| rv.version_or_unknown().to_string())
262 .unwrap_or_else(|| "not resolved".to_string())
263 }
264
265 #[allow(dead_code)]
269 pub fn version_source(&self) -> String {
270 self.resolved_version
271 .as_ref()
272 .map(|rv| rv.source.description().to_string())
273 .unwrap_or_else(|| "not resolved".to_string())
274 }
275
276 fn load_from_file(path: &Path) -> Result<Self> {
277 let content = fs::read_to_string(path)
278 .with_context(|| format!("Failed to read config file: {}", path.display()))?;
279
280 let config: Config = match path.extension().and_then(|ext| ext.to_str()) {
281 Some("toml") => {
282 toml::from_str(&content).with_context(|| "Failed to parse TOML config")?
283 }
284 Some("yaml") | Some("yml") => {
285 serde_yaml::from_str(&content).with_context(|| "Failed to parse YAML config")?
286 }
287 Some("json") => {
288 serde_json::from_str(&content).with_context(|| "Failed to parse JSON config")?
289 }
290 _ => return Err(anyhow::anyhow!("Unsupported config file format")),
291 };
292
293 Ok(config)
294 }
295
296 #[allow(dead_code)]
297 fn load_default() -> Result<Self> {
298 let config_paths = [
300 "cqlite.toml",
301 "cqlite.yaml",
302 "cqlite.yml",
303 "cqlite.json",
304 ".cqlite.toml",
305 ".cqlite.yaml",
306 ".cqlite.yml",
307 ".cqlite.json",
308 ];
309
310 for path in &config_paths {
311 if Path::new(path).exists() {
312 return Self::load_from_file(Path::new(path));
313 }
314 }
315
316 if let Some(config_dir) = dirs::config_dir() {
318 let xdg_paths = [
319 config_dir.join("cqlite").join("config.toml"),
320 config_dir.join("cqlite").join("config.yaml"),
321 config_dir.join("cqlite").join("config.yml"),
322 config_dir.join("cqlite").join("config.json"),
323 ];
324
325 for path in &xdg_paths {
326 if path.exists() {
327 return Self::load_from_file(path);
328 }
329 }
330 }
331
332 Ok(Self::default())
334 }
335
336 #[allow(dead_code)]
337 pub fn save_to_file(&self, path: &Path) -> Result<()> {
338 let content = match path.extension().and_then(|ext| ext.to_str()) {
339 Some("toml") => toml::to_string_pretty(self)
340 .with_context(|| "Failed to serialize config to TOML")?,
341 Some("yaml") | Some("yml") => {
342 serde_yaml::to_string(self).with_context(|| "Failed to serialize config to YAML")?
343 }
344 Some("json") => serde_json::to_string_pretty(self)
345 .with_context(|| "Failed to serialize config to JSON")?,
346 _ => return Err(anyhow::anyhow!("Unsupported config file format")),
347 };
348
349 if let Some(parent) = path.parent() {
350 fs::create_dir_all(parent).with_context(|| {
351 format!("Failed to create config directory: {}", parent.display())
352 })?;
353 }
354
355 fs::write(path, content)
356 .with_context(|| format!("Failed to write config file: {}", path.display()))?;
357
358 Ok(())
359 }
360}
361
362impl Default for ReplConfig {
363 fn default() -> Self {
364 Self {
365 enable_history: true,
366 enable_completion: true,
367 enable_colors: true,
368 show_timing: false,
369 page_size: 50,
370 enable_paging: true,
371 max_history_size: 1000,
372 prompt: "cqlite> ".to_string(),
373 prompt_continuation: " -> ".to_string(),
374 history_file: None,
375 }
376 }
377}
378
379#[derive(Debug, Clone)]
384pub struct OutputConfig {
385 pub color_enabled: bool,
389
390 pub limit: Option<usize>,
394
395 #[allow(dead_code)]
399 pub page_size: Option<usize>,
400
401 pub target: crate::output::OutputTarget,
405
406 pub overwrite: bool,
409}
410
411impl OutputConfig {
412 pub fn from_cli(
455 config: &Config,
456 no_color_flag: bool,
457 limit_flag: Option<usize>,
458 page_size_flag: Option<usize>,
459 output_flag: Option<std::path::PathBuf>,
460 overwrite_flag: bool,
461 ) -> Self {
462 use crate::output::OutputTarget;
463
464 Self {
465 color_enabled: if no_color_flag {
467 false
468 } else {
469 config.output.colors
470 },
471 limit: limit_flag.or(config.query_limit),
473 page_size: page_size_flag.or(Some(config.repl.page_size)),
475 target: output_flag
477 .map(OutputTarget::File)
478 .unwrap_or(OutputTarget::Stdout),
479 overwrite: overwrite_flag,
481 }
482 }
483}
484
485impl Default for OutputConfig {
486 fn default() -> Self {
495 Self {
496 color_enabled: true,
497 limit: None,
498 page_size: Some(50),
499 target: crate::output::OutputTarget::Stdout,
500 overwrite: false,
501 }
502 }
503}
504
505fn merge_partial_config(base: Config, overlay: Config) -> Config {
508 let final_no_color = overlay.no_color || base.no_color;
510
511 let final_output_colors = if final_no_color {
515 false
516 } else {
517 overlay.output.colors
518 };
519
520 Config {
521 data_directory: overlay.data_directory.or(base.data_directory),
523 default_keyspace: overlay.default_keyspace.or(base.default_keyspace),
524
525 schema_paths: if overlay.schema_paths.is_empty() {
527 base.schema_paths
528 } else {
529 overlay.schema_paths
530 },
531
532 output_mode: overlay.output_mode.or(base.output_mode),
534
535 query_limit: overlay.query_limit.or(base.query_limit),
537
538 connection: overlay.connection,
540 output: OutputSettings {
541 max_rows: overlay.output.max_rows.or(base.output.max_rows),
542 pager: overlay.output.pager.or(base.output.pager),
543 colors: final_output_colors,
544 timestamp_format: overlay.output.timestamp_format,
545 },
546 repl: ReplConfig {
547 enable_history: overlay.repl.enable_history,
548 enable_completion: overlay.repl.enable_completion,
549 enable_colors: overlay.repl.enable_colors,
550 show_timing: overlay.repl.show_timing,
551 page_size: overlay.repl.page_size,
552 enable_paging: overlay.repl.enable_paging,
553 max_history_size: overlay.repl.max_history_size,
554 prompt: overlay.repl.prompt,
555 prompt_continuation: overlay.repl.prompt_continuation,
556 history_file: overlay.repl.history_file.or(base.repl.history_file),
557 },
558 performance: overlay.performance,
559 logging: overlay.logging,
560
561 enable_history: overlay.enable_history.or(base.enable_history),
563 enable_completion: overlay.enable_completion.or(base.enable_completion),
564 show_timing: overlay.show_timing.or(base.show_timing),
565 page_size: overlay.page_size.or(base.page_size),
566 enable_paging: overlay.enable_paging.or(base.enable_paging),
567 no_color: final_no_color,
568
569 execution_query: base.execution_query,
571 execution_file: base.execution_file,
572 cassandra_version: overlay.cassandra_version.or(base.cassandra_version),
573 resolved_version: base.resolved_version,
574
575 default_database: base.default_database,
577 }
578}
579
580pub struct ConfigBuilder {
582 config: Config,
583}
584
585impl ConfigBuilder {
586 pub fn from_defaults() -> Self {
588 Self {
589 config: Config::default(),
590 }
591 }
592
593 #[allow(dead_code)]
597 pub fn with_file(mut self, path: Option<PathBuf>) -> Result<Self> {
598 if let Some(p) = path {
599 let loaded = Config::load_from_file(&p)?;
600 self.config = loaded;
602 }
603 Ok(self)
604 }
605
606 pub fn with_user_config(mut self) -> Result<Self> {
608 if let Some(user_path) = Self::user_config_path() {
609 if user_path.exists() {
610 let loaded = Config::load_from_file(&user_path).with_context(|| {
611 format!("Failed to load user config: {}", user_path.display())
612 })?;
613 self.config = merge_partial_config(self.config, loaded);
614 }
615 }
616 Ok(self)
617 }
618
619 pub fn with_project_config(mut self) -> Result<Self> {
621 let project_path = PathBuf::from("./.cqlite.toml");
622 if project_path.exists() {
623 let loaded = Config::load_from_file(&project_path)
624 .with_context(|| "Failed to load project config")?;
625 self.config = merge_partial_config(self.config, loaded);
626 }
627 Ok(self)
628 }
629
630 pub fn with_explicit_config(mut self, path: PathBuf) -> Result<Self> {
632 let loaded = Config::load_from_file(&path)
633 .with_context(|| format!("Failed to load config file: {}", path.display()))?;
634 self.config = merge_partial_config(self.config, loaded);
635 Ok(self)
636 }
637
638 fn user_config_path() -> Option<PathBuf> {
640 #[cfg(target_os = "macos")]
641 {
642 dirs::home_dir().map(|h| h.join("Library/Application Support/cqlite/config.toml"))
643 }
644 #[cfg(target_os = "windows")]
645 {
646 dirs::config_dir().map(|d| d.join("cqlite").join("config.toml"))
647 }
648 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
649 {
650 std::env::var("XDG_CONFIG_HOME")
651 .ok()
652 .map(|p| PathBuf::from(p).join("cqlite/config.toml"))
653 .or_else(|| dirs::home_dir().map(|h| h.join(".config/cqlite/config.toml")))
654 }
655 }
656
657 pub fn with_env(mut self) -> Result<Self> {
659 use std::env;
660
661 if let Ok(val) = env::var("CQLITE_DATA_DIR") {
663 self.config.data_directory = Some(PathBuf::from(val));
664 }
665
666 if let Ok(val) = env::var("CQLITE_SCHEMA") {
668 let paths: Vec<PathBuf> = val.split(',').map(|s| PathBuf::from(s.trim())).collect();
669 self.config.schema_paths = paths; }
671
672 if let Ok(val) = env::var("CQLITE_LIMIT") {
674 let limit: usize = val.parse().with_context(|| "Invalid CQLITE_LIMIT value")?;
675 if limit == 0 {
676 return Err(anyhow::anyhow!("CQLITE_LIMIT must be greater than 0"));
677 }
678 self.config.query_limit = Some(limit);
679 }
680
681 if let Ok(val) = env::var("CQLITE_PAGE_SIZE") {
683 let page_size: usize = val
684 .parse()
685 .with_context(|| "Invalid CQLITE_PAGE_SIZE value")?;
686 if page_size == 0 {
687 return Err(anyhow::anyhow!("CQLITE_PAGE_SIZE must be greater than 0"));
688 }
689 self.config.repl.page_size = page_size;
690 }
691
692 if let Ok(val) = env::var("CQLITE_NO_COLOR") {
694 let no_color = matches!(val.to_lowercase().as_str(), "1" | "true" | "yes" | "on");
695 self.config.no_color = no_color;
696 self.config.output.colors = !no_color;
697 }
698
699 if let Ok(val) = env::var("CQLITE_OUT") {
701 self.config.output_mode = Some(val);
702 }
703
704 Ok(self)
705 }
706
707 pub fn with_flags(mut self, cli: &crate::cli_types::Cli) -> Self {
713 if let Some(ref schema) = cli.schema {
715 self.config.schema_paths = vec![schema.clone()];
716 }
717
718 if let Some(ref data_dir) = cli.data_dir {
720 self.config.data_directory = Some(data_dir.clone());
721 }
722
723 if let Some(ref query) = cli.execute {
725 self.config.execution_query = Some(query.clone());
726 }
727
728 if let Some(ref file) = cli.file {
730 self.config.execution_file = Some(file.clone());
731 }
732
733 if let Some(ref out) = cli.out {
735 self.config.output_mode = Some(out.as_str().to_string());
736 }
737
738 if let Some(limit) = cli.limit {
740 self.config.query_limit = Some(limit);
741 }
742
743 if let Some(page_size) = cli.page_size {
745 self.config.repl.page_size = page_size;
746 }
747
748 if cli.no_color {
750 self.config.no_color = true;
751 self.config.output.colors = false;
752 }
753
754 if let Some(ref version) = cli.cassandra_version {
756 self.config.cassandra_version = Some(version.clone());
757 }
758
759 self
760 }
761
762 pub fn build(self) -> Config {
764 self.config
765 }
766}
767
768#[cfg(test)]
769mod tests {
770 use super::*;
771 use crate::cli_types::Cli;
772 use clap::Parser;
773 use serial_test::serial;
774 use std::sync::Arc;
775 use tempfile::TempDir;
776
777 #[tokio::test]
778 async fn test_cassandra_version_flag_passed_to_config() {
779 let cli = Cli::parse_from(&[
781 "cqlite",
782 "--cassandra-version",
783 "5.0",
784 "--data-dir",
785 "/tmp/data",
786 ]);
787
788 let config = Config::load(None, &cli).unwrap();
790
791 assert_eq!(config.cassandra_version, Some("5.0".to_string()));
793 }
794
795 #[tokio::test]
796 async fn test_version_resolution_user_override() {
797 let temp_dir = TempDir::new().unwrap();
798
799 let cli = Cli::parse_from(&[
801 "cqlite",
802 "--cassandra-version",
803 "5.0",
804 "--data-dir",
805 temp_dir.path().to_str().unwrap(),
806 ]);
807
808 let mut config = Config::load(None, &cli).unwrap();
810
811 let core_config = cqlite_core::Config::default();
813 let platform = Arc::new(cqlite_core::Platform::new(&core_config).await.unwrap());
814
815 config.resolve_version(platform).await.unwrap();
816
817 let version_info = config.version_info().unwrap();
819 assert_eq!(
820 version_info.source,
821 cqlite_core::version_hints::VersionSource::UserFlag
822 );
823 assert_eq!(version_info.version, Some("5.0".to_string()));
824 }
825
826 #[tokio::test]
827 async fn test_version_resolution_metadata_yml() {
828 let temp_dir = TempDir::new().unwrap();
829
830 let metadata_content = "cassandra_version: \"4.0\"\nkeyspaces: []\n";
832 let metadata_path = temp_dir.path().join("metadata.yml");
833 std::fs::write(&metadata_path, metadata_content).unwrap();
834
835 let cli = Cli::parse_from(&["cqlite", "--data-dir", temp_dir.path().to_str().unwrap()]);
837
838 let mut config = Config::load(None, &cli).unwrap();
840
841 let core_config = cqlite_core::Config::default();
843 let platform = Arc::new(cqlite_core::Platform::new(&core_config).await.unwrap());
844
845 config.resolve_version(platform).await.unwrap();
846
847 let version_info = config.version_info().unwrap();
849 assert_eq!(
850 version_info.source,
851 cqlite_core::version_hints::VersionSource::DatasetMetadata
852 );
853 assert_eq!(version_info.version, Some("4.0".to_string()));
854 }
855
856 #[tokio::test]
857 async fn test_version_resolution_unknown() {
858 let temp_dir = TempDir::new().unwrap();
859
860 let cli = Cli::parse_from(&["cqlite", "--data-dir", temp_dir.path().to_str().unwrap()]);
862
863 let mut config = Config::load(None, &cli).unwrap();
865
866 let core_config = cqlite_core::Config::default();
868 let platform = Arc::new(cqlite_core::Platform::new(&core_config).await.unwrap());
869
870 config.resolve_version(platform).await.unwrap();
871
872 let version_info = config.version_info().unwrap();
874 assert_eq!(
875 version_info.source,
876 cqlite_core::version_hints::VersionSource::Unknown
877 );
878 assert_eq!(version_info.version, None);
879 assert_eq!(version_info.version_or_unknown(), "unknown");
880 }
881
882 #[tokio::test]
883 async fn test_version_precedence_user_overrides_metadata() {
884 let temp_dir = TempDir::new().unwrap();
885
886 let metadata_content = "cassandra_version: \"4.0\"\nkeyspaces: []\n";
888 let metadata_path = temp_dir.path().join("metadata.yml");
889 std::fs::write(&metadata_path, metadata_content).unwrap();
890
891 let cli = Cli::parse_from(&[
893 "cqlite",
894 "--cassandra-version",
895 "5.0",
896 "--data-dir",
897 temp_dir.path().to_str().unwrap(),
898 ]);
899
900 let mut config = Config::load(None, &cli).unwrap();
902
903 let core_config = cqlite_core::Config::default();
905 let platform = Arc::new(cqlite_core::Platform::new(&core_config).await.unwrap());
906
907 config.resolve_version(platform).await.unwrap();
908
909 let version_info = config.version_info().unwrap();
911 assert_eq!(
912 version_info.source,
913 cqlite_core::version_hints::VersionSource::UserFlag
914 );
915 assert_eq!(version_info.version, Some("5.0".to_string()));
916 }
917
918 #[tokio::test]
919 async fn test_version_string_helpers() {
920 let temp_dir = TempDir::new().unwrap();
921
922 let cli = Cli::parse_from(&[
924 "cqlite",
925 "--cassandra-version",
926 "5.0",
927 "--data-dir",
928 temp_dir.path().to_str().unwrap(),
929 ]);
930
931 let mut config = Config::load(None, &cli).unwrap();
933
934 assert_eq!(config.version_string(), "not resolved");
936 assert_eq!(config.version_source(), "not resolved");
937
938 let core_config = cqlite_core::Config::default();
940 let platform = Arc::new(cqlite_core::Platform::new(&core_config).await.unwrap());
941 config.resolve_version(platform).await.unwrap();
942
943 assert_eq!(config.version_string(), "5.0");
944 assert!(config.version_source().contains("User-provided flag"));
945 }
946
947 #[test]
948 fn test_config_default_includes_version_fields() {
949 let config = Config::default();
950 assert_eq!(config.cassandra_version, None);
951 assert_eq!(config.resolved_version, None);
952 }
953
954 #[test]
955 fn test_config_builder_preserves_version_flag() {
956 let cli = Cli::parse_from(&["cqlite", "--cassandra-version", "4.0"]);
957
958 let config = ConfigBuilder::from_defaults().with_flags(&cli).build();
959
960 assert_eq!(config.cassandra_version, Some("4.0".to_string()));
961 }
962
963 #[test]
964 #[serial]
965 fn test_env_var_replaces_config_file_schema_paths() {
966 use std::env;
967
968 env::set_var("CQLITE_SCHEMA", "/env/path1,/env/path2");
970
971 let mut config = Config::default();
973 config.schema_paths = vec![PathBuf::from("/file/path1"), PathBuf::from("/file/path2")];
974
975 let builder = ConfigBuilder { config };
977 let result = builder.with_env().unwrap();
978
979 assert_eq!(result.config.schema_paths.len(), 2);
981 assert_eq!(result.config.schema_paths[0], PathBuf::from("/env/path1"));
982 assert_eq!(result.config.schema_paths[1], PathBuf::from("/env/path2"));
983
984 env::remove_var("CQLITE_SCHEMA");
986 }
987
988 #[test]
989 #[serial]
990 fn test_env_var_single_schema_path_replaces_multiple() {
991 use std::env;
992
993 env::set_var("CQLITE_SCHEMA", "/env/single/path");
995
996 let mut config = Config::default();
998 config.schema_paths = vec![
999 PathBuf::from("/file/path1"),
1000 PathBuf::from("/file/path2"),
1001 PathBuf::from("/file/path3"),
1002 ];
1003
1004 let builder = ConfigBuilder { config };
1006 let result = builder.with_env().unwrap();
1007
1008 assert_eq!(result.config.schema_paths.len(), 1);
1010 assert_eq!(
1011 result.config.schema_paths[0],
1012 PathBuf::from("/env/single/path")
1013 );
1014
1015 env::remove_var("CQLITE_SCHEMA");
1017 }
1018
1019 #[test]
1020 #[serial]
1021 fn test_cli_flag_overrides_env_var_schema() {
1022 use std::env;
1023
1024 env::set_var("CQLITE_SCHEMA", "/env/path1,/env/path2");
1026
1027 let mut config = Config::default();
1029 config.schema_paths = vec![PathBuf::from("/file/path")];
1030
1031 let builder = ConfigBuilder { config };
1032 let result = builder.with_env().unwrap();
1033
1034 assert_eq!(result.config.schema_paths.len(), 2);
1036
1037 let cli = Cli::parse_from(&["cqlite", "--schema", "/cli/path"]);
1039 let final_config = result.with_flags(&cli).build();
1040
1041 assert_eq!(final_config.schema_paths.len(), 1);
1043 assert_eq!(final_config.schema_paths[0], PathBuf::from("/cli/path"));
1044
1045 env::remove_var("CQLITE_SCHEMA");
1047 }
1048
1049 #[test]
1050 #[serial]
1051 fn test_schema_precedence_chain_complete() {
1052 use std::env;
1053
1054 let mut config = Config::default();
1058 config.schema_paths = vec![PathBuf::from("/file/path")];
1059
1060 env::set_var("CQLITE_SCHEMA", "/env/path");
1062 let builder = ConfigBuilder { config };
1063 let with_env = builder.with_env().unwrap();
1064 assert_eq!(
1065 with_env.config.schema_paths,
1066 vec![PathBuf::from("/env/path")]
1067 );
1068
1069 let cli = Cli::parse_from(&["cqlite", "--schema", "/cli/path"]);
1071 let final_config = with_env.with_flags(&cli).build();
1072 assert_eq!(final_config.schema_paths, vec![PathBuf::from("/cli/path")]);
1073
1074 env::remove_var("CQLITE_SCHEMA");
1076 }
1077
1078 #[test]
1079 #[serial]
1080 fn test_no_env_var_preserves_file_schema() {
1081 use std::env;
1082
1083 env::remove_var("CQLITE_SCHEMA");
1085
1086 let mut config = Config::default();
1088 config.schema_paths = vec![PathBuf::from("/file/path1"), PathBuf::from("/file/path2")];
1089
1090 let builder = ConfigBuilder { config };
1092 let result = builder.with_env().unwrap();
1093
1094 assert_eq!(result.config.schema_paths.len(), 2);
1096 assert_eq!(result.config.schema_paths[0], PathBuf::from("/file/path1"));
1097 assert_eq!(result.config.schema_paths[1], PathBuf::from("/file/path2"));
1098 }
1099
1100 #[test]
1101 #[serial]
1102 fn test_env_var_with_whitespace_trimming() {
1103 use std::env;
1104
1105 env::set_var("CQLITE_SCHEMA", " /path1 , /path2 , /path3 ");
1107
1108 let config = Config::default();
1110
1111 let builder = ConfigBuilder { config };
1113 let result = builder.with_env().unwrap();
1114
1115 assert_eq!(result.config.schema_paths.len(), 3);
1117 assert_eq!(result.config.schema_paths[0], PathBuf::from("/path1"));
1118 assert_eq!(result.config.schema_paths[1], PathBuf::from("/path2"));
1119 assert_eq!(result.config.schema_paths[2], PathBuf::from("/path3"));
1120
1121 env::remove_var("CQLITE_SCHEMA");
1123 }
1124
1125 #[test]
1128 #[serial]
1129 fn test_output_config_uses_defaults_when_no_flags_or_env() {
1130 use std::env;
1131
1132 env::remove_var("CQLITE_LIMIT");
1134 env::remove_var("CQLITE_PAGE_SIZE");
1135 env::remove_var("CQLITE_NO_COLOR");
1136
1137 let cli = Cli::parse_from(&["cqlite"]);
1139
1140 let config = Config::load(None, &cli).unwrap();
1142
1143 let output = OutputConfig::from_cli(&config, false, None, None, None, false);
1145
1146 assert!(output.color_enabled); assert_eq!(output.limit, None); assert_eq!(output.page_size, Some(50)); }
1151
1152 #[test]
1153 #[serial]
1154 fn test_output_config_env_vars_override_defaults() {
1155 use std::env;
1156
1157 env::set_var("CQLITE_LIMIT", "100");
1159 env::set_var("CQLITE_PAGE_SIZE", "25");
1160 env::set_var("CQLITE_NO_COLOR", "true");
1161
1162 let cli = Cli::parse_from(&["cqlite"]);
1164
1165 let config = Config::load(None, &cli).unwrap();
1167
1168 let output = OutputConfig::from_cli(&config, false, None, None, None, false);
1170
1171 assert!(!output.color_enabled); assert_eq!(output.limit, Some(100)); assert_eq!(output.page_size, Some(25)); env::remove_var("CQLITE_LIMIT");
1178 env::remove_var("CQLITE_PAGE_SIZE");
1179 env::remove_var("CQLITE_NO_COLOR");
1180 }
1181
1182 #[test]
1183 #[serial]
1184 fn test_output_config_cli_flags_override_env_vars() {
1185 use std::env;
1186
1187 env::set_var("CQLITE_LIMIT", "100");
1189 env::set_var("CQLITE_PAGE_SIZE", "25");
1190 env::set_var("CQLITE_NO_COLOR", "false");
1191
1192 let cli = Cli::parse_from(&[
1194 "cqlite",
1195 "--limit",
1196 "200",
1197 "--page-size",
1198 "10",
1199 "--no-color",
1200 ]);
1201
1202 let config = Config::load(None, &cli).unwrap();
1204
1205 let output = OutputConfig::from_cli(
1207 &config,
1208 cli.no_color,
1209 cli.limit,
1210 cli.page_size,
1211 cli.output.clone(),
1212 cli.overwrite,
1213 );
1214
1215 assert!(!output.color_enabled); assert_eq!(output.limit, Some(200)); assert_eq!(output.page_size, Some(10)); env::remove_var("CQLITE_LIMIT");
1222 env::remove_var("CQLITE_PAGE_SIZE");
1223 env::remove_var("CQLITE_NO_COLOR");
1224 }
1225
1226 #[test]
1227 #[serial]
1228 fn test_output_config_partial_cli_flags_preserve_env_vars() {
1229 use std::env;
1230
1231 env::set_var("CQLITE_LIMIT", "100");
1233 env::set_var("CQLITE_PAGE_SIZE", "25");
1234
1235 let cli = Cli::parse_from(&["cqlite", "--no-color"]);
1237
1238 let config = Config::load(None, &cli).unwrap();
1240
1241 let output = OutputConfig::from_cli(
1243 &config,
1244 cli.no_color,
1245 cli.limit,
1246 cli.page_size,
1247 cli.output.clone(),
1248 cli.overwrite,
1249 );
1250
1251 assert!(!output.color_enabled); assert_eq!(output.limit, Some(100)); assert_eq!(output.page_size, Some(25)); env::remove_var("CQLITE_LIMIT");
1258 env::remove_var("CQLITE_PAGE_SIZE");
1259 }
1260
1261 #[test]
1262 #[serial]
1263 fn test_output_config_no_color_flag_false_preserves_env() {
1264 use std::env;
1265
1266 env::set_var("CQLITE_NO_COLOR", "true");
1268
1269 let cli = Cli::parse_from(&["cqlite"]);
1271
1272 let config = Config::load(None, &cli).unwrap();
1274
1275 let output = OutputConfig::from_cli(&config, false, None, None, None, false);
1277
1278 assert!(!output.color_enabled); env::remove_var("CQLITE_NO_COLOR");
1283 }
1284
1285 #[test]
1286 #[serial]
1287 fn test_output_config_complete_precedence_chain() {
1288 use std::env;
1289
1290 env::remove_var("CQLITE_LIMIT");
1294 env::remove_var("CQLITE_PAGE_SIZE");
1295 env::remove_var("CQLITE_NO_COLOR");
1296
1297 let cli = Cli::parse_from(&["cqlite"]);
1298 let config = Config::load(None, &cli).unwrap();
1299 let output = OutputConfig::from_cli(&config, false, None, None, None, false);
1300
1301 assert!(output.color_enabled);
1302 assert_eq!(output.limit, None);
1303 assert_eq!(output.page_size, Some(50));
1304
1305 env::set_var("CQLITE_LIMIT", "150");
1307 env::set_var("CQLITE_PAGE_SIZE", "30");
1308 env::set_var("CQLITE_NO_COLOR", "true");
1309
1310 let cli = Cli::parse_from(&["cqlite"]);
1311 let config = Config::load(None, &cli).unwrap();
1312 let output = OutputConfig::from_cli(&config, false, None, None, None, false);
1313
1314 assert!(!output.color_enabled);
1315 assert_eq!(output.limit, Some(150));
1316 assert_eq!(output.page_size, Some(30));
1317
1318 let cli = Cli::parse_from(&["cqlite", "--limit", "300", "--page-size", "15"]);
1320 let config = Config::load(None, &cli).unwrap();
1321 let output = OutputConfig::from_cli(
1322 &config,
1323 cli.no_color,
1324 cli.limit,
1325 cli.page_size,
1326 cli.output.clone(),
1327 cli.overwrite,
1328 );
1329
1330 assert!(!output.color_enabled); assert_eq!(output.limit, Some(300)); assert_eq!(output.page_size, Some(15)); env::remove_var("CQLITE_LIMIT");
1337 env::remove_var("CQLITE_PAGE_SIZE");
1338 env::remove_var("CQLITE_NO_COLOR");
1339 }
1340}