use anyhow::{Context, Result};
use rusqlite::Connection;
use std::path::Path;
pub fn import_csv_to_table(conn: &mut Connection, file_path: &str, table_name: &str) -> Result<()> {
let file = Path::new(file_path);
if !file.exists() {
anyhow::bail!("File not found: {}", file_path);
}
let mut rdr = csv::Reader::from_path(file_path)?;
let headers = rdr.headers()?.clone();
let tx = conn.transaction()?;
{
let sql = format!(
"INSERT INTO {} ({}) VALUES ({})",
table_name,
headers
.iter()
.map(|h| format!("\"{}\"", h))
.collect::<Vec<_>>()
.join(","),
headers.iter().map(|_| "?").collect::<Vec<_>>().join(",")
);
let mut stmt = tx.prepare(&sql)?;
for result in rdr.records() {
let record = result?;
let params: Vec<&str> = record.iter().collect();
stmt.execute(rusqlite::params_from_iter(params))?;
}
}
tx.commit()?;
Ok(())
}
pub fn export_to_csv(conn: &Connection, query: &str, filename: &str) -> Result<()> {
validate_export_inputs(query, filename)?;
let mut stmt = conn.prepare(query).with_context(|| {
format!(
"Failed to prepare export query. Check SQL syntax: {}",
query
)
})?;
let column_names: Vec<String> = stmt.column_names().iter().map(|&s| s.to_string()).collect();
if column_names.is_empty() {
anyhow::bail!(
"Query returned no columns. Make sure your query includes SELECT statements."
);
}
let mut wtr = csv::Writer::from_path(filename).with_context(|| {
format!(
"Failed to create CSV file '{}'. Check permissions and disk space.",
filename
)
})?;
wtr.write_record(&column_names)
.with_context(|| format!("Failed to write CSV header to '{}'", filename))?;
let mut rows = stmt
.query([])
.with_context(|| format!("Failed to execute export query: {}", query))?;
let mut row_count = 0;
let mut error_count = 0;
while let Some(row) = rows
.next()
.with_context(|| format!("Failed to fetch row {} from query results", row_count + 1))?
{
match process_row(&row, &column_names) {
Ok(record) => {
if let Err(e) = wtr.write_record(&record) {
error_count += 1;
eprintln!("Warning: Failed to write row {}: {}", row_count + 1, e);
if error_count > 10 {
anyhow::bail!("Too many write errors ({}). Stopping export.", error_count);
}
} else {
row_count += 1;
if row_count % 10000 == 0 {
println!("Exported {} rows...", row_count);
}
}
}
Err(e) => {
error_count += 1;
eprintln!("Warning: Failed to process row {}: {}", row_count + 1, e);
if error_count > 10 {
anyhow::bail!(
"Too many processing errors ({}). Stopping export.",
error_count
);
}
}
}
}
wtr.flush()
.with_context(|| format!("Failed to flush data to CSV file '{}'", filename))?;
verify_export_file(filename, row_count)?;
if error_count > 0 {
println!("Export completed with {} warning(s)", error_count);
}
println!("Successfully exported {} rows to '{}'", row_count, filename);
Ok(())
}
fn validate_export_inputs(query: &str, filename: &str) -> Result<()> {
if query.trim().is_empty() {
anyhow::bail!("Export query cannot be empty");
}
let query_lower = query.to_lowercase();
if !query_lower.contains("select") {
anyhow::bail!("Export query must contain a SELECT statement");
}
let dangerous_keywords = ["drop", "delete", "update", "insert", "create", "alter"];
for keyword in &dangerous_keywords {
if query_lower.contains(keyword) {
eprintln!(
"Warning: Query contains '{}' - this may modify data",
keyword
);
}
}
if filename.trim().is_empty() {
anyhow::bail!("Filename cannot be empty");
}
if filename.len() > 255 {
anyhow::bail!("Filename is too long (maximum 255 characters)");
}
let path = Path::new(filename);
if let Some(file_name) = path.file_name() {
let name_str = file_name.to_string_lossy();
if name_str
.chars()
.any(|c| c.is_control() || "\\:*?\"<>|".contains(c))
{
anyhow::bail!("Filename contains invalid characters. Avoid: \\ : * ? \" < > |");
}
}
if Path::new(filename).exists() {
eprintln!(
"Warning: File '{}' already exists and will be overwritten",
filename
);
}
if let Some(parent) = Path::new(filename).parent() {
if parent != Path::new("") && !parent.exists() {
anyhow::bail!(
"Directory '{}' does not exist. Create it first or use a different path.",
parent.display()
);
}
}
Ok(())
}
fn process_row(row: &rusqlite::Row, column_names: &[String]) -> Result<Vec<String>> {
let mut record = Vec::with_capacity(column_names.len());
for i in 0..column_names.len() {
let val: rusqlite::types::Value = row.get(i).with_context(|| {
format!(
"Failed to get value from column {} ('{}')",
i, column_names[i]
)
})?;
let value_str = match val {
rusqlite::types::Value::Null => String::new(),
rusqlite::types::Value::Integer(i) => i.to_string(),
rusqlite::types::Value::Real(f) => {
if f.is_nan() {
"NaN".to_string()
} else if f.is_infinite() {
if f.is_sign_positive() {
"Infinity".to_string()
} else {
"-Infinity".to_string()
}
} else {
f.to_string()
}
}
rusqlite::types::Value::Text(t) => {
if t.contains(',') || t.contains('"') || t.contains('\n') {
format!("\"{}\"", t.replace('"', "\"\""))
} else {
t
}
}
rusqlite::types::Value::Blob(b) => {
format!("[BLOB {} bytes]", b.len())
}
};
record.push(value_str);
}
Ok(record)
}
fn verify_export_file(filename: &str, expected_rows: usize) -> Result<()> {
let path = Path::new(filename);
if !path.exists() {
anyhow::bail!("Export file '{}' was not created", filename);
}
let metadata = std::fs::metadata(path)
.with_context(|| format!("Cannot read metadata for export file '{}'", filename))?;
if metadata.len() == 0 && expected_rows > 0 {
anyhow::bail!(
"Export file '{}' is empty but {} rows were expected",
filename,
expected_rows
);
}
let file_size = metadata.len();
if file_size > 0 {
println!("Export file size: {} bytes", file_size);
}
Ok(())
}