1use anyhow::{Context, Result};
15use rusqlite::Connection;
16use std::path::Path;
17
18pub fn import_csv_to_table(conn: &mut Connection, file_path: &str, table_name: &str) -> Result<()> {
35 let file = Path::new(file_path);
36 if !file.exists() {
37 anyhow::bail!("File not found: {}", file_path);
38 }
39
40 let mut rdr = csv::Reader::from_path(file_path)?;
41 let headers = rdr.headers()?.clone();
42
43 let tx = conn.transaction()?;
44
45 {
46 let sql = format!(
47 "INSERT INTO {} ({}) VALUES ({})",
48 table_name,
49 headers
50 .iter()
51 .map(|h| format!("\"{}\"", h))
52 .collect::<Vec<_>>()
53 .join(","),
54 headers.iter().map(|_| "?").collect::<Vec<_>>().join(",")
55 );
56
57 let mut stmt = tx.prepare(&sql)?;
58
59 for result in rdr.records() {
60 let record = result?;
61 let params: Vec<&str> = record.iter().collect();
62 stmt.execute(rusqlite::params_from_iter(params))?;
63 }
64 } tx.commit()?;
67 Ok(())
68}
69
70pub fn export_to_csv(conn: &Connection, query: &str, filename: &str) -> Result<()> {
87 validate_export_inputs(query, filename)?;
89
90 let mut stmt = conn.prepare(query).with_context(|| {
92 format!(
93 "Failed to prepare export query. Check SQL syntax: {}",
94 query
95 )
96 })?;
97
98 let column_names: Vec<String> = stmt.column_names().iter().map(|&s| s.to_string()).collect();
99
100 if column_names.is_empty() {
101 anyhow::bail!(
102 "Query returned no columns. Make sure your query includes SELECT statements."
103 );
104 }
105
106 let mut wtr = csv::Writer::from_path(filename).with_context(|| {
108 format!(
109 "Failed to create CSV file '{}'. Check permissions and disk space.",
110 filename
111 )
112 })?;
113
114 wtr.write_record(&column_names)
116 .with_context(|| format!("Failed to write CSV header to '{}'", filename))?;
117
118 let mut rows = stmt
120 .query([])
121 .with_context(|| format!("Failed to execute export query: {}", query))?;
122
123 let mut row_count = 0;
124 let mut error_count = 0;
125
126 while let Some(row) = rows
127 .next()
128 .with_context(|| format!("Failed to fetch row {} from query results", row_count + 1))?
129 {
130 match process_row(&row, &column_names) {
131 Ok(record) => {
132 if let Err(e) = wtr.write_record(&record) {
133 error_count += 1;
134 eprintln!("Warning: Failed to write row {}: {}", row_count + 1, e);
135
136 if error_count > 10 {
137 anyhow::bail!("Too many write errors ({}). Stopping export.", error_count);
138 }
139 } else {
140 row_count += 1;
141
142 if row_count % 10000 == 0 {
144 println!("Exported {} rows...", row_count);
145 }
146 }
147 }
148 Err(e) => {
149 error_count += 1;
150 eprintln!("Warning: Failed to process row {}: {}", row_count + 1, e);
151
152 if error_count > 10 {
153 anyhow::bail!(
154 "Too many processing errors ({}). Stopping export.",
155 error_count
156 );
157 }
158 }
159 }
160 }
161
162 wtr.flush()
164 .with_context(|| format!("Failed to flush data to CSV file '{}'", filename))?;
165
166 verify_export_file(filename, row_count)?;
168
169 if error_count > 0 {
170 println!("Export completed with {} warning(s)", error_count);
171 }
172
173 println!("Successfully exported {} rows to '{}'", row_count, filename);
174
175 Ok(())
176}
177
178fn validate_export_inputs(query: &str, filename: &str) -> Result<()> {
187 if query.trim().is_empty() {
189 anyhow::bail!("Export query cannot be empty");
190 }
191
192 let query_lower = query.to_lowercase();
193 if !query_lower.contains("select") {
194 anyhow::bail!("Export query must contain a SELECT statement");
195 }
196
197 let dangerous_keywords = ["drop", "delete", "update", "insert", "create", "alter"];
199 for keyword in &dangerous_keywords {
200 if query_lower.contains(keyword) {
201 eprintln!(
202 "Warning: Query contains '{}' - this may modify data",
203 keyword
204 );
205 }
206 }
207
208 if filename.trim().is_empty() {
210 anyhow::bail!("Filename cannot be empty");
211 }
212
213 if filename.len() > 255 {
214 anyhow::bail!("Filename is too long (maximum 255 characters)");
215 }
216
217 let path = Path::new(filename);
219 if let Some(file_name) = path.file_name() {
220 let name_str = file_name.to_string_lossy();
221 if name_str
223 .chars()
224 .any(|c| c.is_control() || "\\:*?\"<>|".contains(c))
225 {
226 anyhow::bail!("Filename contains invalid characters. Avoid: \\ : * ? \" < > |");
227 }
228 }
229
230 if Path::new(filename).exists() {
232 eprintln!(
233 "Warning: File '{}' already exists and will be overwritten",
234 filename
235 );
236 }
237
238 if let Some(parent) = Path::new(filename).parent() {
240 if parent != Path::new("") && !parent.exists() {
241 anyhow::bail!(
242 "Directory '{}' does not exist. Create it first or use a different path.",
243 parent.display()
244 );
245 }
246 }
247
248 Ok(())
249}
250
251fn process_row(row: &rusqlite::Row, column_names: &[String]) -> Result<Vec<String>> {
256 let mut record = Vec::with_capacity(column_names.len());
257
258 for i in 0..column_names.len() {
259 let val: rusqlite::types::Value = row.get(i).with_context(|| {
260 format!(
261 "Failed to get value from column {} ('{}')",
262 i, column_names[i]
263 )
264 })?;
265
266 let value_str = match val {
267 rusqlite::types::Value::Null => String::new(),
268 rusqlite::types::Value::Integer(i) => i.to_string(),
269 rusqlite::types::Value::Real(f) => {
270 if f.is_nan() {
272 "NaN".to_string()
273 } else if f.is_infinite() {
274 if f.is_sign_positive() {
275 "Infinity".to_string()
276 } else {
277 "-Infinity".to_string()
278 }
279 } else {
280 f.to_string()
281 }
282 }
283 rusqlite::types::Value::Text(t) => {
284 if t.contains(',') || t.contains('"') || t.contains('\n') {
286 format!("\"{}\"", t.replace('"', "\"\""))
287 } else {
288 t
289 }
290 }
291 rusqlite::types::Value::Blob(b) => {
292 format!("[BLOB {} bytes]", b.len())
294 }
295 };
296
297 record.push(value_str);
298 }
299
300 Ok(record)
301}
302
303fn verify_export_file(filename: &str, expected_rows: usize) -> Result<()> {
307 let path = Path::new(filename);
308
309 if !path.exists() {
310 anyhow::bail!("Export file '{}' was not created", filename);
311 }
312
313 let metadata = std::fs::metadata(path)
314 .with_context(|| format!("Cannot read metadata for export file '{}'", filename))?;
315
316 if metadata.len() == 0 && expected_rows > 0 {
317 anyhow::bail!(
318 "Export file '{}' is empty but {} rows were expected",
319 filename,
320 expected_rows
321 );
322 }
323
324 let file_size = metadata.len();
326 if file_size > 0 {
327 println!("Export file size: {} bytes", file_size);
328 }
329
330 Ok(())
331}