feat(export): configurable CSV field quoting mode

This commit is contained in:
zipg authored and GitHub committed 2026-09-03 17:51:50 +08:00
1 parent d421bd33fc
commit 203eab3dec
32 files changed
+442 -56

No files matched your search

+62 -12
View File
@@ -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,