feat(export): configurable CSV field quoting mode
This commit is contained in:
32 files changed
+442
-56
No files matched your search
@@ -10,8 +10,13 @@ use tokio_util::sync::CancellationToken;
|
||||
use crate::connection::MysqlMode;
|
||||
use crate::connection::{config_for_pool_key, task_client_session_id, AppState, PoolKind};
|
||||
#[cfg(test)]
|
||||
use crate::csv_export::format_csv;
|
||||
#[cfg(test)]
|
||||
use crate::csv_export::format_tsv_rows;
|
||||
use crate::csv_export::{format_csv, format_tsv, push_table_csv_row, push_tsv_row};
|
||||
use crate::csv_export::{
|
||||
format_csv_with_quote_mode, format_tsv, push_table_csv_row, push_table_csv_row_with_quote_mode, push_tsv_row,
|
||||
CsvQuoteMode,
|
||||
};
|
||||
pub use crate::database_export::ExportStatus;
|
||||
use crate::database_export::{
|
||||
build_export_insert_statements, is_export_cancelled, is_internal_export_column, BuildExportInsertStatementsOptions,
|
||||
@@ -47,6 +52,8 @@ pub struct TableExportRequest {
|
||||
/// "csv", "xlsx", "json", "markdown", "sql", or "txt"
|
||||
pub format: String,
|
||||
#[serde(default)]
|
||||
pub csv_quote_mode: CsvQuoteMode,
|
||||
#[serde(default)]
|
||||
pub columns: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
pub column_types: Option<Vec<Option<String>>>,
|
||||
@@ -100,11 +107,17 @@ fn format_csv_rows(rows: &[Vec<Value>]) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn write_table_text_row<W: Write>(file: &mut W, csv: bool, row: &[Value], buffer: &mut String) -> Result<(), String> {
|
||||
fn write_table_text_row<W: Write>(
|
||||
file: &mut W,
|
||||
csv: bool,
|
||||
row: &[Value],
|
||||
buffer: &mut String,
|
||||
csv_quote_mode: CsvQuoteMode,
|
||||
) -> Result<(), String> {
|
||||
buffer.clear();
|
||||
buffer.push('\n');
|
||||
if csv {
|
||||
push_table_csv_row(buffer, row);
|
||||
push_table_csv_row_with_quote_mode(buffer, row, csv_quote_mode);
|
||||
} else {
|
||||
push_tsv_row(buffer, row);
|
||||
}
|
||||
@@ -116,12 +129,13 @@ fn write_table_text_rows<W: Write>(
|
||||
csv: bool,
|
||||
rows: &[Vec<Value>],
|
||||
buffer: &mut String,
|
||||
csv_quote_mode: CsvQuoteMode,
|
||||
) -> Result<(), String> {
|
||||
buffer.clear();
|
||||
for row in rows {
|
||||
buffer.push('\n');
|
||||
if csv {
|
||||
push_table_csv_row(buffer, row);
|
||||
push_table_csv_row_with_quote_mode(buffer, row, csv_quote_mode);
|
||||
} else {
|
||||
push_tsv_row(buffer, row);
|
||||
}
|
||||
@@ -958,7 +972,7 @@ async fn try_export_native_table_stream(
|
||||
std::fs::File::create(&request.file_path).map_err(|e| format!("Failed to create file: {e}"))?,
|
||||
);
|
||||
file.write_all(b"\xEF\xBB\xBF").map_err(|e| format!("Failed to write BOM: {e}"))?;
|
||||
let header = format_csv(col_names, &[]);
|
||||
let header = format_csv_with_quote_mode(col_names, &[], request.csv_quote_mode);
|
||||
let header = header.strip_suffix('\n').unwrap_or(&header);
|
||||
file.write_all(header.as_bytes()).map_err(|e| format!("Failed to write CSV: {e}"))?;
|
||||
let mut row_buffer = String::new();
|
||||
@@ -977,7 +991,7 @@ async fn try_export_native_table_stream(
|
||||
column_types,
|
||||
request.date_time_format.as_deref(),
|
||||
);
|
||||
write_table_text_row(&mut file, true, formatted.as_ref(), &mut row_buffer)?;
|
||||
write_table_text_row(&mut file, true, formatted.as_ref(), &mut row_buffer, request.csv_quote_mode)?;
|
||||
rows_exported += 1;
|
||||
if rows_exported.is_multiple_of(progress_interval) {
|
||||
on_progress(TableExportProgress {
|
||||
@@ -1021,7 +1035,13 @@ async fn try_export_native_table_stream(
|
||||
column_types,
|
||||
request.date_time_format.as_deref(),
|
||||
);
|
||||
write_table_text_row(&mut file, false, formatted.as_ref(), &mut row_buffer)?;
|
||||
write_table_text_row(
|
||||
&mut file,
|
||||
false,
|
||||
formatted.as_ref(),
|
||||
&mut row_buffer,
|
||||
request.csv_quote_mode,
|
||||
)?;
|
||||
rows_exported += 1;
|
||||
if rows_exported.is_multiple_of(progress_interval) {
|
||||
on_progress(TableExportProgress {
|
||||
@@ -1578,12 +1598,19 @@ async fn export_table_data_core_inner(
|
||||
|
||||
if is_first_batch {
|
||||
// First batch: write header + rows via format_csv
|
||||
let csv_content = format_csv(&col_names, formatted_rows.as_ref());
|
||||
let csv_content =
|
||||
format_csv_with_quote_mode(&col_names, formatted_rows.as_ref(), request.csv_quote_mode);
|
||||
file.write_all(csv_content.as_bytes()).map_err(|e| format!("Failed to write CSV: {e}"))?;
|
||||
is_first_batch = false;
|
||||
} else {
|
||||
// Subsequent batches: write rows only (prepend newline for separation)
|
||||
write_table_text_rows(&mut file, true, formatted_rows.as_ref(), &mut text_buffer)?;
|
||||
write_table_text_rows(
|
||||
&mut file,
|
||||
true,
|
||||
formatted_rows.as_ref(),
|
||||
&mut text_buffer,
|
||||
request.csv_quote_mode,
|
||||
)?;
|
||||
}
|
||||
|
||||
rows_exported += row_count as u64;
|
||||
@@ -1665,10 +1692,22 @@ async fn export_table_data_core_inner(
|
||||
);
|
||||
|
||||
if is_first_batch {
|
||||
write_table_text_rows(&mut file, false, formatted_rows.as_ref(), &mut text_buffer)?;
|
||||
write_table_text_rows(
|
||||
&mut file,
|
||||
false,
|
||||
formatted_rows.as_ref(),
|
||||
&mut text_buffer,
|
||||
request.csv_quote_mode,
|
||||
)?;
|
||||
is_first_batch = false;
|
||||
} else {
|
||||
write_table_text_rows(&mut file, false, formatted_rows.as_ref(), &mut text_buffer)?;
|
||||
write_table_text_rows(
|
||||
&mut file,
|
||||
false,
|
||||
formatted_rows.as_ref(),
|
||||
&mut text_buffer,
|
||||
request.csv_quote_mode,
|
||||
)?;
|
||||
}
|
||||
|
||||
rows_exported += row_count as u64;
|
||||
@@ -2210,6 +2249,7 @@ mod tests {
|
||||
batch_size: Some(batch_size),
|
||||
row_limit,
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
@@ -2314,7 +2354,7 @@ mod tests {
|
||||
let mut output = Vec::new();
|
||||
let mut buffer = String::new();
|
||||
|
||||
write_table_text_row(&mut output, true, &row, &mut buffer).expect("write csv row");
|
||||
write_table_text_row(&mut output, true, &row, &mut buffer, CsvQuoteMode::All).expect("write csv row");
|
||||
assert_eq!(String::from_utf8(output).expect("utf8 csv"), "\n\"\",\"\",\"line\n\"\"two\"\"\"");
|
||||
}
|
||||
|
||||
@@ -2391,6 +2431,7 @@ mod tests {
|
||||
batch_size: Some(50),
|
||||
row_limit: None,
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
@@ -2447,6 +2488,7 @@ mod tests {
|
||||
batch_size: Some(50),
|
||||
row_limit: None,
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
@@ -2481,6 +2523,7 @@ mod tests {
|
||||
batch_size: Some(50),
|
||||
row_limit: None,
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
@@ -2510,6 +2553,7 @@ mod tests {
|
||||
batch_size: Some(50),
|
||||
row_limit: None,
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
@@ -2547,6 +2591,7 @@ mod tests {
|
||||
batch_size: Some(25),
|
||||
row_limit: None,
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
@@ -2596,6 +2641,7 @@ mod tests {
|
||||
batch_size: Some(500),
|
||||
row_limit: Some(1000),
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
@@ -2639,6 +2685,7 @@ mod tests {
|
||||
batch_size: Some(50),
|
||||
row_limit: None,
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
@@ -2710,6 +2757,7 @@ mod tests {
|
||||
batch_size: Some(100),
|
||||
row_limit: None,
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
@@ -2763,6 +2811,7 @@ mod tests {
|
||||
batch_size: Some(100),
|
||||
row_limit: None,
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
@@ -2818,6 +2867,7 @@ mod tests {
|
||||
batch_size: Some(100),
|
||||
row_limit: None,
|
||||
date_time_format: None,
|
||||
csv_quote_mode: CsvQuoteMode::All,
|
||||
numeric_column_right_align: false,
|
||||
column_comments: None,
|
||||
auto_filter: None,
|
||||
|
||||
Reference in new issue
Block a user