fix(csv): unwrap export force-text markers
This commit is contained in:
1 parent
083f81bc03
commit
076c39bc21
1 file changed
+75
@@ -492,8 +492,29 @@ pub fn effective_delimited_config(
|
||||
})
|
||||
}
|
||||
|
||||
/// Undo the `="..."` force-text wrapper that CSV export writes around temporal
|
||||
/// cells (see `temporal_format::wrap_csv_force_text`). Without this, exporting a
|
||||
/// table with a datetime column and importing the file straight back stores the
|
||||
/// literal `="2026-06-24 02:00:07"` instead of the timestamp, which every
|
||||
/// temporal column type then rejects.
|
||||
///
|
||||
/// Only the exact wrapper shape is unwrapped: a leading `="`, a trailing `"`,
|
||||
/// and no `"` in between. A genuine value that merely starts with `=` (or
|
||||
/// contains quotes of its own) is left untouched, so this cannot corrupt CSV
|
||||
/// files that dbx did not produce.
|
||||
fn unwrap_csv_force_text(value: &str) -> &str {
|
||||
let Some(inner) = value.strip_prefix("=\"").and_then(|rest| rest.strip_suffix('"')) else {
|
||||
return value;
|
||||
};
|
||||
if inner.contains('"') {
|
||||
return value;
|
||||
}
|
||||
inner
|
||||
}
|
||||
|
||||
pub fn csv_value_with_config(value: &str, config: DelimitedParseConfig) -> serde_json::Value {
|
||||
let value = if config.trim_values { value.trim() } else { value };
|
||||
let value = unwrap_csv_force_text(value);
|
||||
if config.empty_string_as_null && value.is_empty() {
|
||||
serde_json::Value::Null
|
||||
} else {
|
||||
@@ -9690,6 +9711,60 @@ mod tests {
|
||||
assert_eq!(text_batches[0].sql, "INSERT INTO \"issue_6491\" (\"amount\") VALUES\n('1,234.56'),\n('12,345')");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_import_unwraps_the_force_text_wrapper_written_by_csv_export() {
|
||||
// Export wraps temporal cells as `="..."` so spreadsheets stop re-typing
|
||||
// them; re-importing that file has to produce the plain timestamp again.
|
||||
let parsed = parse_csv_bytes(b"insert_time,id\n\"=\"\"2026-06-24 02:00:07\"\"\",695350\n", 10).unwrap();
|
||||
|
||||
assert_eq!(parsed.rows[0][0], serde_json::Value::String("2026-06-24 02:00:07".to_string()));
|
||||
assert_eq!(parsed.rows[0][1], serde_json::Value::String("695350".to_string()));
|
||||
|
||||
let mappings = vec![
|
||||
TableImportColumnMapping {
|
||||
source_column: "insert_time".to_string(),
|
||||
target_column: "insert_time".to_string(),
|
||||
target_data_type: None,
|
||||
},
|
||||
TableImportColumnMapping {
|
||||
source_column: "id".to_string(),
|
||||
target_column: "id".to_string(),
|
||||
target_data_type: None,
|
||||
},
|
||||
];
|
||||
let batches = build_import_insert_batches(
|
||||
&parsed,
|
||||
&mappings,
|
||||
&[("insert_time".to_string(), "datetime".to_string()), ("id".to_string(), "bigint".to_string())],
|
||||
"issue_8803",
|
||||
"",
|
||||
&DatabaseType::Mysql,
|
||||
500,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
batches[0].sql,
|
||||
"INSERT INTO `issue_8803` (`insert_time`, `id`) VALUES\n('2026-06-24 02:00:07', 695350)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csv_import_keeps_values_that_only_look_like_the_force_text_wrapper() {
|
||||
// Formula-looking data that dbx did not write must survive untouched,
|
||||
// otherwise importing a third-party CSV silently rewrites its cells.
|
||||
let parsed = parse_csv_bytes(
|
||||
b"expr\n\"=\"\"a\"\"&\"\"b\"\"\"\n=SUM(A1:A2)\n\"=\"\"unterminated\"\n\"just \"\"quoted\"\" text\"\n",
|
||||
10,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(parsed.rows[0][0], serde_json::Value::String("=\"a\"&\"b\"".to_string()));
|
||||
assert_eq!(parsed.rows[1][0], serde_json::Value::String("=SUM(A1:A2)".to_string()));
|
||||
assert_eq!(parsed.rows[2][0], serde_json::Value::String("=\"unterminated".to_string()));
|
||||
assert_eq!(parsed.rows[3][0], serde_json::Value::String("just \"quoted\" text".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_only_excel_columns_mapped_to_text_targets() {
|
||||
let path = std::env::temp_dir().join(format!("dbx-table-import-display-formats-{}.xlsx", uuid::Uuid::new_v4()));
|
||||
|
||||
Reference in new issue
Block a user