Skip to main content

dataprof_core/
config.rs

1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use crate::errors::DataProfilerError;
6
7/// Relative weights used to aggregate assessed quality dimensions.
8///
9/// Weights do not need to sum to one: the score renormalizes them over the
10/// dimensions that were actually assessed. Values should be non-negative and
11/// at least one weight must be positive.
12#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
13#[serde(default)]
14pub struct QualityScoreWeights {
15    pub completeness: f64,
16    pub consistency: f64,
17    pub uniqueness: f64,
18    pub accuracy: f64,
19    pub timeliness: f64,
20    pub validity: f64,
21    pub precision: f64,
22}
23
24impl Default for QualityScoreWeights {
25    fn default() -> Self {
26        Self {
27            completeness: 0.25,
28            consistency: 0.20,
29            uniqueness: 0.15,
30            accuracy: 0.15,
31            timeliness: 0.10,
32            validity: 0.10,
33            precision: 0.05,
34        }
35    }
36}
37
38impl QualityScoreWeights {
39    pub fn is_default(&self) -> bool {
40        self == &Self::default()
41    }
42
43    pub fn is_valid(&self) -> bool {
44        let weights = [
45            self.completeness,
46            self.consistency,
47            self.uniqueness,
48            self.accuracy,
49            self.timeliness,
50            self.validity,
51            self.precision,
52        ];
53        weights
54            .iter()
55            .all(|weight| weight.is_finite() && *weight >= 0.0)
56            && weights.iter().any(|weight| *weight > 0.0)
57    }
58}
59
60/// Quality thresholds and score settings informed by ISO 8000/25012 concepts.
61/// These values are dataprof configuration, not ISO-mandated parameters.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct IsoQualityConfig {
64    /// Maximum acceptable null percentage for a column (default: 50%).
65    pub max_null_percentage: f64,
66    /// Threshold for reporting null value issues (default: 10%).
67    pub null_report_threshold: f64,
68    /// Minimum acceptable type consistency percentage (default: 95%).
69    pub min_type_consistency: f64,
70    /// Threshold for reporting duplicate rows (default: 5%).
71    pub duplicate_report_threshold: f64,
72    /// High cardinality warning threshold (uniqueness ratio, default: 95%).
73    pub high_cardinality_threshold: f64,
74    /// IQR multiplier for outlier detection (default: 1.5, ISO standard).
75    pub outlier_iqr_multiplier: f64,
76    /// Minimum samples required for outlier detection (default: 4).
77    pub outlier_min_samples: usize,
78    /// Maximum age in years for data to be considered fresh (default: 5 years).
79    pub max_data_age_years: f64,
80    /// Percentage threshold for reporting stale data (default: 20%).
81    pub stale_data_threshold: f64,
82    /// Dataprof-defined weights for the overall quality score.
83    #[serde(default)]
84    pub score_weights: QualityScoreWeights,
85}
86
87impl Default for IsoQualityConfig {
88    fn default() -> Self {
89        Self {
90            max_null_percentage: 50.0,
91            null_report_threshold: 10.0,
92            min_type_consistency: 95.0,
93            duplicate_report_threshold: 5.0,
94            high_cardinality_threshold: 95.0,
95            outlier_iqr_multiplier: 1.5,
96            outlier_min_samples: 4,
97            max_data_age_years: 5.0,
98            stale_data_threshold: 20.0,
99            score_weights: QualityScoreWeights::default(),
100        }
101    }
102}
103
104impl IsoQualityConfig {
105    /// Create strict thresholds for high-compliance industries (finance, healthcare).
106    pub fn strict() -> Self {
107        Self {
108            max_null_percentage: 30.0,
109            null_report_threshold: 5.0,
110            min_type_consistency: 98.0,
111            duplicate_report_threshold: 1.0,
112            high_cardinality_threshold: 98.0,
113            outlier_iqr_multiplier: 1.5,
114            outlier_min_samples: 10,
115            max_data_age_years: 2.0,
116            stale_data_threshold: 10.0,
117            score_weights: QualityScoreWeights::default(),
118        }
119    }
120
121    /// Create lenient thresholds for exploratory/marketing data.
122    pub fn lenient() -> Self {
123        Self {
124            max_null_percentage: 70.0,
125            null_report_threshold: 20.0,
126            min_type_consistency: 90.0,
127            duplicate_report_threshold: 10.0,
128            high_cardinality_threshold: 90.0,
129            outlier_iqr_multiplier: 2.0,
130            outlier_min_samples: 4,
131            max_data_age_years: 10.0,
132            stale_data_threshold: 30.0,
133            score_weights: QualityScoreWeights::default(),
134        }
135    }
136}
137
138// ============================================================================
139// Configuration Constants
140// ============================================================================
141// These constants define the default values for various configuration options.
142// Each constant is documented with its rationale to avoid "magic numbers".
143
144// Output Configuration Defaults
145/// Default output format for report serialization.
146/// "text" provides human-readable output suitable for direct display.
147const DEFAULT_OUTPUT_FORMAT: &str = "text";
148
149/// Enable colored output by default.
150/// Colors improve readability in modern terminals that support ANSI codes.
151const DEFAULT_COLORED_OUTPUT: bool = true;
152
153/// Default verbosity level (1 = normal).
154/// 0=quiet, 1=normal, 2=verbose, 3=debug
155/// Level 1 strikes a balance between informativeness and noise.
156const DEFAULT_VERBOSITY: u8 = 1;
157
158/// Show progress bars by default.
159/// Progress feedback is important for long-running operations.
160const DEFAULT_SHOW_PROGRESS: bool = true;
161
162// Quality Configuration Defaults
163/// Enable quality checking by default.
164/// Quality analysis is a core feature of DataProf.
165/// When enabled, all configured quality metrics are calculated.
166const DEFAULT_QUALITY_ENABLED: bool = true;
167
168// Engine Configuration Defaults
169/// Default engine selection: "auto".
170/// Automatic engine selection adapts to file size and format.
171const DEFAULT_ENGINE: &str = "auto";
172
173/// Enable parallel processing by default.
174/// Modern systems benefit from parallel processing for large datasets.
175const DEFAULT_PARALLEL_PROCESSING: bool = true;
176
177// Memory Configuration Defaults
178/// Maximum memory usage in MB (0 = unlimited).
179/// 0 allows the OS to manage memory, suitable for most use cases.
180const DEFAULT_MAX_MEMORY_MB: usize = 0;
181
182/// Enable memory monitoring by default.
183/// Monitoring helps detect memory leaks and optimize performance.
184const DEFAULT_MEMORY_MONITORING: bool = true;
185
186/// Auto-switch to streaming for files larger than 100MB.
187/// 100MB is a reasonable threshold where streaming provides clear benefits
188/// over loading the entire file into memory.
189const DEFAULT_AUTO_STREAMING_THRESHOLD_MB: f64 = 100.0;
190
191// Database Configuration Defaults (when database feature is enabled)
192#[cfg(feature = "database")]
193/// Default connection timeout in seconds.
194/// 30 seconds allows for slow networks while avoiding indefinite hangs.
195const DEFAULT_DB_CONNECTION_TIMEOUT_SECS: u64 = 30;
196
197#[cfg(feature = "database")]
198/// Default batch size for database queries.
199/// 10,000 rows per batch balances memory usage and query performance.
200const DEFAULT_DB_BATCH_SIZE: usize = 10_000;
201
202#[cfg(feature = "database")]
203/// Maximum number of database connections in the pool.
204/// 10 connections supports moderate concurrency without overwhelming the database.
205const DEFAULT_DB_MAX_CONNECTIONS: usize = 10;
206
207#[cfg(feature = "database")]
208/// Enable SSL by default for security.
209/// SSL should be the default for production database connections.
210const DEFAULT_DB_SSL_ENABLED: bool = true;
211
212#[cfg(feature = "database")]
213/// Enable sampling for large database tables by default.
214/// Sampling provides faster analysis for exploratory data profiling.
215const DEFAULT_DB_SAMPLING_ENABLED: bool = true;
216
217#[cfg(feature = "database")]
218/// Default sample size for database tables.
219/// 100,000 rows provides statistical significance for most analyses
220/// while keeping query times reasonable.
221const DEFAULT_DB_SAMPLE_SIZE: usize = 100_000;
222
223#[cfg(feature = "database")]
224/// Threshold for automatic sampling (number of rows).
225/// Tables with over 1 million rows benefit significantly from sampling.
226const DEFAULT_DB_AUTO_SAMPLE_THRESHOLD: usize = 1_000_000;
227
228/// Main configuration structure for dataprof.
229#[derive(Debug, Clone, Serialize, Deserialize, Default)]
230pub struct DataprofConfig {
231    /// Output configuration
232    pub output: OutputConfig,
233
234    /// Quality checking configuration
235    pub quality: QualityConfig,
236
237    /// Engine selection and performance tuning
238    pub engine: EngineConfig,
239
240    /// Database configuration
241    #[cfg(feature = "database")]
242    pub database: Option<DatabaseSettings>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct OutputConfig {
247    /// Default output format (text, json, csv, yaml, plain)
248    pub default_format: String,
249
250    /// Enable colored output by default
251    pub colored: bool,
252
253    /// Default verbosity level (0=quiet, 1=normal, 2=verbose, 3=debug)
254    pub verbosity: u8,
255
256    /// Show progress bars by default
257    pub show_progress: bool,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct QualityConfig {
262    /// Enable quality checking by default
263    pub enabled: bool,
264
265    /// ISO 8000/25012 compliant thresholds
266    ///
267    /// All quality metrics (null detection, duplicate detection, type consistency,
268    /// date format checking, outlier detection, etc.) are controlled through
269    /// these ISO-compliant thresholds.
270    ///
271    /// Use `IsoQualityConfig::strict()` for high-compliance industries,
272    /// `IsoQualityConfig::lenient()` for exploratory data, or customize
273    /// individual thresholds as needed.
274    pub iso_thresholds: IsoQualityConfig,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct EngineConfig {
279    /// Default engine selection (auto, streaming, memory_efficient, etc.)
280    pub default_engine: String,
281
282    /// Default chunk size for streaming operations
283    pub default_chunk_size: Option<usize>,
284
285    /// Enable parallel processing by default
286    pub parallel: bool,
287
288    /// Maximum concurrent operations
289    pub max_concurrent: usize,
290
291    /// Memory usage limits
292    pub memory: MemoryConfig,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct MemoryConfig {
297    /// Maximum memory usage in MB (0 = unlimited)
298    pub max_usage_mb: usize,
299
300    /// Enable memory monitoring
301    pub monitor: bool,
302
303    /// Auto-switch to streaming for large files
304    pub auto_streaming_threshold_mb: f64,
305}
306
307#[cfg(feature = "database")]
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct DatabaseSettings {
310    /// Default connection timeout in seconds
311    pub connection_timeout: u64,
312
313    /// Default batch size for database queries
314    pub batch_size: usize,
315
316    /// Maximum number of database connections
317    pub max_connections: usize,
318
319    /// Enable SSL by default
320    pub ssl_enabled: bool,
321
322    /// Default sampling configuration
323    pub sampling: DatabaseSamplingConfig,
324}
325
326#[cfg(feature = "database")]
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct DatabaseSamplingConfig {
329    /// Enable sampling for large tables
330    pub enabled: bool,
331
332    /// Default sample size
333    pub default_sample_size: usize,
334
335    /// Threshold for automatic sampling (number of rows)
336    pub auto_sample_threshold: usize,
337}
338
339impl Default for OutputConfig {
340    fn default() -> Self {
341        Self {
342            default_format: DEFAULT_OUTPUT_FORMAT.to_string(),
343            colored: DEFAULT_COLORED_OUTPUT,
344            verbosity: DEFAULT_VERBOSITY,
345            show_progress: DEFAULT_SHOW_PROGRESS,
346        }
347    }
348}
349
350impl Default for QualityConfig {
351    fn default() -> Self {
352        Self {
353            enabled: DEFAULT_QUALITY_ENABLED,
354            iso_thresholds: IsoQualityConfig::default(),
355        }
356    }
357}
358
359impl Default for EngineConfig {
360    fn default() -> Self {
361        Self {
362            default_engine: DEFAULT_ENGINE.to_string(),
363            default_chunk_size: None,
364            parallel: DEFAULT_PARALLEL_PROCESSING,
365            max_concurrent: num_cpus::get(),
366            memory: MemoryConfig::default(),
367        }
368    }
369}
370
371impl Default for MemoryConfig {
372    fn default() -> Self {
373        Self {
374            max_usage_mb: DEFAULT_MAX_MEMORY_MB,
375            monitor: DEFAULT_MEMORY_MONITORING,
376            auto_streaming_threshold_mb: DEFAULT_AUTO_STREAMING_THRESHOLD_MB,
377        }
378    }
379}
380
381#[cfg(feature = "database")]
382impl Default for DatabaseSettings {
383    fn default() -> Self {
384        Self {
385            connection_timeout: DEFAULT_DB_CONNECTION_TIMEOUT_SECS,
386            batch_size: DEFAULT_DB_BATCH_SIZE,
387            max_connections: DEFAULT_DB_MAX_CONNECTIONS,
388            ssl_enabled: DEFAULT_DB_SSL_ENABLED,
389            sampling: DatabaseSamplingConfig::default(),
390        }
391    }
392}
393
394#[cfg(feature = "database")]
395impl Default for DatabaseSamplingConfig {
396    fn default() -> Self {
397        Self {
398            enabled: DEFAULT_DB_SAMPLING_ENABLED,
399            default_sample_size: DEFAULT_DB_SAMPLE_SIZE,
400            auto_sample_threshold: DEFAULT_DB_AUTO_SAMPLE_THRESHOLD,
401        }
402    }
403}
404
405impl DataprofConfig {
406    /// Load configuration from file, with fallback to default
407    pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, DataProfilerError> {
408        let content = fs::read_to_string(path)?;
409        let config: DataprofConfig = toml::from_str(&content)?;
410        Ok(config)
411    }
412
413    /// Save configuration to file
414    pub fn save_to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), DataProfilerError> {
415        let content = toml::to_string_pretty(self)?;
416        fs::write(path, content)?;
417        Ok(())
418    }
419
420    /// Load configuration with automatic file discovery.
421    ///
422    /// Searches for configuration files in the following order:
423    /// 1. `.dataprof.toml` in current directory
424    /// 2. `$HOME/.config/dataprof/config.toml` (Linux/macOS)
425    /// 3. `%USERPROFILE%\.config\dataprof\config.toml` (Windows)
426    /// 4. `dataprof.toml` in current directory
427    ///
428    /// If no config file is found, returns default configuration with
429    /// environment variable overrides applied.
430    pub fn load_with_discovery() -> Self {
431        // Build list of config paths to try
432        let mut config_paths = vec![
433            // Current directory - highest priority
434            PathBuf::from(".dataprof.toml"),
435            PathBuf::from("dataprof.toml"),
436        ];
437
438        // User config directory - cross-platform
439        if let Some(home) = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
440            let user_config = PathBuf::from(home)
441                .join(".config")
442                .join("dataprof")
443                .join("config.toml");
444            config_paths.push(user_config);
445        }
446
447        // Try loading from each path
448        for path in &config_paths {
449            if path.exists() {
450                match Self::load_from_file(path) {
451                    Ok(mut config) => {
452                        log::info!("Loaded configuration from: {}", path.display());
453                        // Apply environment overrides on top of file config
454                        config.apply_env_overrides();
455                        return config;
456                    }
457                    Err(e) => {
458                        log::warn!(
459                            "Found config file at {} but failed to load: {}",
460                            path.display(),
461                            e
462                        );
463                    }
464                }
465            }
466        }
467
468        // No config file found - use defaults
469        log::debug!("No configuration file found. Using defaults with environment overrides.");
470        let mut config = Self::default();
471        config.apply_env_overrides();
472        config
473    }
474
475    /// Apply environment variable overrides
476    pub fn apply_env_overrides(&mut self) {
477        // Output format override
478        if let Ok(format) = std::env::var("DATAPROF_FORMAT") {
479            self.output.default_format = format;
480        }
481
482        // Verbosity override
483        if let Ok(verbosity) = std::env::var("DATAPROF_VERBOSITY")
484            && let Ok(level) = verbosity.parse::<u8>()
485        {
486            self.output.verbosity = level;
487        }
488
489        // Engine override
490        if let Ok(engine) = std::env::var("DATAPROF_ENGINE") {
491            self.engine.default_engine = engine;
492        }
493
494        // Quality checking override
495        if let Ok(quality) = std::env::var("DATAPROF_QUALITY")
496            && let Ok(enabled) = quality.parse::<bool>()
497        {
498            self.quality.enabled = enabled;
499        }
500
501        // Disable colors if NO_COLOR is set
502        if std::env::var("NO_COLOR").is_ok() {
503            self.output.colored = false;
504        }
505
506        // Progress override
507        if let Ok(progress) = std::env::var("DATAPROF_PROGRESS")
508            && let Ok(enabled) = progress.parse::<bool>()
509        {
510            self.output.show_progress = enabled;
511        }
512    }
513
514    /// Apply explicit runtime overrides to configuration.
515    pub fn apply_overrides(
516        &mut self,
517        format: Option<&str>,
518        quality: Option<bool>,
519        progress: Option<bool>,
520    ) {
521        if let Some(format) = format {
522            self.output.default_format = format.to_string();
523        }
524
525        if let Some(quality) = quality {
526            self.quality.enabled = quality;
527        }
528
529        if let Some(progress) = progress {
530            self.output.show_progress = progress;
531        }
532    }
533
534    /// Deprecated compatibility wrapper for the old CLI-oriented name.
535    #[deprecated(note = "use DataprofConfig::apply_overrides instead")]
536    pub fn merge_with_cli_args(
537        &mut self,
538        format: Option<&str>,
539        quality: Option<bool>,
540        progress: Option<bool>,
541    ) {
542        self.apply_overrides(format, quality, progress);
543    }
544
545    /// Create a sample configuration file for users
546    pub fn create_sample_config<P: AsRef<Path>>(path: P) -> Result<(), DataProfilerError> {
547        let sample_config = Self::default();
548        sample_config.save_to_file(path)?;
549        Ok(())
550    }
551}
552
553/// Configuration validation with comprehensive error messages
554impl DataprofConfig {
555    /// Validate the configuration and return detailed error messages.
556    ///
557    /// This method checks:
558    /// - Output format validity
559    /// - Verbosity level range
560    /// - Quality threshold ranges
561    /// - ISO threshold consistency
562    /// - Engine validity
563    /// - Memory configuration sanity
564    /// - Chunk size validity
565    pub fn validate(&self) -> Result<(), DataProfilerError> {
566        // Validate output format
567        let valid_formats = ["text", "json", "csv", "plain"];
568        if !valid_formats.contains(&self.output.default_format.as_str()) {
569            return Err(DataProfilerError::ConfigValidationError {
570                message: format!(
571                    "Invalid output format '{}'. Valid formats: {}\n\
572                     → Fix: Set output.default_format to one of the valid formats in your config file.",
573                    self.output.default_format,
574                    valid_formats.join(", ")
575                ),
576            });
577        }
578
579        // Validate verbosity level
580        if self.output.verbosity > 3 {
581            return Err(DataProfilerError::ConfigValidationError {
582                message: format!(
583                    "Invalid verbosity level {}. Must be between 0 (quiet) and 3 (debug).\n\
584                     → Fix: Set output.verbosity to 0, 1, 2, or 3 in your config file.",
585                    self.output.verbosity
586                ),
587            });
588        }
589
590        // Validate ISO thresholds
591        let iso = &self.quality.iso_thresholds;
592
593        if iso.outlier_iqr_multiplier <= 0.0 {
594            return Err(DataProfilerError::ConfigValidationError {
595                message: format!(
596                    "IQR multiplier must be positive (standard value: 1.5), got {}.\n\
597                     → Fix: Set quality.iso_thresholds.outlier_iqr_multiplier to a positive value.\n\
598                     → Recommended: Use 1.5 (ISO standard) for normal cases, 3.0 for lenient detection.",
599                    iso.outlier_iqr_multiplier
600                ),
601            });
602        }
603
604        if iso.max_null_percentage < 0.0 || iso.max_null_percentage > 100.0 {
605            return Err(DataProfilerError::ConfigValidationError {
606                message: format!(
607                    "Max null percentage must be between 0 and 100, got {}.\n\
608                     → Fix: Set quality.iso_thresholds.max_null_percentage to a value between 0.0 and 100.0.",
609                    iso.max_null_percentage
610                ),
611            });
612        }
613
614        if iso.null_report_threshold < 0.0 || iso.null_report_threshold > 100.0 {
615            return Err(DataProfilerError::ConfigValidationError {
616                message: format!(
617                    "Null report threshold must be between 0 and 100, got {}.\n\
618                     → Fix: Set quality.iso_thresholds.null_report_threshold to a value between 0.0 and 100.0.",
619                    iso.null_report_threshold
620                ),
621            });
622        }
623
624        if iso.min_type_consistency < 0.0 || iso.min_type_consistency > 100.0 {
625            return Err(DataProfilerError::ConfigValidationError {
626                message: format!(
627                    "Min type consistency must be between 0 and 100, got {}.\n\
628                     → Fix: Set quality.iso_thresholds.min_type_consistency to a value between 0.0 and 100.0.",
629                    iso.min_type_consistency
630                ),
631            });
632        }
633
634        if iso.high_cardinality_threshold < 0.0 || iso.high_cardinality_threshold > 100.0 {
635            return Err(DataProfilerError::ConfigValidationError {
636                message: format!(
637                    "High cardinality threshold must be between 0 and 100, got {}.\n\
638                     → Fix: Set quality.iso_thresholds.high_cardinality_threshold to a value between 0.0 and 100.0.",
639                    iso.high_cardinality_threshold
640                ),
641            });
642        }
643
644        if iso.duplicate_report_threshold < 0.0 || iso.duplicate_report_threshold > 100.0 {
645            return Err(DataProfilerError::ConfigValidationError {
646                message: format!(
647                    "Duplicate report threshold must be between 0 and 100, got {}.\n\
648                     → Fix: Set quality.iso_thresholds.duplicate_report_threshold to a value between 0.0 and 100.0.",
649                    iso.duplicate_report_threshold
650                ),
651            });
652        }
653
654        if iso.max_data_age_years < 0.0 {
655            return Err(DataProfilerError::ConfigValidationError {
656                message: format!(
657                    "Max data age must be non-negative, got {} years.\n\
658                     → Fix: Set quality.iso_thresholds.max_data_age_years to a positive value.",
659                    iso.max_data_age_years
660                ),
661            });
662        }
663
664        if iso.stale_data_threshold < 0.0 || iso.stale_data_threshold > 100.0 {
665            return Err(DataProfilerError::ConfigValidationError {
666                message: format!(
667                    "Stale data threshold must be between 0 and 100, got {}.\n\
668                     → Fix: Set quality.iso_thresholds.stale_data_threshold to a value between 0.0 and 100.0.",
669                    iso.stale_data_threshold
670                ),
671            });
672        }
673
674        if !iso.score_weights.is_valid() {
675            return Err(DataProfilerError::ConfigValidationError {
676                message: "Quality score weights must be finite and non-negative, with at least one positive weight.\n\
677                     → Fix: Set quality.iso_thresholds.score_weights to valid relative weights."
678                    .to_string(),
679            });
680        }
681
682        // Validate engine
683        let valid_engines = ["auto", "streaming", "memory_efficient", "true_streaming"];
684        if !valid_engines.contains(&self.engine.default_engine.as_str()) {
685            return Err(DataProfilerError::ConfigValidationError {
686                message: format!(
687                    "Invalid engine '{}'. Valid engines: {}\n\
688                     → Fix: Set engine.default_engine to one of the valid engines in your config file.\n\
689                     → Recommended: Use 'auto' for automatic selection based on file size.",
690                    self.engine.default_engine,
691                    valid_engines.join(", ")
692                ),
693            });
694        }
695
696        // Validate memory configuration
697        if self.engine.memory.auto_streaming_threshold_mb < 0.0 {
698            return Err(DataProfilerError::ConfigValidationError {
699                message: format!(
700                    "Auto-streaming threshold must be non-negative, got {} MB.\n\
701                     → Fix: Set engine.memory.auto_streaming_threshold_mb to a positive value.\n\
702                     → Recommended: 100.0 MB is a good default for most systems.",
703                    self.engine.memory.auto_streaming_threshold_mb
704                ),
705            });
706        }
707
708        // Validate chunk size if specified
709        if let Some(chunk_size) = self.engine.default_chunk_size {
710            if chunk_size == 0 {
711                return Err(DataProfilerError::ConfigValidationError {
712                    message: format!(
713                        "Chunk size must be greater than 0, got {}.\n\
714                         → Fix: Set engine.default_chunk_size to a positive value or null for adaptive sizing.\n\
715                         → Recommended: 8192-65536 rows for most CSV files.",
716                        chunk_size
717                    ),
718                });
719            }
720
721            if chunk_size > 1_000_000 {
722                return Err(DataProfilerError::ConfigValidationError {
723                    message: format!(
724                        "Chunk size {} is very large and may cause memory issues.\n\
725                         → Fix: Set engine.default_chunk_size to a smaller value.\n\
726                         → Recommended: 8192-65536 rows for most CSV files.",
727                        chunk_size
728                    ),
729                });
730            }
731        }
732
733        // Validate max concurrent operations
734        if self.engine.max_concurrent == 0 {
735            return Err(DataProfilerError::ConfigValidationError {
736                message: "Max concurrent operations must be greater than 0.\n\
737                     → Fix: Set engine.max_concurrent to a positive value.\n\
738                     → Recommended: Use num_cpus::get() or leave unspecified for automatic detection."
739                    .to_string(),
740            });
741        }
742
743        // Validate database settings if present
744        #[cfg(feature = "database")]
745        if let Some(ref db) = self.database {
746            if db.connection_timeout == 0 {
747                return Err(DataProfilerError::ConfigValidationError {
748                    message: "Database connection timeout must be greater than 0 seconds.\n\
749                         → Fix: Set database.connection_timeout to a positive value.\n\
750                         → Recommended: 30 seconds for most network conditions."
751                        .to_string(),
752                });
753            }
754
755            if db.batch_size == 0 {
756                return Err(DataProfilerError::ConfigValidationError {
757                    message: "Database batch size must be greater than 0.\n\
758                         → Fix: Set database.batch_size to a positive value.\n\
759                         → Recommended: 10000 rows for most databases."
760                        .to_string(),
761                });
762            }
763
764            if db.max_connections == 0 {
765                return Err(DataProfilerError::ConfigValidationError {
766                    message: "Database max connections must be greater than 0.\n\
767                         → Fix: Set database.max_connections to a positive value.\n\
768                         → Recommended: 10 connections for most use cases."
769                        .to_string(),
770                });
771            }
772
773            if db.sampling.default_sample_size == 0 {
774                return Err(DataProfilerError::ConfigValidationError {
775                    message: "Database sample size must be greater than 0.\n\
776                         → Fix: Set database.sampling.default_sample_size to a positive value.\n\
777                         → Recommended: 100000 rows for statistical significance."
778                        .to_string(),
779                });
780            }
781        }
782
783        Ok(())
784    }
785}
786
787// ============================================================================
788// Builder Pattern Implementation
789// ============================================================================
790
791/// Builder for constructing DataprofConfig with a fluent API.
792///
793/// The builder pattern provides:
794/// - Clear, self-documenting configuration
795/// - Type-safe construction
796/// - Validation at build time
797/// - Easy preset configurations
798///
799/// # Examples
800///
801/// ```
802/// use dataprof_core::config::{DataprofConfigBuilder, IsoQualityConfig};
803///
804/// // Simple configuration with defaults
805/// let config = DataprofConfigBuilder::new()
806///     .build()
807///     .expect("Failed to build config");
808///
809/// // Configuration with custom settings
810/// let config = DataprofConfigBuilder::new()
811///     .output_format("json")
812///     .verbosity(2)
813///     .engine("streaming")
814///     .build()
815///     .expect("Failed to build config");
816///
817/// // Strict quality profile for finance/healthcare
818/// let config = DataprofConfigBuilder::new()
819///     .iso_quality_profile_strict()
820///     .build()
821///     .expect("Failed to build config");
822/// ```
823#[derive(Debug, Clone)]
824pub struct DataprofConfigBuilder {
825    output: OutputConfig,
826    quality: QualityConfig,
827    engine: EngineConfig,
828    #[cfg(feature = "database")]
829    database: Option<DatabaseSettings>,
830}
831
832impl DataprofConfigBuilder {
833    /// Create a new builder with default values.
834    pub fn new() -> Self {
835        Self {
836            output: OutputConfig::default(),
837            quality: QualityConfig::default(),
838            engine: EngineConfig::default(),
839            #[cfg(feature = "database")]
840            database: Some(DatabaseSettings::default()),
841        }
842    }
843
844    // ========================================================================
845    // Output Configuration Methods
846    // ========================================================================
847
848    /// Set the default output format.
849    ///
850    /// Valid formats: "text", "json", "csv", "plain"
851    pub fn output_format(mut self, format: &str) -> Self {
852        self.output.default_format = format.to_string();
853        self
854    }
855
856    /// Enable or disable colored output.
857    pub fn colored(mut self, enabled: bool) -> Self {
858        self.output.colored = enabled;
859        self
860    }
861
862    /// Set verbosity level (0=quiet, 1=normal, 2=verbose, 3=debug).
863    pub fn verbosity(mut self, level: u8) -> Self {
864        self.output.verbosity = level;
865        self
866    }
867
868    /// Enable or disable progress bars.
869    pub fn show_progress(mut self, enabled: bool) -> Self {
870        self.output.show_progress = enabled;
871        self
872    }
873
874    // ========================================================================
875    // Quality Configuration Methods
876    // ========================================================================
877
878    /// Enable or disable quality checking.
879    ///
880    /// When enabled, all quality dimensions are calculated:
881    /// - Completeness (null detection)
882    /// - Consistency (type consistency, mixed types)
883    /// - Uniqueness (duplicate detection, high cardinality)
884    /// - Accuracy (outlier detection)
885    /// - Timeliness (stale data detection)
886    /// - Validity (semantic pattern conformance)
887    /// - Precision (effective decimal-scale consistency)
888    pub fn quality_enabled(mut self, enabled: bool) -> Self {
889        self.quality.enabled = enabled;
890        self
891    }
892
893    /// Set custom quality thresholds and score settings.
894    pub fn iso_quality_thresholds(mut self, thresholds: IsoQualityConfig) -> Self {
895        self.quality.iso_thresholds = thresholds;
896        self
897    }
898
899    /// Set relative weights for the overall quality score.
900    pub fn quality_score_weights(mut self, weights: QualityScoreWeights) -> Self {
901        self.quality.iso_thresholds.score_weights = weights;
902        self
903    }
904
905    /// Use strict ISO quality thresholds (finance, healthcare).
906    ///
907    /// Stricter thresholds for high-compliance industries:
908    /// - Lower null tolerance (30% vs 50%)
909    /// - Higher type consistency requirements (98% vs 95%)
910    /// - Stricter duplicate detection (1% vs 5%)
911    /// - More recent data requirements (2 years vs 5 years)
912    pub fn iso_quality_profile_strict(mut self) -> Self {
913        self.quality.iso_thresholds = IsoQualityConfig::strict();
914        self
915    }
916
917    /// Use lenient ISO quality thresholds (exploratory, marketing).
918    ///
919    /// More relaxed thresholds for exploratory data:
920    /// - Higher null tolerance (70% vs 50%)
921    /// - Lower type consistency requirements (90% vs 95%)
922    /// - More lenient duplicate detection (10% vs 5%)
923    /// - Older data accepted (10 years vs 5 years)
924    pub fn iso_quality_profile_lenient(mut self) -> Self {
925        self.quality.iso_thresholds = IsoQualityConfig::lenient();
926        self
927    }
928
929    // ========================================================================
930    // Engine Configuration Methods
931    // ========================================================================
932
933    /// Set the default engine selection.
934    ///
935    /// Valid engines: "auto", "streaming", "memory_efficient", "true_streaming"
936    pub fn engine(mut self, engine: &str) -> Self {
937        self.engine.default_engine = engine.to_string();
938        self
939    }
940
941    /// Set default chunk size for streaming operations.
942    pub fn chunk_size(mut self, size: usize) -> Self {
943        self.engine.default_chunk_size = Some(size);
944        self
945    }
946
947    /// Enable or disable parallel processing.
948    pub fn parallel(mut self, enabled: bool) -> Self {
949        self.engine.parallel = enabled;
950        self
951    }
952
953    /// Set maximum concurrent operations.
954    pub fn max_concurrent(mut self, max: usize) -> Self {
955        self.engine.max_concurrent = max;
956        self
957    }
958
959    /// Set maximum memory usage in MB (0 = unlimited).
960    pub fn max_memory_mb(mut self, mb: usize) -> Self {
961        self.engine.memory.max_usage_mb = mb;
962        self
963    }
964
965    /// Set auto-streaming threshold in MB.
966    pub fn auto_streaming_threshold_mb(mut self, mb: f64) -> Self {
967        self.engine.memory.auto_streaming_threshold_mb = mb;
968        self
969    }
970
971    // ========================================================================
972    // Database Configuration Methods (optional feature)
973    // ========================================================================
974
975    #[cfg(feature = "database")]
976    /// Set database connection timeout in seconds.
977    pub fn db_connection_timeout(mut self, seconds: u64) -> Self {
978        if let Some(ref mut db) = self.database {
979            db.connection_timeout = seconds;
980        }
981        self
982    }
983
984    #[cfg(feature = "database")]
985    /// Set database batch size for queries.
986    pub fn db_batch_size(mut self, size: usize) -> Self {
987        if let Some(ref mut db) = self.database {
988            db.batch_size = size;
989        }
990        self
991    }
992
993    #[cfg(feature = "database")]
994    /// Enable or disable database sampling.
995    pub fn db_sampling_enabled(mut self, enabled: bool) -> Self {
996        if let Some(ref mut db) = self.database {
997            db.sampling.enabled = enabled;
998        }
999        self
1000    }
1001
1002    // ========================================================================
1003    // Preset Configurations
1004    // ========================================================================
1005
1006    /// Create a configuration optimized for CI/CD pipelines.
1007    ///
1008    /// - No colors (for log compatibility)
1009    /// - No progress bars (cleaner output)
1010    /// - JSON output format
1011    /// - Verbose logging
1012    pub fn ci_preset() -> Self {
1013        Self::new()
1014            .colored(false)
1015            .show_progress(false)
1016            .output_format("json")
1017            .verbosity(2)
1018    }
1019
1020    /// Create a configuration for interactive terminal use.
1021    ///
1022    /// - Colors enabled
1023    /// - Progress bars enabled
1024    /// - Text output format
1025    /// - Normal verbosity
1026    pub fn interactive_preset() -> Self {
1027        Self::new()
1028            .colored(true)
1029            .show_progress(true)
1030            .output_format("text")
1031            .verbosity(1)
1032    }
1033
1034    /// Create a configuration for production quality checks.
1035    ///
1036    /// - Strict ISO quality thresholds
1037    /// - Quality checking enabled
1038    /// - Memory monitoring enabled
1039    /// - Conservative memory limits
1040    pub fn production_quality_preset() -> Self {
1041        Self::new()
1042            .iso_quality_profile_strict()
1043            .quality_enabled(true)
1044            .max_memory_mb(512) // 512MB limit for predictability
1045    }
1046
1047    // ========================================================================
1048    // Build and Validation
1049    // ========================================================================
1050
1051    /// Build the configuration and validate it.
1052    ///
1053    /// Returns an error if the configuration is invalid.
1054    pub fn build(self) -> Result<DataprofConfig, DataProfilerError> {
1055        let config = DataprofConfig {
1056            output: self.output,
1057            quality: self.quality,
1058            engine: self.engine,
1059            #[cfg(feature = "database")]
1060            database: self.database,
1061        };
1062
1063        // Validate the configuration before returning
1064        config.validate()?;
1065
1066        Ok(config)
1067    }
1068
1069    /// Build the configuration without validation.
1070    ///
1071    /// Use this only if you're certain the configuration is valid
1072    /// and want to skip validation for performance reasons.
1073    pub fn build_unchecked(self) -> DataprofConfig {
1074        DataprofConfig {
1075            output: self.output,
1076            quality: self.quality,
1077            engine: self.engine,
1078            #[cfg(feature = "database")]
1079            database: self.database,
1080        }
1081    }
1082}
1083
1084impl Default for DataprofConfigBuilder {
1085    fn default() -> Self {
1086        Self::new()
1087    }
1088}
1089
1090#[cfg(test)]
1091mod tests {
1092    use super::*;
1093
1094    #[test]
1095    fn legacy_quality_config_defaults_score_weights() {
1096        let mut value = serde_json::to_value(IsoQualityConfig::default()).unwrap();
1097        value
1098            .as_object_mut()
1099            .expect("quality config object")
1100            .remove("score_weights");
1101
1102        let restored: IsoQualityConfig = serde_json::from_value(value).unwrap();
1103        assert_eq!(restored.score_weights, QualityScoreWeights::default());
1104    }
1105
1106    #[test]
1107    fn five_dimension_weights_gain_new_dimension_defaults() {
1108        let weights: QualityScoreWeights = serde_json::from_str(
1109            r#"{
1110                "completeness": 0.30,
1111                "consistency": 0.25,
1112                "uniqueness": 0.20,
1113                "accuracy": 0.15,
1114                "timeliness": 0.10
1115            }"#,
1116        )
1117        .unwrap();
1118
1119        assert_eq!(weights.validity, 0.10);
1120        assert_eq!(weights.precision, 0.05);
1121    }
1122
1123    #[test]
1124    fn config_validation_rejects_invalid_score_weights() {
1125        let weights = QualityScoreWeights {
1126            completeness: 0.0,
1127            consistency: 0.0,
1128            uniqueness: 0.0,
1129            accuracy: 0.0,
1130            timeliness: 0.0,
1131            validity: 0.0,
1132            precision: 0.0,
1133        };
1134        let config = DataprofConfigBuilder::new()
1135            .quality_score_weights(weights)
1136            .build();
1137
1138        assert!(config.is_err());
1139    }
1140}