Skip to main content

polars_view/
data_filter.rs

1use crate::{
2    Arguments, DEFAULT_OVERRIDE_REGEX, DEFAULT_QUERY, FileExtension, PathExtension,
3    PolarsViewError, PolarsViewResult, UniqueElements, sql_commands,
4};
5use egui::{
6    Align, CollapsingHeader, Color32, DragValue, Frame, Grid, Layout, Stroke, TextEdit, Ui, Vec2,
7};
8use polars::{io::RowIndex, prelude::*};
9use regex::Regex;
10use tokio::task::spawn_blocking;
11
12use std::{
13    fmt::Debug,
14    fs::File,
15    num::NonZero,
16    path::{Path, PathBuf},
17    sync::Arc,
18};
19
20// --- Constants ---
21
22/// Static string listing common values treated as null/missing during CSV parsing.
23/// The `r#""#` syntax denotes a raw string literal, avoiding the need to escape quotes.
24pub static NULL_VALUES: &str = r#""", <N/D>"#;
25
26/// Default delimiter used for CSV parsing if not specified or detected.
27/// Using `&'static str` for common, immutable delimiters saves memory allocation.
28pub static DEFAULT_CSV_DELIMITER: &str = ";";
29
30/// Default name for the row number column if added.
31pub const DEFAULT_INDEX_COLUMN_NAME: &str = "Row Number";
32
33/// Default regex
34const DEFAULT_NORM_REGEX: &str = "^Val.*$";
35
36/// Default drop regex
37const DEFAULT_DROP_REGEX: &str = "^Temp.*$";
38
39/// Default starting offset for the row index column (e.g., 1 for 1-based).
40const DEFAULT_INDEX_COLUMN_OFFSET: u32 = 1;
41
42const DEFAULT_INFER_SCHEMA_ROWS: usize = 200;
43
44// Prevent potential infinite loops (e.g., schema keeps changing).
45pub const MAX_ATTEMPTS: u32 = 1000;
46
47// --- DataFilter Struct ---
48
49/// Holds configuration parameters related to **loading and querying** data.
50///
51/// This struct focuses on settings that define how data is initially read from a file
52/// and transformed via SQL queries or basic processing like null column removal.
53///
54/// Instances are created from `Arguments`, updated by the UI in `render_query`, and passed
55/// to `DataFrameContainer::load_data`. Changes here typically trigger a data reload/requery.
56#[derive(Debug, Clone, PartialEq)] // PartialEq allows simple change detection
57pub struct DataFilter {
58    /// The canonical, absolute path to the data file.
59    pub absolute_path: PathBuf,
60    /// The name assigned to the loaded DataFrame for use in SQL queries.
61    pub table_name: String,
62    /// The character used to separate columns in a CSV file.
63    pub csv_delimiter: String,
64    /// Read data from file
65    pub read_data_from_file: bool,
66    /// The schema (column names and data types) of the most recently loaded DataFrame.
67    /// Used by `sql_commands` for generating relevant examples.
68    pub schema: Arc<Schema>,
69    /// Maximum rows to scan for schema inference (CSV, JSON, NDJson).
70    pub infer_schema_rows: usize,
71    /// Flag to control removal of all-null columns after loading/querying.
72    pub exclude_null_cols: bool,
73    /// Comma-separated string of values to interpret as nulls during CSV parsing.
74    pub null_values: String,
75
76    /// Regex patterns matching columns to force read as String type.
77    ///
78    /// List of column names to force reading as String, overriding inference.
79    /// Useful for columns with large IDs/keys that look numeric.
80    pub force_string_patterns: Option<String>,
81
82    /// Flag indicating if the `query` should be executed during the next `load_data`.
83    /// Set by `render_query` if relevant UI fields change or the Apply button is clicked.
84    pub apply_sql: bool,
85    /// The SQL query string entered by the user.
86    pub query: String,
87
88    // --- NEW FIELDS for Index Column ---
89    /// Flag indicating if a row index column should be added.
90    pub add_row_index: bool,
91    /// The desired name for the row index column (will be checked for uniqueness later).
92    pub index_column_name: String,
93    /// The starting value for the row index column (e.g., 0 or 1).
94    pub index_column_offset: u32,
95    // --- END NEW FIELDS ---
96
97    // --- Normalize Columns ---
98    /// Flag indicating whether string columns will be normalized.
99    pub normalize: bool,
100    /// Regex pattern to select string columns.
101    pub normalize_regex: String,
102
103    // --- Drop Columns ---
104    pub drop: bool,
105    pub drop_regex: String,
106}
107
108impl Default for DataFilter {
109    /// Creates default `DataFilter` with sensible initial values.
110    fn default() -> Self {
111        DataFilter {
112            absolute_path: PathBuf::new(),
113            table_name: "AllData".to_string(),
114            csv_delimiter: DEFAULT_CSV_DELIMITER.to_string(),
115            read_data_from_file: true,
116            schema: Schema::default().into(),
117            infer_schema_rows: DEFAULT_INFER_SCHEMA_ROWS,
118            exclude_null_cols: false,
119            null_values: NULL_VALUES.to_string(),
120
121            force_string_patterns: DEFAULT_OVERRIDE_REGEX.map(ToString::to_string),
122
123            apply_sql: false,
124            query: DEFAULT_QUERY.to_string(),
125
126            // --- NEW DEFAULTS ---
127            add_row_index: false, // Default to false
128            index_column_name: DEFAULT_INDEX_COLUMN_NAME.to_string(),
129            index_column_offset: DEFAULT_INDEX_COLUMN_OFFSET,
130            // --- END NEW DEFAULTS ---
131
132            // --- NEW FIELDS for Normalize Columns ---
133            normalize: false,
134            normalize_regex: DEFAULT_NORM_REGEX.to_string(),
135            // --- END NEW FIELDS ---
136            drop: false,
137            drop_regex: DEFAULT_DROP_REGEX.to_string(),
138        }
139    }
140}
141
142// --- Methods ---
143
144impl DataFilter {
145    /// Creates a new `DataFilter` instance configured from command-line `Arguments`.
146    /// This is typically called once at application startup in `main.rs`.
147    ///
148    /// ### Arguments
149    /// * `args`: Parsed command-line arguments (`crate::Arguments`).
150    ///
151    /// ### Returns
152    /// A `PolarsViewResult` containing the configured `DataFilter` or an error
153    /// (e.g., if the path cannot be canonicalized).
154    pub fn new(args: &Arguments) -> PolarsViewResult<Self> {
155        let absolute_path = match &args.path {
156            Some(p) => p.canonicalize()?, // Se houver path, tenta tornar absoluto
157            None => PathBuf::new(),       // Se não houver, inicia vazio
158        };
159
160        // Determine apply_sql state from the CLI argument
161        let apply_sql = args.query.is_some();
162        let query = args
163            .query
164            .clone()
165            .unwrap_or_else(|| DEFAULT_QUERY.to_string()); // Use CLI arg or default
166
167        // Determine normalization state from the CLI argument
168        let normalize = args.regex.is_some();
169        let normalize_regex = args
170            .regex
171            .clone()
172            .unwrap_or_else(|| DEFAULT_NORM_REGEX.to_string()); // Use CLI arg or default
173
174        // Use or_else: takes a closure executed only if the first option is None.
175        // This avoids the .to_string() for the default unless actually needed.
176        let force_string_patterns = args
177            .force_string_patterns // This is Option<String>
178            .clone() // Clone the Option<String> from args if needed later, otherwise maybe take ownership
179            .or(DEFAULT_OVERRIDE_REGEX.map(ToString::to_string)); // Use CLI arg or default
180
181        Ok(DataFilter {
182            absolute_path,
183            table_name: args.table_name.clone(),
184            csv_delimiter: args.delimiter.clone(),
185
186            apply_sql, // Directly set based on CLI argument presence
187            query,     // Directly set based on CLI argument value (or default)
188
189            exclude_null_cols: args.exclude_null_cols,
190            null_values: args.null_values.clone(), // Use user-provided nulls.
191
192            force_string_patterns,
193
194            normalize,            // Directly set based on CLI argument presence
195            normalize_regex,      // Directly set based on CLI argument value (or default)
196            ..Default::default()  // Use defaults for `schema`, `infer_schema_rows`.
197        })
198    }
199
200    /// Sets the data source path, canonicalizing it.
201    pub fn set_path(&mut self, path: &Path) -> PolarsViewResult<()> {
202        self.absolute_path = path.canonicalize()?;
203        tracing::debug!("absolute_path set to: {:#?}", self.absolute_path);
204        Ok(())
205    }
206
207    /// Gets the file extension from `absolute_path` in lowercase.
208    pub fn get_extension(&self) -> Option<String> {
209        self.absolute_path.extension_as_lowercase()
210    }
211
212    /// Determines the configuration for an optional row index column by resolving a unique name
213    /// against the provided schema.
214    ///
215    /// If `self.add_row_index` is true, this method finds a unique name based on
216    /// `self.index_column_name` and the provided `schema`, returning a `Some(RowIndex)`.
217    /// If the name resolution fails, it returns the specific PolarsError.
218    /// If `self.add_row_index` is false, it returns `Ok(None)`.
219    ///
220    /// ### Arguments
221    /// * `schema`: The schema against which the index column name should be checked for uniqueness.
222    ///   This should be the schema of the DataFrame *before* adding the index column.
223    ///
224    /// ### Returns
225    /// `PolarsResult<Option<RowIndex>>`: Ok(Some) if config is resolved, Ok(None) if disabled, Err if resolution fails.
226    pub fn get_row_index(&self, schema: &Schema) -> PolarsResult<Option<RowIndex>> {
227        // Check the main feature flag
228        if !self.add_row_index {
229            tracing::trace!("Row index addition disabled in filter.");
230            return Ok(None); // Feature disabled, return None config
231        }
232
233        // Feature is enabled. Resolve a unique name using the helper.
234        let unique_name = resolve_unique_column_name(
235            // Use the helper function
236            &self.index_column_name, // Base name
237            schema,                  // Schema to check uniqueness against
238        )?; // Propagate potential error from unique name resolution
239
240        // If we successfully resolved a unique name, return the full RowIndex config
241        let index_offset = self.index_column_offset;
242        Ok(Some(RowIndex {
243            name: unique_name,
244            offset: index_offset,
245        }))
246    }
247
248    /// Determines the `FileExtension` and orchestrates loading the DataFrame using the appropriate Polars reader.
249    /// This method centralizes the file-type-specific loading logic. Called by `DataFrameContainer::load_data`.
250    ///
251    /// **Important:** It mutates `self` by potentially updating `csv_delimiter` if automatic
252    /// detection during `read_csv_data` finds a different working delimiter than initially configured.
253    ///
254    /// ### Returns
255    /// A `PolarsViewResult` containing a tuple: `(DataFrame, FileExtension)` on success,
256    /// or a `PolarsViewError` (e.g., `FileType`, `CsvParsing`) on failure.
257    pub async fn get_df_and_extension(&mut self) -> PolarsViewResult<(DataFrame, FileExtension)> {
258        // Determine the file extension type using the helper from `extension.rs`.
259        let extension = FileExtension::from_path(&self.absolute_path);
260
261        // Match on the determined extension to call the correct reader function.
262        let (df, detected_delimiter) = match &extension {
263            FileExtension::Csv => self.read_csv_data().await?,
264            FileExtension::Json => self.read_json_data().await?,
265            FileExtension::NDJson => self.read_ndjson_data().await?,
266            FileExtension::Parquet => self.read_parquet_data().await?,
267            // Handle unsupported or missing extensions with specific errors.
268            FileExtension::Unknown(ext) => {
269                return Err(PolarsViewError::FileType(format!(
270                    "Unsupported extension: `{}` for file: `{}`",
271                    ext,
272                    self.absolute_path.display()
273                )));
274            }
275            FileExtension::Missing => {
276                return Err(PolarsViewError::FileType(format!(
277                    "Missing extension for file: `{}`",
278                    self.absolute_path.display()
279                )));
280            }
281        };
282
283        // If reading a CSV successfully detected a working delimiter, update the filters state.
284        // This ensures the UI reflects the delimiter actually used.
285        if let Some(byte) = detected_delimiter {
286            self.csv_delimiter = (byte as char).to_string();
287        }
288
289        tracing::debug!(
290            "fn get_df_and_extension(): Successfully loaded DataFrame with extension: {:?}",
291            extension
292        );
293
294        Ok((df, extension)) // Return the loaded DataFrame and the detected extension.
295    }
296
297    // --- Data Reading Helper Methods ---
298
299    /// Reads a standard JSON file into a Polars DataFrame.
300    /// Configures the reader using settings from `self` (e.g., `infer_schema_rows`).
301    ///
302    /// ### Returns
303    /// A `PolarsViewResult` containing `(DataFrame, None)` (delimiter is not applicable to JSON).
304    async fn read_json_data(&self) -> PolarsViewResult<(DataFrame, Option<u8>)> {
305        tracing::debug!("Reading JSON data from: {}", self.absolute_path.display());
306        let file = File::open(&self.absolute_path)?;
307        let infer_schema_rows_for_task = self.infer_schema_rows;
308
309        // Execute the blocking read operation on a separate thread
310        let df = execute_polars_blocking(move || {
311            JsonReader::new(file)
312                .infer_schema_len(NonZero::new(infer_schema_rows_for_task))
313                .finish()
314        })
315        .await?;
316
317        tracing::debug!("JSON read complete. Shape: {:?}", df.shape());
318
319        Ok((df, None))
320    }
321
322    /// Reads a Newline-Delimited JSON (NDJson / JSON Lines) file into a Polars DataFrame.
323    /// Uses `LazyJsonLineReader` for potentially better performance/memory usage on large files.
324    ///
325    /// ### Returns
326    /// A `PolarsViewResult` containing `(DataFrame, None)`.
327    async fn read_ndjson_data(&self) -> PolarsViewResult<(DataFrame, Option<u8>)> {
328        tracing::debug!("Reading NDJSON data from: {}", self.absolute_path.display());
329
330        // Clone data from self needed for the task closure.
331        let pl_ref_path = PlRefPath::try_from_path(&self.absolute_path)?;
332        let infer_schema_rows_for_task = self.infer_schema_rows;
333
334        // *** Use the helper function ***
335        let df = execute_polars_blocking(move || {
336            // 'move' captures pl_ref_path, infer_schema_rows_for_task
337            // This code runs on the blocking thread.
338            let lazyframe = LazyJsonLineReader::new(pl_ref_path) // Use cloned path
339                .low_memory(false) // Option to optimize for memory.
340                .with_infer_schema_length(NonZero::new(infer_schema_rows_for_task))
341                .with_ignore_errors(true)
342                .finish()?; // Returns PolarsResult<LazyFrame> (this finish() isn't the main blocking part)
343
344            // Collect the lazy frame - THIS IS THE BLOCKING PART
345            lazyframe.with_streaming(true).collect() // Returns PolarsResult<DataFrame>
346        })
347        .await?; // await the helper function
348
349        tracing::debug!("NDJSON read complete. Shape: {:?}", df.shape());
350        Ok((df, None))
351    }
352
353    /// Reads an Apache Parquet file into a Polars DataFrame.
354    ///
355    /// ### Returns
356    /// A `PolarsViewResult` containing `(DataFrame, None)`.
357    async fn read_parquet_data(&self) -> PolarsViewResult<(DataFrame, Option<u8>)> {
358        tracing::debug!(
359            "Reading Parquet data from: {}",
360            self.absolute_path.display()
361        );
362
363        // Clone data from self needed for the task closure.
364        let pl_ref_path = PlRefPath::try_from_path(&self.absolute_path)?;
365        let args = ScanArgsParquet {
366            // ScanArgsParquet should be Send
367            low_memory: false, // Configure scan arguments as needed.
368            ..Default::default()
369        };
370
371        let df = execute_polars_blocking(move || {
372            // Use `LazyFrame::scan_parquet` for efficient scanning.
373            let lazyframe = LazyFrame::scan_parquet(pl_ref_path, args)?; // Returns PolarsResult<LazyFrame>
374
375            // Collect into an eager DataFrame - THIS IS THE BLOCKING/COMPUTE PART.
376            lazyframe.with_streaming(true).collect() // Returns PolarsResult<DataFrame>
377        })
378        .await?; // await the helper function
379
380        tracing::debug!("Parquet read complete. Shape: {:?}", df.shape());
381
382        Ok((df, None))
383    }
384
385    /// Reads a CSV file, attempting automatic delimiter detection if the initial one fails.
386    /// Iterates through common delimiters and tries reading a small chunk first for efficiency.
387    ///
388    /// ### Returns
389    /// A `PolarsViewResult` containing `(DataFrame, Option<u8>)` where `Option<u8>` is the
390    /// *successfully used* delimiter byte. Returns `Err(PolarsViewError::CsvParsing)` if
391    /// no common delimiter works.
392    async fn read_csv_data(&self) -> PolarsViewResult<(DataFrame, Option<u8>)> {
393        // Get the currently configured separator byte. Error if invalid (e.g., empty string).
394        let initial_separator = self.get_csv_separator()?;
395
396        // List of common delimiters to try, starting with the configured one.
397        let mut delimiters_to_try = vec![initial_separator, b',', b';', b'|', b'\t', b':'];
398        // Remove duplicates if the initial separator is already in the common list.
399        delimiters_to_try.unique();
400        tracing::debug!(
401            "Attempting CSV read. Delimiters to try: {:?}",
402            delimiters_to_try
403                .iter()
404                .map(|&b| b as char)
405                .collect::<Vec<_>>()
406        );
407
408        // Look at the next element of the iterator without consuming it.
409        let mut iterator = delimiters_to_try.iter().peekable();
410
411        // Iterate through the potential delimiters.
412        while let Some(&delimiter) = iterator.next() {
413            // If peek() returns None, it means the current item was the last one
414            let is_last_element = iterator.peek().is_none();
415
416            // 1. Quick Check: Try reading only a small number of rows (NROWS_CHECK).
417            // This fails fast if the delimiter is fundamentally wrong (e.g., results in 1 column).
418            if let Ok(schema) = self
419                .attempt_csv_parse_structure(delimiter, is_last_element)
420                .await
421            {
422                // 2. Full Read: If the quick check passed, attempt to read the entire file.
423                tracing::debug!(
424                    "Trying to read full CSV file with delimiter: '{}'",
425                    delimiter as char
426                );
427                match self.attempt_read_csv(delimiter, &schema).await {
428                    Ok(lazyframe) => {
429                        // Success! Return the DataFrame and the delimiter that worked.
430                        tracing::info!(
431                            "Successfully read CSV with delimiter: '{}'",
432                            delimiter as char
433                        );
434
435                        // Execute the lazy plan and collect into an eager DataFrame on a blocking thread
436                        let df = execute_polars_blocking(move || {
437                            lazyframe.with_streaming(true).collect()
438                        })
439                        .await?;
440
441                        tracing::debug!("Data collection complete. Shape: {:?}", df.shape());
442                        return Ok((df, Some(delimiter)));
443                    }
444                    Err(e) => {
445                        // Full read failed even after quick check passed. Log and try next delimiter.
446                        tracing::warn!(
447                            "Full CSV read failed with delimiter '{}' after quick check passed: {}",
448                            delimiter as char,
449                            e
450                        );
451                        continue; // Try the next delimiter.
452                    }
453                }
454            }
455            // If quick check fails, implicitly try the next delimiter.
456        }
457
458        // If all delimiters failed, return a parsing error.
459        let msg = format!(
460            "Failed to read CSV '{}' with common delimiters. Check format or specify delimiter.",
461            self.absolute_path.display()
462        );
463        let error = PolarsViewError::CsvParsing(msg);
464        tracing::error!("{}", error);
465        Err(error)
466    }
467
468    /// Retrieves the CSV separator byte from the `csv_delimiter` String configuration.
469    ///
470    /// ### Returns
471    /// `Ok(u8)` containing the first byte, or `Err(PolarsViewError::InvalidDelimiter)`
472    /// if the string is empty or contains multi-byte characters (only first byte is used).
473    fn get_csv_separator(&self) -> PolarsViewResult<u8> {
474        self.csv_delimiter
475            .as_bytes() // Convert String to byte slice.
476            .first() // Get the first byte.
477            .copied() // Copy the byte out of the Option<&u8>.
478            // Map `None` (empty string) to an InvalidDelimiter error.
479            .ok_or_else(|| PolarsViewError::InvalidDelimiter(self.csv_delimiter.clone()))
480    }
481
482    /// Attempts to parse the CSV structure from the initial chunk of the file
483    /// using a specific delimiter and validates the result.
484    async fn attempt_csv_parse_structure(
485        &self,
486        delimiter: u8,
487        is_last_element: bool,
488    ) -> PolarsViewResult<Arc<Schema>> {
489        // Constant: Number of data rows to ask Polars to read *after* the header
490        // during this quick probe. 100 is a common heuristic to get enough
491        // context without reading the whole file.
492        const ROW_LIMIT: usize = 100;
493
494        tracing::debug!(
495            "Trying to parse CSV with delimiter: '{}'",
496            delimiter as char,
497        );
498
499        let file_path = &self.absolute_path;
500
501        // Perform a partial read from the file using the given delimiter.
502        let data_frame = read_csv_partial_from_path(delimiter, ROW_LIMIT, file_path).await?;
503
504        // **Basic Validation**: Check resulting width (important for delimiter detection loops)
505        // it's highly likely the delimiter was incorrect. Return an error early.
506        // This check is crucial for the delimiter detection loop in `read_csv_data`.
507        let min_expected_cols_on_success = if self.add_row_index { 2 } else { 1 }; // Assumes index is added *later*
508
509        if data_frame.width() <= min_expected_cols_on_success && !is_last_element {
510            tracing::warn!(
511                "CSV read with delimiter '{}' resulted in {} columns (expected > {}). Assuming incorrect delimiter.",
512                delimiter as char,
513                data_frame.width(),
514                min_expected_cols_on_success
515            );
516            // Return a specific error type or message indicating a likely delimiter issue.
517            return Err(PolarsViewError::CsvParsing(format!(
518                "Delimiter '{}' likely incorrect (resulted in {} columns)",
519                delimiter as char,
520                data_frame.width()
521            )));
522        }
523
524        tracing::debug!(
525            "CSV read successful with delimiter '{}'. Final shape (rows, columns): {:?}",
526            delimiter as char,
527            data_frame.shape()
528        );
529
530        Ok(data_frame.schema().clone())
531    }
532
533    async fn attempt_read_csv(
534        &self,
535        delimiter: u8,
536        previous_scheme: &Arc<Schema>,
537    ) -> PolarsViewResult<LazyFrame> {
538        tracing::debug!(
539            "Attempting CSV read with delimiter: '{}'",
540            delimiter as char,
541        );
542
543        /*
544        When LazyCsvReader tries to determine the data types (with_infer_schema_length()), it looks at the first N rows.
545        If your problematic column contains only digits in those initial rows,
546        Polars will likely infer a numeric type (like Int64, UInt64, or Float64).
547
548        However, a standard 64-bit integer or float cannot represent an arbitrarily long sequence of digits (like a 44-digit key).
549        When the reader later encounters these huge numbers and tries to parse them into the inferred numeric type, the parsing fails.
550        with_ignore_errors(true), instead of stopping, Polars replaces the unparseable value with null.
551        If all values in that column exceed the capacity of the inferred numeric type, the entire column becomes null, appearing "empty".
552
553        Solution:
554        .with_dtype_overwrite(dtypes_opt)
555        */
556
557        let mut dtypes_opt: Option<Arc<Schema>> = None;
558
559        if let Some(force_string_patterns) = &self.force_string_patterns {
560            // Build dtype overrides using the dedicated function
561            //    Pass the actual headers and the configured regex patterns.
562            let override_schema = build_dtype_override_schema(
563                previous_scheme,
564                force_string_patterns, // Get patterns from self
565            )?; // Propagate potential regex compilation errors
566
567            // Convert the resulting Schema into Option<Arc<Schema>>.
568            if !override_schema.is_empty() {
569                dtypes_opt = Some(Arc::new(override_schema));
570            };
571        }
572
573        let pl_ref_path = PlRefPath::try_from_path(&self.absolute_path)?;
574
575        // Configure the LazyCsvReader using settings from `self`.
576        let lazyframe = LazyCsvReader::new(pl_ref_path)
577            .with_low_memory(false) // Can be set to true for lower memory usage at cost of speed.
578            .with_encoding(CsvEncoding::LossyUtf8) // Gracefully handle potential encoding errors.
579            .with_has_header(true) // Assume a header row.
580            .with_try_parse_dates(true) // Attempt automatic date parsing.
581            .with_separator(delimiter) // Use the specified delimiter.
582            .with_infer_schema_length(Some(self.infer_schema_rows)) // Use filter setting for inference.
583            .with_dtype_overwrite(dtypes_opt)
584            .with_ignore_errors(true) // Rows with parsing errors become nulls instead of stopping the read.
585            .with_missing_is_null(true) // Treat missing fields as null.
586            .with_null_values(None) // Apply fn replace_values_with_null()
587            .with_n_rows(None) // Apply row limit if specified.
588            .with_decimal_comma(false) // If files use ',' as decimal separator.
589            .with_row_index(None) // Apply fn add_row_index_column()
590            .with_rechunk(true) // Rechunk the memory to contiguous chunks when parsing is done.
591            .finish()?; // Finalize configuration and create the LazyFrame.
592
593        Ok(lazyframe)
594    }
595
596    /// Parses the comma-separated `null_values` string into a `Vec<&str>`,
597    /// removing surrounding double quotes if present.
598    ///
599    /// Logic:
600    /// 1. Splits the input string (`self.null_values`) by commas.
601    /// 2. Iterates through each resulting substring (`s`).
602    /// 3. For each substring:
603    ///    a. Trims leading and trailing whitespace.
604    ///    b. Checks if the `trimmed` string has at least 2 characters AND starts with `"` AND ends with `"`.
605    ///    c. If true, returns a slice (`&str`) representing the content *between* the quotes.
606    ///    Example: `"\"\""` becomes `""`, `" N/A "` becomes `"N/A"`, `" " "` becomes `" "`.
607    ///    d. If false (no surrounding quotes), returns a slice (`&str`) of the `trimmed` string itself.
608    ///    Example: `<N/D>` remains `<N/D>`, ` NA ` becomes `NA`.
609    /// 4. Collects all the resulting string slices into a `Vec<&str>`.
610    ///
611    /// Example Input: `"\"\", \" \", <N/D>, NA "`
612    /// Example Output: `vec!["", " ", "<N/D>", "NA"]`
613    pub fn parse_null_values(&self) -> Vec<&str> {
614        self.null_values
615            .split(',') // 1. Split the string by commas.
616            .map(|s| {
617                // For each part resulting from the split:
618                // 3a. Trim leading/trailing whitespace.
619                let trimmed = s.trim();
620                // 3b. Check if it's quoted (length >= 2, starts/ends with ").
621                if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') {
622                    // 3c. If quoted, return the slice between the quotes.
623                    trimmed[1..trimmed.len() - 1].trim()
624                } else {
625                    // 3d. If not quoted, return the trimmed slice directly.
626                    trimmed
627                }
628            })
629            .collect() // 4. Collect the processed slices into a vector.
630    }
631
632    // --- UI Rendering Methods ---
633
634    /// Renders the UI widgets for configuring data filters within the "Query" collapsing header.
635    /// This function is called by `layout.rs::render_side_panel`.
636    ///
637    /// **Crucially, it takes `&mut self`. Widgets modify `self` directly.**
638    /// It compares the state of `self` *before* and *after* rendering the widgets.
639    /// If any change occurred (user typed in a field, clicked a checkbox), it returns
640    /// `Some(self.clone())` containing the *modified* state. Otherwise, it returns `None`.
641    ///
642    /// The `layout.rs` code uses this return value:
643    /// - If `Some(new_filters)`, it triggers an asynchronous `DataFrameContainer::load_data` call.
644    /// - If `None`, no user change was detected in this frame, so no action is taken.
645    ///
646    /// It also sets `self.apply_sql = true` if any changes are detected, ensuring the SQL
647    /// query is re-applied upon reload.
648    ///
649    /// ### Arguments
650    /// * `ui`: The `egui::Ui` context for drawing the widgets.
651    ///
652    /// ### Returns
653    /// * `Some(DataFilter)`: If any filter setting was changed by the user in this frame.
654    /// * `None`: If no changes were detected.
655    pub fn render_query(&mut self, ui: &mut Ui) -> Option<DataFilter> {
656        // Clone the state *before* rendering UI widgets to detect changes later.
657        let filters_before_render = self.clone();
658        let mut result = None;
659
660        let width_min = 450.0; // Minimum width for the grid area.
661
662        // Use a grid layout for label-input pairs.
663        let grid = Grid::new("data_query_grid")
664            .num_columns(2)
665            .spacing([10.0, 20.0]) // Horizontal and vertical spacing.
666            .striped(true); // Alternating row backgrounds.
667
668        // Allocate UI space for the grid.
669        ui.allocate_ui_with_layout(
670            Vec2::new(ui.available_width(), ui.available_height()), // Occupy available width.
671            Layout::top_down(Align::LEFT),
672            |ui| {
673                grid.show(ui, |ui| {
674                    ui.set_min_width(width_min);
675
676                    // --- Render Individual Filter Widgets ---
677                    // Each `render_*` method takes `&mut self` and `ui`.
678
679                    self.render_add_row_number(ui);
680
681                    self.render_exclude_null_cols(ui);
682
683                    self.render_exclude_columns(ui);
684
685                    self.render_normalize_numbers(ui);
686
687                    self.render_null_values(ui);
688
689                    // Input for schema inference length (only for relevant file types).
690                    if matches!(
691                        self.get_extension().as_deref(), // Get extension as &str
692                        Some("csv" | "json" | "ndjson")  // Check if it's one of these
693                    ) {
694                        self.render_schema_length_input(ui);
695                    }
696
697                    // CSV-specific settings: delimiter.
698                    if self.get_extension().as_deref() == Some("csv") {
699                        self.render_csv_delimiter(ui);
700                    }
701
702                    // Input for table name used in SQL.
703                    self.render_table_name_input(ui);
704
705                    // Multiline input for the SQL query.
706                    self.render_sql_query_input(ui);
707
708                    // --- Change Detection & Apply Button ---
709
710                    // Compare current state `self` with the state before rendering.
711                    if *self != filters_before_render {
712                        // Mark that SQL needs to be (re-)applied.
713                        self.apply_sql = true;
714                        tracing::debug!("Change detected in DataFilter UI.");
715                    }
716
717                    if (self.csv_delimiter != filters_before_render.csv_delimiter)
718                        || (self.infer_schema_rows != filters_before_render.infer_schema_rows)
719                    {
720                        self.read_data_from_file = true;
721                    }
722
723                    // Add the "Apply SQL commands" button.
724                    ui.label(""); // For alignment.
725                    ui.with_layout(Layout::top_down(Align::Center), |ui| {
726                        if ui.button("Apply SQL commands").clicked() {
727                            if self.apply_sql {
728                                // Result contains DataFilter after editing some fields
729                                result = Some(self.clone());
730                            }
731
732                            tracing::debug!("Apply SQL commands: {}", self.apply_sql);
733                        }
734                    });
735                    ui.end_row();
736                }); // End grid.show
737            }, // End allocate_ui_with_layout
738        ); // End allocation
739
740        // Display the SQL examples section (collapsible).
741        self.render_sql_examples(ui);
742
743        result // Return the potentially updated filters.
744    }
745
746    // --- Helper Rendering Methods ---
747
748    fn render_add_row_number(&mut self, ui: &mut Ui) {
749        // --- Row 1: Feature Checkbox ---
750        ui.label("Add Row Number:");
751        // The checkbox directly modifies `self.add_row_index`
752        ui.checkbox(&mut self.add_row_index, "")
753            .on_hover_text("Add a new column that counts the rows (first column).");
754        ui.end_row();
755
756        // --- Conditional Configuration Inputs ---
757        // These rows are only added to the grid if the checkbox is checked.
758        if self.add_row_index {
759            // --- Index Name Input ---
760            // Use simple indentation in the label for visual structure
761            ui.label("\tName:");
762            let name_edit =
763                TextEdit::singleline(&mut self.index_column_name).desired_width(f32::INFINITY); // Use available width in the grid cell
764            ui.add(name_edit)
765                .on_hover_text("Name for the new index column (uniqueness checked later).");
766            ui.end_row();
767
768            // --- Index Offset Input ---
769            // Use simple indentation
770            ui.label("\tOffset:");
771            let offset_drag = DragValue::new(&mut self.index_column_offset)
772                .speed(1) // Increment by 1
773                .range(0..=u32::MAX); // Allow 0-based or 1-based commonly
774            ui.add(offset_drag)
775                .on_hover_text("Starting value for the index (e.g., 0 or 1).");
776            ui.end_row();
777        }
778        // No 'else' needed. If add_row_index is false, these rows are simply skipped.
779    }
780
781    /// Renders the checkbox for the "Remove Null Cols" option.
782    /// Modifies `self.exclude_null_cols` directly.
783    fn render_exclude_null_cols(&mut self, ui: &mut Ui) {
784        ui.label("Exclude Null Cols:");
785        ui.checkbox(&mut self.exclude_null_cols, "")
786            .on_hover_text("Remove columns containing only null values.");
787        ui.end_row();
788    }
789
790    fn render_exclude_columns(&mut self, ui: &mut Ui) {
791        // --- Row 1: Feature Checkbox ---
792        ui.label("Remove Columns:");
793        ui.checkbox(&mut self.drop, "")
794            .on_hover_text("Remove columns whose names match the specified regex pattern.");
795        ui.end_row();
796
797        // --- Conditional Configuration Inputs ---
798        // These rows are only added to the grid if the checkbox is checked.
799        if self.drop {
800            // --- Regex Input ---
801            // Use simple indentation in the label for visual structure
802            ui.label("\tRegex:");
803            let name_edit = TextEdit::singleline(&mut self.drop_regex).desired_width(f32::INFINITY); // Use available width in the grid cell
804            ui.add(name_edit).on_hover_text(
805                "Enter the regex pattern to identify columns to drop by name.\n\n\
806                Format Requirements:\n\
807                - Use `*` to drop ALL columns.\n\
808                - Use `^YourPattern$` to match the entire column name.\n  \
809                   (Must start with `^` and end with `$`).\n\n\
810                Regex Examples:\n\
811                - `^Temp.*$`   (Matches columns starting with 'Temp')\n\
812                - `^Value B$`    (Matches the exact column named 'Value B')\n\
813                - `^(ID|Key|Index)$` (Matches 'ID', 'Key', or 'Index' exactly)\n\
814                - `^.*_OLD$`    (Matches columns ending with '_OLD')\n\n\
815                (Invalid regex syntax or format will cause errors.)",
816            );
817            ui.end_row();
818        }
819    }
820
821    fn render_normalize_numbers(&mut self, ui: &mut Ui) {
822        // --- Row 1: Feature Checkbox ---
823        ui.label("Normalize Columns:");
824        ui.checkbox(&mut self.normalize, "").on_hover_text(
825            "Normalize Euro-style number strings in selected column names (via regex) to Float64.\n\
826            Example: '1.234,56' (String) to '1234.56' (Float64).",
827        );
828        ui.end_row();
829
830        // --- Conditional Configuration Inputs ---
831        // These rows are only added to the grid if the checkbox is checked.
832        if self.normalize {
833            // --- Regex Input ---
834            // Use simple indentation in the label for visual structure
835            ui.label("\tRegex:");
836            let name_edit =
837                TextEdit::singleline(&mut self.normalize_regex).desired_width(f32::INFINITY); // Use available width in the grid cell
838            ui.add(name_edit).on_hover_text(
839                r#"
840Enter a regex pattern to select String columns by name.
841
842Rules:
843- Use '*' for ALL String columns (caution!).
844- Use '^PATTERN$' for specific names (matches entire name).
845
846Example Columns:
847Row Number, Value1, Value2, ValueA, Valor, Total, SubTotal, Last Info
848
849Example Patterns:
8501. To select 'Value1', 'Value2':
851   ^Value\d$
852
8532. To select 'Value1', 'Value2', 'ValueA':
854   ^Value.*$
855
8563. To select 'Value1', 'Value2', 'ValueA', 'Valor':
857   ^Val.*$
858
8594. To select 'Value1', 'Value2', 'ValueA', 'Valor', 'Total', 'SubTotal':
860   ^(Val|.*Total).*$
861
8625. To select only 'Last Info' (note the space):
863   ^Last Info$
864
865(Applies only to columns that Polars identifies as String type.)"#,
866            );
867            ui.end_row();
868        }
869    }
870
871    /// Renders the `TextEdit` widget for specifying custom null values.
872    /// Modifies `self.null_values` directly based on user input.
873    fn render_null_values(&mut self, ui: &mut Ui) {
874        // Null Values Input Label
875        ui.label("Null Values:");
876
877        // Single-line text edit widget bound to the `self.null_values` string.
878        let null_values_edit =
879            TextEdit::singleline(&mut self.null_values).desired_width(f32::INFINITY); // Take available horizontal space.
880
881        // Add the widget to the UI and set its hover text.
882        ui.add(null_values_edit).on_hover_text(
883            "Comma-separated values to interpret as null during loading.\n\
884            Leading/trailing whitespace for each value is automatically trimmed.",
885        );
886
887        // End the row in the parent Grid layout.
888        ui.end_row();
889    }
890
891    /// Renders the `DragValue` widget for setting `infer_schema_rows`.
892    /// Modifies `self.infer_schema_rows` directly.
893    fn render_schema_length_input(&mut self, ui: &mut Ui) {
894        ui.label("Infer Rows:");
895        ui.add(
896            DragValue::new(&mut self.infer_schema_rows)
897                .speed(1) // Increment/decrement speed.
898                .range(0..=usize::MAX), // 0: No inference
899        )
900        .on_hover_text(
901            "Number of rows to scan for inferring data types (CSV/JSON)\n0: No inference",
902        );
903        ui.end_row();
904    }
905
906    /// Renders the `TextEdit` widgets for CSV-specific settings: delimiter.
907    /// Modifies `self.csv_delimiter` directly.
908    fn render_csv_delimiter(&mut self, ui: &mut Ui) {
909        // CSV Delimiter Input
910        ui.label("CSV Delimiter:");
911        let csv_delimiter_edit = TextEdit::singleline(&mut self.csv_delimiter)
912            .char_limit(1) // Restrict to a single character.
913            .desired_width(f32::INFINITY);
914        ui.add(csv_delimiter_edit)
915            .on_hover_text("Enter the single character CSV delimiter");
916        ui.end_row();
917    }
918
919    /// Renders the `TextEdit` widget for the SQL table name.
920    /// Modifies `self.table_name` directly.
921    fn render_table_name_input(&mut self, ui: &mut Ui) {
922        ui.label("SQL Table Name:");
923        let table_name_edit =
924            TextEdit::singleline(&mut self.table_name).desired_width(f32::INFINITY);
925        ui.add(table_name_edit)
926            .on_hover_text("Name of the table to use in SQL queries (e.g., FROM TableName)");
927        ui.end_row();
928    }
929
930    /*
931    /// Renders the multiline `TextEdit` widget for the SQL query.
932    /// Modifies `self.query` directly.
933    fn render_sql_query_input(&mut self, ui: &mut Ui) {
934        ui.label("SQL Query:");
935        let query_edit = TextEdit::multiline(&mut self.query)
936            .desired_width(f32::INFINITY)
937            // Set a reasonable initial height for the multiline input.
938            .desired_rows(4);
939        ui.add(query_edit)
940            .on_hover_text("Enter SQL query to filter/transform data (uses Polars SQL syntax)");
941        ui.end_row();
942    }
943    */
944
945    /// Renders tabbed SQL examples and the editable query input `self.query`.
946    /// Handles selecting examples and editing the query. Tabs will wrap if needed.
947    /// ### Logic
948    /// 1. Generate SQL examples via `sql_commands` using `self.schema`.
949    /// 2. Manage selected tab index using `egui::Memory`.
950    /// 3. Render **wrapping horizontal tabs** for examples using `ui.horizontal_wrapped`.
951    /// 4. On tab click: update index, copy example to `self.query`.
952    /// 5. Render multiline `TextEdit` bound to `&mut self.query`.
953    ///
954    /// Note: Actual *triggering* of reload happens in `render_query` based on overall state change detection or Apply click.
955    fn render_sql_query_input(&mut self, ui: &mut Ui) {
956        ui.label("SQL Query:"); // Label for the whole section
957        ui.vertical(|ui| {
958            // Group the examples and editor vertically
959            // Configure minimum width for the vertical group if needed
960            ui.set_min_width(300.0);
961
962            // 1. Generate examples based on the current schema
963            let examples = sql_commands(&self.schema);
964            if examples.is_empty() {
965                // If no schema or examples, just show the editor
966                ui.add(
967                    TextEdit::multiline(&mut self.query)
968                        .desired_width(f32::INFINITY)
969                        .desired_rows(8) // Slightly more rows if no examples
970                        .font(egui::TextStyle::Monospace),
971                );
972                return; // Skip rendering examples if none exist
973            }
974
975            // 2. Get/Set selected tab index from Memory for persistence
976            let tab_id = ui.id().with("sql_query_tab_index");
977            let mut selected_tab_index =
978                ui.memory_mut(|mem| *mem.data.get_persisted_mut_or_default::<usize>(tab_id));
979
980            // Clamp selected index to be valid (in case number of examples changes)
981            selected_tab_index = selected_tab_index.min(examples.len().saturating_sub(1));
982
983            // 3. Render Tabs using horizontal_wrapped
984            ui.separator();
985            ui.label("Examples:"); // Label for the tabs
986
987            // Use horizontal_wrapped to lay out tabs, wrapping them onto new lines
988            ui.horizontal_wrapped(|ui| {
989                // Iterate through examples to create selectable labels (tabs)
990                for i in 0..examples.len() {
991                    let is_selected = selected_tab_index == i;
992                    let tab_name = format!("{}", i + 1); // Simple number for the tab
993
994                    // Create the selectable label (acting as a tab)
995                    let resp = ui
996                        .selectable_label(is_selected, tab_name)
997                        // Show the first line of the SQL query as hover text
998                        .on_hover_text(
999                            examples
1000                                .get(i) // Safely get the example string
1001                                .and_then(|s| s.lines().next()) // Get the first line
1002                                .unwrap_or(""), // Default to empty string if error/empty
1003                        );
1004
1005                    // 4. Handle tab click logic
1006                    if resp.clicked() && !is_selected {
1007                        selected_tab_index = i; // Update the selected index
1008
1009                        // Update the main query editor text with the clicked example
1010                        if let Some(example_query) = examples.get(i) {
1011                            self.query = example_query.clone(); // Set editor text
1012                            // Change is detected by render_query comparing before/after state
1013                            tracing::debug!(
1014                                "Switched SQL Query tab to Example {}, query text updated.",
1015                                i + 1
1016                            );
1017                        }
1018
1019                        // Store the newly selected index back into egui's memory
1020                        ui.memory_mut(|mem| mem.data.insert_persisted(tab_id, selected_tab_index));
1021                    }
1022                }
1023            }); // End horizontal_wrapped
1024
1025            ui.separator(); // Separator between tabs and editor
1026
1027            // 5. Render the ACTIVE query editor below the tabs
1028            ui.add(
1029                TextEdit::multiline(&mut self.query)
1030                    .desired_width(f32::INFINITY) // Take full available width
1031                    .desired_rows(6) // Set preferred number of visible lines
1032                    .font(egui::TextStyle::Monospace), // Use a monospace font for SQL
1033            )
1034            .on_hover_text(
1035                "Enter SQL query (Polars SQL).\n\
1036                Click Example tabs above.\n\
1037                Changes trigger reload on Apply/focus change.",
1038            );
1039        }); // End vertical group
1040        ui.end_row(); // End the row in the parent Grid layout
1041    }
1042
1043    /// Renders the collapsible section displaying SQL command examples.
1044    /// Uses `sql_commands` to generate examples relevant to the current `self.schema`.
1045    fn render_sql_examples(&self, ui: &mut Ui) {
1046        CollapsingHeader::new("SQL Command Examples")
1047            .default_open(false)
1048            .show(ui, |ui| {
1049                // Tip about quoting identifiers.
1050                let quoting_tip = "Tip: Use double quotes (\") or backticks (`) around column names with spaces or special characters (e.g., \"Column Name\" or `Column Name`).";
1051                ui.label(quoting_tip);
1052
1053                // Frame around the examples.
1054                Frame::default()
1055                    .stroke(Stroke::new(1.0_f32, Color32::GRAY))
1056                    .outer_margin(2.0)
1057                    .inner_margin(10.0)
1058                    .show(ui, |ui| {
1059                        // Link to Polars SQL documentation.
1060                        ui.vertical_centered(|ui| {
1061                             let polars_sql_url = "https://docs.pola.rs/api/python/stable/reference/sql/index.html";
1062                            ui.hyperlink_to("Polars SQL Reference", polars_sql_url).on_hover_text(polars_sql_url);
1063                        });
1064                        ui.separator();
1065
1066                        // Generate and display SQL examples based on the current schema.
1067                        // The `sql_commands` function (in `sqls.rs`) dynamically creates these.
1068                        let examples = sql_commands(&self.schema);
1069                        let mut ex_num = Vec::new();
1070                        for (index, example) in examples.iter().enumerate() {
1071                            ex_num.push(format!("Example {count}:\n{example}", count = index + 1));
1072                        }
1073
1074                        // Make the examples selectable for easy copying.
1075                        ui.add(egui::Label::new(ex_num.join("\n\n")).selectable(true));
1076                    });
1077            });
1078    }
1079}
1080
1081/// Reads a CSV file from the specified path using Polars, applying given options
1082/// and limiting the number of data rows read.
1083///
1084/// This function configures a Polars CsvReader with specific parsing and reading
1085/// options and executes the read operation directly from the file path.
1086/// It's suitable for getting the schema (when `infer_schema_length(Some(0))`) or
1087/// reading a limited number of initial data rows (`with_n_rows`).
1088///
1089/// ### Configuration Behavior:
1090/// - `has_header(true)`: Assumes the file has a header row for column names.
1091/// - `infer_schema_length(Some(0))`: Instructs Polars to infer column names from the
1092///   header row *only*, using default types (typically String), without using data
1093///   rows to guess types. If combined with `n_rows > 0`, it reads data
1094///   rows but ignores their content for type inference.
1095/// - `with_n_rows(n_rows)`: Limits the number of *data* rows parsed after the header.
1096/// - `ignore_errors(true)`: Skips rows/fields with parsing errors rather than stopping.
1097/// - `missing_is_null(true)`: Treats empty fields (`""`) as null values.
1098pub async fn read_csv_partial_from_path(
1099    delimiter: u8,
1100    n_rows: usize,
1101    path: &Path,
1102) -> PolarsViewResult<DataFrame> {
1103    tracing::debug!("Read a CSV file using Polars limited to {} rows.", n_rows,);
1104
1105    // 1. Define the CSV parsing options.
1106    let csv_parse_options = CsvParseOptions::default()
1107        .with_encoding(CsvEncoding::LossyUtf8) // Handle potentially non-strict UTF8
1108        .with_missing_is_null(true) // Treat empty fields as nulls
1109        .with_separator(delimiter); // Set the chosen delimiter
1110
1111    // 2. Define the main CSV reading options.
1112    let csv_read_options = CsvReadOptions::default()
1113        .with_parse_options(csv_parse_options) // Apply the parsing sub-options
1114        .with_has_header(true) // File has a header row
1115        .with_infer_schema_length(Some(0)) // Number of rows to use for schema inference (0 means header only)
1116        .with_ignore_errors(true) // Allow skipping rows/fields that fail to parse
1117        .with_n_rows(Some(n_rows)) // Limits the number of rows to read.
1118        .try_into_reader_with_file_path(Some(path.to_path_buf()))?;
1119
1120    // 3. Execute the blocking read operation on a separate thread
1121    let df = execute_polars_blocking(move || csv_read_options.finish()).await?;
1122
1123    tracing::debug!("Partial CSV read complete. Shape: {:?}", df.shape());
1124    Ok(df)
1125}
1126
1127/// Builds a Polars Schema specifying DataType::String overrides for columns
1128/// whose names match a given regex pattern or wildcard.
1129///
1130/// This function creates a schema intended for the `with_dtypes` option
1131/// of Polars readers (like `LazyCsvReader`), ensuring specific columns are treated
1132/// as text regardless of their inferred type.
1133fn build_dtype_override_schema(
1134    input_schema: &Arc<Schema>,
1135    regex_pattern: &str,
1136) -> PolarsViewResult<Schema> {
1137    let mut overrides_schema = Schema::default(); // Initialize the resulting schema
1138
1139    // --- Handle Wildcard Case ("*") ---
1140    // If the pattern is "*", override ALL columns to String.
1141    if regex_pattern.trim() == "*" {
1142        tracing::debug!(
1143            "Wildcard pattern '{regex_pattern}' provided. Overriding all columns to String."
1144        );
1145        return Ok(input_schema.as_ref().clone()); // Return the fully populated override schema
1146    }
1147
1148    // --- Handle Specific Regex Pattern Case ---
1149    // If it's not a wildcard, compile the regex pattern.
1150
1151    // Validate the required ^...$ format *before* compiling
1152    if !(regex_pattern.starts_with('^') && regex_pattern.ends_with('$')) {
1153        return Err(PolarsViewError::InvalidRegexPattern(
1154            regex_pattern.to_string(),
1155        ));
1156    }
1157
1158    // Attempt to compile the regex
1159    let compiled_regex = match Regex::new(regex_pattern) {
1160        Ok(re) => re,
1161        Err(e) => {
1162            // Return specific error for invalid syntax
1163            return Err(PolarsViewError::InvalidRegexSyntax {
1164                pattern: regex_pattern.to_string(),
1165                error: e.to_string(),
1166            });
1167        }
1168    };
1169
1170    // Check this compiled regex against each actual header name.
1171    for col_name in input_schema.iter_names() {
1172        if compiled_regex.is_match(col_name) {
1173            // Insert the override into the schema.
1174            overrides_schema.insert(col_name.clone(), DataType::String);
1175        }
1176    }
1177
1178    // Log the final outcome for debugging purposes.
1179    if !overrides_schema.is_empty() {
1180        tracing::debug!(
1181            override_cols = ?overrides_schema.iter_names().collect::<Vec<_>>(),
1182            "Pattern '{}' matched {} columns: ",
1183            regex_pattern,
1184            overrides_schema.len()
1185        );
1186    } else {
1187        tracing::debug!("Provided regex patterns did not match any header columns.");
1188    }
1189
1190    Ok(overrides_schema) // Return the successfully built schema (might be empty)
1191}
1192
1193/// Helper function to find a unique column name based on a base name and schema.
1194/// Appends suffixes "_1", "_2", etc., if the base name conflicts with existing column names.
1195fn resolve_unique_column_name(base_name: &str, schema: &Schema) -> PolarsResult<PlSmallStr> {
1196    // Check if the base name is available first (most common case)
1197    if schema.get(base_name).is_none() {
1198        tracing::debug!("Base name '{}' is available.", base_name);
1199        return Ok(base_name.into());
1200    }
1201
1202    // Base name conflicts, generate alternative names with suffixes.
1203    tracing::debug!(
1204        "Base name '{}' conflicts. Searching unique name.",
1205        base_name
1206    );
1207    let mut suffix_counter = 1u32;
1208    loop {
1209        let candidate_name = format!("{base_name}_{suffix_counter}");
1210
1211        if schema.get(&candidate_name).is_none() {
1212            // Found a unique name
1213            tracing::debug!("Found unique name: '{}'.", candidate_name);
1214            return Ok(candidate_name.into()); // Return the unique name
1215        }
1216
1217        // Safety check for potential overflow and limit attempts
1218        suffix_counter = suffix_counter.checked_add(1).unwrap_or(MAX_ATTEMPTS); // If overflow, go to max attempts
1219
1220        // Prevent infinite loops. Return error if a unique name cannot be found after max attempts.
1221        if suffix_counter >= MAX_ATTEMPTS {
1222            let msg = format!(
1223                "Failed to find a unique column name starting with '{base_name}' after {MAX_ATTEMPTS} attempts."
1224            );
1225            tracing::error!("{}", msg);
1226            return Err(PolarsError::ComputeError(msg.into()));
1227        }
1228    }
1229}
1230
1231/// Executes a potentially blocking Polars operation on a separate Tokio blocking thread.
1232///
1233/// Wraps the closure `op` which is expected to return a `PolarsResult<T>`,
1234/// runs it with `spawn_blocking`, awaits the result, and maps both the
1235/// `JoinError` and the inner `PolarsError` to `PolarsViewError`.
1236///
1237/// ### Arguments
1238/// * `op`: A closure that performs the blocking work and returns `PolarsResult<T>`.
1239///   It must be `Send` and have a `'static` lifetime, meaning it must
1240///   take ownership of or only use data that can be moved across threads
1241///   and lives for the duration of the program (or the task).
1242///
1243/// ### Returns
1244/// A `PolarsViewResult<T>` containing the result of the operation `T` on success,
1245/// or a mapped `PolarsViewError` if the spawned task fails (`TokioJoin`) or
1246/// the Polars operation itself fails (`Polars`).
1247async fn execute_polars_blocking<T, F>(op: F) -> PolarsViewResult<T>
1248where
1249    // F is the type of the closure
1250    F: FnOnce() -> Result<T, PolarsError> + Send + 'static, // The closure trait bounds
1251    // T is the success type returned by the closure (e.g., DataFrame)
1252    T: Debug + Send + 'static, // The success type must be Send and have static lifetime
1253                               // PolarsError: Debug,
1254{
1255    // Spawn the blocking task
1256    let result_from_task = spawn_blocking(op).await; // Result<Result<T, PolarsError>, JoinError>
1257
1258    // Map JoinError to PolarsViewError::TokioJoin
1259    let polars_result = result_from_task.map_err(PolarsViewError::from)?; // Requires PolarsViewError::from(JoinError)
1260
1261    // Map PolarsError to PolarsViewError::Polars
1262    let final_result = polars_result.map_err(PolarsViewError::from)?; // Requires PolarsViewError::from(PolarsError)
1263
1264    Ok(final_result) // Return the successfully extracted value or the mapped PolarsError
1265}
1266
1267//----------------------------------------------------------------------------//
1268//                                   Tests                                    //
1269//----------------------------------------------------------------------------//
1270
1271/// Run tests with:
1272/// cargo test -- --show-output tests_override_columns
1273#[cfg(test)]
1274mod tests_override_columns {
1275    use super::*;
1276    use std::{fs::File, io::Write};
1277    use tempfile::NamedTempFile;
1278
1279    // --- Test Setup Helper (Unchanged conceptually, ensure path and error mapping ok) ---
1280    fn setup_test_csv(
1281        content: &str, // CSV content as string
1282        delimiter: char,
1283        force_string_patterns: Option<String>, // Regex Columns to configure for override
1284    ) -> PolarsViewResult<(NamedTempFile, DataFilter)> {
1285        let temp_file = NamedTempFile::new()?;
1286        let file_path = temp_file.path().to_path_buf();
1287
1288        // Write content to the temp file
1289        let mut file = File::create(&file_path)?;
1290        file.write_all(content.as_bytes())?;
1291        file.flush()?; // Ensure data is written
1292
1293        // Create DataFilter using struct update syntax (Clippy Fix)
1294        let filter = DataFilter {
1295            absolute_path: file_path,             // Set specific value
1296            force_string_patterns,                // Set specific value
1297            csv_delimiter: delimiter.to_string(), // Set specific value
1298            ..Default::default()                  // Fill the rest with defaults
1299        };
1300
1301        Ok((temp_file, filter))
1302    }
1303
1304    // --- Test Case 1: Override applied correctly ---
1305    #[tokio::test] // Requires tokio features in dev-dependencies
1306    async fn test_csv_read_with_override_success() -> PolarsViewResult<()> {
1307        println!("\n--- Test: Override Applied Successfully ---");
1308        // 1. Define CSV Content with large numbers AS TEXT
1309        let csv_content = "\
1310long_id;value;text
131112345678901234567890123456789012345678901234;10.5;abc
131298765432109876543210987654321098765432109876;20.0;def
131312345;30.7;ghi";
1314        // No need for df_input - the csv_content is the direct input representation
1315        println!("Input CSV Content:\n{csv_content}\n");
1316
1317        // 2. Define Expected Output DataFrame (long_id is String)
1318        let df_expected = df!(
1319            "long_id" => &[
1320                "12345678901234567890123456789012345678901234",
1321                "98765432109876543210987654321098765432109876",
1322                "12345"
1323            ],
1324            "value" => &[10.5, 20.0, 30.7],
1325            "text" => &["abc", "def", "ghi"]
1326        )
1327        .expect("Failed to create expected DataFrame");
1328        println!("Expected DF (After Read):\n{df_expected}");
1329
1330        // 3. Setup: Use helper to create CSV and Filter WITH the override
1331        let delimiter = ';';
1332        let col_regex = "^long_id$".to_string();
1333        let (_temp_file, filter) = // Keep _temp_file handle!
1334                setup_test_csv(csv_content, delimiter, Some(col_regex))?;
1335
1336        let schema = filter
1337            .attempt_csv_parse_structure(delimiter as u8, false)
1338            .await?;
1339        println!("schema: {schema:#?}");
1340
1341        // 4. Execute the function under test
1342        let lazyframe = filter.attempt_read_csv(delimiter as u8, &schema).await?;
1343        println!("get lazyframe");
1344
1345        // Execute the lazy plan and collect into an eager DataFrame
1346        // *** WRAP COLLECT IN SPAWN_BLOCKING ***
1347        let df_output =
1348            execute_polars_blocking(move || lazyframe.with_streaming(true).collect()).await?;
1349
1350        println!("Output DF (Actual Read):\n{df_output}");
1351
1352        // 5. Assertions
1353        assert_eq!(
1354            df_output.schema().get("long_id"),
1355            Some(&DataType::String),
1356            "Schema Check Failed: 'long_id' should be DataType::String"
1357        );
1358        assert_eq!(
1359            df_output.schema().get("value"),
1360            Some(&DataType::Float64),
1361            "Schema Check Failed: 'value' should be DataType::Float64"
1362        );
1363        assert_eq!(
1364            df_output.schema().get("text"),
1365            Some(&DataType::String),
1366            "Schema Check Failed: 'text' should be DataType::String"
1367        );
1368        assert_eq!(
1369            df_output, df_expected,
1370            "Content Check Failed: Output DF does not match expected DF"
1371        );
1372
1373        Ok(())
1374    }
1375
1376    // --- Test Case 2: Override *not* applied (expect nulls) ---
1377    #[tokio::test] // Requires tokio features in dev-dependencies
1378    async fn test_csv_read_without_override_yields_nulls() -> PolarsViewResult<()> {
1379        println!("\n--- Test: No Override Applied (Expect Nulls) ---");
1380        // 1. Define CSV Content (same large numbers AS TEXT)
1381        let csv_content = "\
1382long_id;value;text
138312345678901234567890123456789012345678901234;10.5;abc
138498765432109876543210987654321098765432109876;20.0;def";
1385        println!("Input CSV Content:\n{csv_content}\n");
1386
1387        // 2. Define Expected Output Pattern (long_id should be all nulls)
1388        let df_expected_pattern = df!(
1389            "long_id" => Series::new_null("long_id".into(), 2).cast(&DataType::Int64)?, // Series of 2 nulls
1390            "value" => &[10.5, 20.0],
1391            "text" => &["abc", "def"]
1392        )
1393        .expect("Failed to create expected pattern DataFrame");
1394        println!("Expected DF Pattern (After Read, note long_id nulls):\n{df_expected_pattern}");
1395
1396        // 3. Setup: Use helper with an EMPTY override list
1397        let delimiter = ';';
1398        let col_regex = "^Col Name$".to_string();
1399        let (_temp_file, filter) = setup_test_csv(csv_content, delimiter, Some(col_regex))?;
1400
1401        let schema = filter
1402            .attempt_csv_parse_structure(delimiter as u8, false)
1403            .await?;
1404        println!("schema: {schema:#?}");
1405
1406        // 4. Execute the function under test
1407        let lazyframe = filter.attempt_read_csv(delimiter as u8, &schema).await?;
1408        println!("get lazyframe");
1409
1410        // Execute the lazy plan and collect into an eager DataFrame
1411        // *** WRAP COLLECT IN SPAWN_BLOCKING ***
1412        let df_output = spawn_blocking(move || lazyframe.with_streaming(true).collect())
1413            .await
1414            .map_err(PolarsViewError::from)? // Convert Tokio JoinError to PolarsViewError
1415            .map_err(PolarsViewError::from)?; // Convert PolarsError to PolarsViewError
1416
1417        println!("Output DF (Actual Read):\n{df_output}");
1418
1419        // 5. Assertions
1420        let long_id_col = df_output.column("long_id")?;
1421        assert!(
1422            long_id_col.is_null().all(), // Verify ALL values are null
1423            "Content Check Failed: 'long_id' column should be all nulls without override. Type: {:?}, Null count: {}",
1424            long_id_col.dtype(),
1425            long_id_col.null_count()
1426        );
1427        // Verify other columns match the pattern
1428        assert_eq!(
1429            df_output.column("value")?,
1430            df_expected_pattern.column("value")?
1431        );
1432        assert_eq!(
1433            df_output.column("text")?,
1434            df_expected_pattern.column("text")?
1435        );
1436
1437        Ok(())
1438    }
1439}