sql_cli/services/
data_loader_service.rs

1use crate::data::csv_datasource::CsvApiClient;
2use crate::data::data_view::DataView;
3use crate::data::datatable::DataTable;
4use crate::ui::utils::enhanced_tui_helpers;
5use anyhow::Result;
6use std::path::Path;
7use std::sync::Arc;
8use tracing::{info, warn};
9
10/// Service responsible for loading data from various sources
11/// This encapsulates all file loading logic that was previously in the TUI
12pub struct DataLoaderService {
13    case_insensitive: bool,
14}
15
16impl DataLoaderService {
17    #[must_use]
18    pub fn new(case_insensitive: bool) -> Self {
19        Self { case_insensitive }
20    }
21
22    /// Load a file and return a `DataView`
23    /// The TUI doesn't need to know about file types or loading strategies
24    pub fn load_file(&self, file_path: &str) -> Result<DataLoadResult> {
25        let path = Path::new(file_path);
26        let extension = path
27            .extension()
28            .and_then(|e| e.to_str())
29            .ok_or_else(|| anyhow::anyhow!("File has no extension: {}", file_path))?;
30
31        let raw_table_name = path
32            .file_stem()
33            .and_then(|s| s.to_str())
34            .unwrap_or("data")
35            .to_string();
36
37        // Sanitize the table name to be SQL-friendly
38        let table_name = enhanced_tui_helpers::sanitize_table_name(&raw_table_name);
39
40        match extension.to_lowercase().as_str() {
41            "csv" => self.load_csv(file_path, &table_name, &raw_table_name),
42            "json" => self.load_json(file_path, &table_name, &raw_table_name),
43            _ => Err(anyhow::anyhow!(
44                "Unsupported file type: {}. Use .csv or .json files.",
45                extension
46            )),
47        }
48    }
49
50    /// Load a CSV file
51    fn load_csv(
52        &self,
53        file_path: &str,
54        table_name: &str,
55        raw_table_name: &str,
56    ) -> Result<DataLoadResult> {
57        info!("Loading CSV file: {}", file_path);
58        let start = std::time::Instant::now();
59
60        // Try advanced loader first (with string interning)
61        let datatable = match crate::data::advanced_csv_loader::AdvancedCsvLoader::new()
62            .load_csv_optimized(file_path, table_name)
63        {
64            Ok(dt) => {
65                info!("Successfully loaded CSV with advanced optimizations");
66                dt
67            }
68            Err(e) => {
69                warn!(
70                    "Advanced CSV loader failed: {}, falling back to standard loader",
71                    e
72                );
73                crate::data::datatable_loaders::load_csv_to_datatable(file_path, table_name)?
74            }
75        };
76
77        self.create_result(
78            datatable,
79            file_path.to_string(),
80            table_name.to_string(),
81            raw_table_name.to_string(),
82            start.elapsed(),
83        )
84    }
85
86    /// Load a JSON file
87    fn load_json(
88        &self,
89        file_path: &str,
90        table_name: &str,
91        raw_table_name: &str,
92    ) -> Result<DataLoadResult> {
93        info!("Loading JSON file: {}", file_path);
94        let start = std::time::Instant::now();
95
96        let datatable =
97            crate::data::datatable_loaders::load_json_to_datatable(file_path, table_name)?;
98
99        self.create_result(
100            datatable,
101            file_path.to_string(),
102            table_name.to_string(),
103            raw_table_name.to_string(),
104            start.elapsed(),
105        )
106    }
107
108    /// Load data using `CsvApiClient` (for additional files)
109    pub fn load_with_client(&self, file_path: &str) -> Result<DataLoadResult> {
110        let mut csv_client = CsvApiClient::new();
111        csv_client.set_case_insensitive(self.case_insensitive);
112
113        let path = Path::new(file_path);
114        let raw_table_name = path
115            .file_stem()
116            .and_then(|s| s.to_str())
117            .unwrap_or("data")
118            .to_string();
119
120        // Sanitize the table name to be SQL-friendly
121        let table_name = enhanced_tui_helpers::sanitize_table_name(&raw_table_name);
122
123        let extension = path
124            .extension()
125            .and_then(|e| e.to_str())
126            .ok_or_else(|| anyhow::anyhow!("File has no extension: {}", file_path))?;
127
128        let start = std::time::Instant::now();
129
130        match extension.to_lowercase().as_str() {
131            "csv" => csv_client.load_csv(file_path, &table_name)?,
132            "json" => csv_client.load_json(file_path, &table_name)?,
133            _ => return Err(anyhow::anyhow!("Unsupported file type: {}", extension)),
134        }
135
136        // Get the DataTable from the client
137        let datatable = csv_client
138            .get_datatable()
139            .ok_or_else(|| anyhow::anyhow!("Failed to load data from {}", file_path))?;
140
141        self.create_result(
142            datatable,
143            file_path.to_string(),
144            table_name,
145            raw_table_name,
146            start.elapsed(),
147        )
148    }
149
150    /// Create a `DataLoadResult` from a `DataTable`
151    fn create_result(
152        &self,
153        datatable: DataTable,
154        source_path: String,
155        table_name: String,
156        raw_table_name: String,
157        load_time: std::time::Duration,
158    ) -> Result<DataLoadResult> {
159        // Create initial DataView
160        let initial_row_count = datatable.row_count();
161        let initial_column_count = datatable.column_count();
162
163        // Create DataView
164        let dataview = DataView::new(Arc::new(datatable));
165
166        Ok(DataLoadResult {
167            dataview,
168            source_path,
169            table_name,
170            raw_table_name,
171            initial_row_count,
172            initial_column_count,
173            load_time,
174        })
175    }
176
177    /// Update configuration
178    pub fn set_case_insensitive(&mut self, case_insensitive: bool) {
179        self.case_insensitive = case_insensitive;
180    }
181}
182
183/// Result of loading data
184pub struct DataLoadResult {
185    /// The loaded `DataView` ready for display
186    pub dataview: DataView,
187
188    /// Path to the source file
189    pub source_path: String,
190
191    /// SQL-friendly table name
192    pub table_name: String,
193
194    /// Original table name (before sanitization)
195    pub raw_table_name: String,
196
197    /// Initial row count (before any filtering)
198    pub initial_row_count: usize,
199
200    /// Initial column count
201    pub initial_column_count: usize,
202
203    /// Time taken to load the file
204    pub load_time: std::time::Duration,
205}
206
207impl DataLoadResult {
208    /// Generate a status message for the load operation
209    #[must_use]
210    pub fn status_message(&self) -> String {
211        format!(
212            "Loaded {} ({} rows, {} columns) in {} ms",
213            self.source_path,
214            self.initial_row_count,
215            self.initial_column_count,
216            self.load_time.as_millis()
217        )
218    }
219}