Skip to content

Commit c0cd6c5

Browse files
committed
Avoid the need for a patched arrow-csv with DataFrame level string escaping
I was hoping to have changes upstreamed into arrow-csv but without that, this should be sufficient Signed-off-by: R. Tyler Croy <rtyler@buoyantdata.com>
1 parent 1ee8a90 commit c0cd6c5

3 files changed

Lines changed: 32 additions & 19 deletions

File tree

Cargo.toml

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,6 @@ datafusion-datasource = { git = "https://github.com/buoyant-data/datafusion", br
3939
datafusion-physical-expr-adapter = { git = "https://github.com/buoyant-data/datafusion", branch = "buoyant/release-52.4.1" }
4040
datafusion-ffi = { git = "https://github.com/buoyant-data/datafusion", branch = "buoyant/release-52.4.1" }
4141
datafusion-proto = { git = "https://github.com/buoyant-data/datafusion", branch = "buoyant/release-52.4.1" }
42-
arrow = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
43-
arrow-array = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
44-
arrow-buffer = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
45-
arrow-cast = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
46-
arrow-data = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
47-
arrow-ipc = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
48-
arrow-json = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
49-
arrow-ord = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
50-
arrow-schema = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
51-
arrow-select = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
52-
arrow-csv = { git = "https://github.com/buoyant-data/arrow-rs", branch = "buoyant/release-57.3.1" }
53-
5442

5543
[profile.release]
5644
panic = "abort"

lambdas/cdf-to-csv/README.adoc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ s3://csvs/
5858
|
5959
| Optional primary key for delete CSV files
6060

61+
| `CSV_ESCAPE_FORWARD_SLASH`
62+
|
63+
| When this environment exists, `\` characters in CSV strings will be converted into `\\`
64+
6165
| `CSV_BOOL_AS_INT`
6266
|
6367
| When this environment variable exists, any boolean `true` will be written as `1` in the CSV output. False is encoded as `0`.

lambdas/cdf-to-csv/src/main.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,18 +197,27 @@ fn escape_dataframe(input: DataFrame) -> DeltaResult<DataFrame> {
197197
let mut df = input.clone();
198198
let schema = input.schema();
199199
for field in schema.fields() {
200+
for dt in [&DataType::LargeUtf8, &DataType::Utf8, &DataType::Utf8View] {
201+
if field.data_type() == dt {
202+
df = df.with_column(
203+
field.name(),
204+
replace(col(field.name()), lit("\n"), lit("\\n")),
205+
)?;
206+
207+
if std::env::var("CSV_ESCAPE_FORWARD_SLASH").is_ok() {
208+
df = df.with_column(
209+
field.name(),
210+
replace(col(field.name()), lit("\\"), lit("\\\\")),
211+
)?;
212+
}
213+
}
214+
}
200215
if field.data_type() == &DataType::Boolean && std::env::var("CSV_BOOL_AS_INT").is_ok() {
201216
if std::env::var("CSV_BOOL_NULL_AS_INT").is_ok() {
202217
df = df.fill_null(ScalarValue::from(0), [field.name().clone()].to_vec())?;
203218
}
204219
df = df.with_column(field.name(), cast(col(field.name()), DataType::Int32))?;
205220
}
206-
if field.data_type() == &DataType::Utf8 || field.data_type() == &DataType::LargeUtf8 {
207-
df = df.with_column(
208-
field.name(),
209-
replace(col(field.name()), lit("\n"), lit("\\n")),
210-
)?;
211-
}
212221
}
213222
Ok(df)
214223
}
@@ -358,7 +367,7 @@ mod tests {
358367
let mut insert_found = false;
359368
let mut delete_found = false;
360369
while let Some(Ok(entry)) = stream.next().await {
361-
println!("entry: {entry:?}");
370+
dbg!(&entry);
362371
if entry.location.prefix_matches(&Path::from("deletes")) {
363372
delete_found = true;
364373
}
@@ -457,6 +466,9 @@ mod tests {
457466
#[tokio::test]
458467
#[serial]
459468
async fn test_writing_with_newlines() -> DeltaResult<()> {
469+
unsafe {
470+
std::env::set_var("CSV_ESCAPE_FORWARD_SLASH", "1");
471+
}
460472
let ctx = SessionContext::new();
461473
let temp = tempfile::tempdir()?;
462474
let tempfile = temp.path().join("some.csv");
@@ -487,10 +499,19 @@ mod tests {
487499
lines.len(),
488500
"Should have only written four lines for the sample input data"
489501
);
502+
assert_eq!(
503+
lines[3].as_ref().expect("Failure"),
504+
r#""","209","149",":-\\","1.0""#,
505+
"The CSV output was not what we expected"
506+
);
490507

491508
let df = ctx.read_csv(tempfile, CsvReadOptions::default()).await?;
492509
assert_eq!(written_schema.as_arrow(), df.schema().as_arrow());
493510

511+
unsafe {
512+
std::env::remove_var("CSV_ESCAPE_FORWARD_SLASH");
513+
}
514+
494515
Ok(())
495516
}
496517
}

0 commit comments

Comments
 (0)