This is an automated email from the ASF dual-hosted git repository.

JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git


The following commit(s) were added to refs/heads/master by this push:
     new 9ad161e120 [format] Escape the escape character when writing CSV 
(#9278)
9ad161e120 is described below

commit 9ad161e120d61cbbf194bf7fb9f48b1f978963ff
Author: ZIHAN DAI <[email protected]>
AuthorDate: Thu Aug 20 12:02:32 2026 +1000

    [format] Escape the escape character when writing CSV (#9278)
---
 .../apache/paimon/format/csv/CsvFormatWriter.java  | 18 +++++++++------
 .../paimon/format/csv/CsvFileFormatTest.java       | 27 ++++++++++++++++++++++
 2 files changed, 38 insertions(+), 7 deletions(-)

diff --git 
a/paimon-format/src/main/java/org/apache/paimon/format/csv/CsvFormatWriter.java 
b/paimon-format/src/main/java/org/apache/paimon/format/csv/CsvFormatWriter.java
index 2004eece23..6de063d0d7 100644
--- 
a/paimon-format/src/main/java/org/apache/paimon/format/csv/CsvFormatWriter.java
+++ 
b/paimon-format/src/main/java/org/apache/paimon/format/csv/CsvFormatWriter.java
@@ -98,22 +98,26 @@ public class CsvFormatWriter extends AbstractTextFileWriter 
{
             return csvOptions.nullLiteral();
         }
 
+        String quote = csvOptions.quoteCharacter();
+        String escape = csvOptions.escapeCharacter();
+        boolean escapable = !escape.isEmpty();
+
         // Optimized escaping with early exit checks
         boolean needsQuoting =
                 field.indexOf(csvOptions.fieldDelimiter().charAt(0)) >= 0
                         || field.indexOf(csvOptions.lineDelimiter().charAt(0)) 
>= 0
-                        || 
field.indexOf(csvOptions.quoteCharacter().charAt(0)) >= 0;
+                        || field.indexOf(quote.charAt(0)) >= 0
+                        || (escapable && field.indexOf(escape.charAt(0)) >= 0);
 
         if (!needsQuoting) {
             return field;
         }
 
-        // Only escape if needed
-        String escaped =
-                field.replace(
-                        csvOptions.quoteCharacter(),
-                        csvOptions.escapeCharacter() + 
csvOptions.quoteCharacter());
-        return csvOptions.quoteCharacter() + escaped + 
csvOptions.quoteCharacter();
+        // Only escape if needed. The escape character goes first: CsvParser 
drops an escape
+        // character that is not followed by a quote or another escape, and 
escaping the quotes
+        // first would double the escape characters inserted for them.
+        String escaped = escapable ? field.replace(escape, escape + escape) : 
field;
+        return quote + escaped.replace(quote, escape + quote) + quote;
     }
 
     /** Optimized string casting with caching and fast paths for common types. 
*/
diff --git 
a/paimon-format/src/test/java/org/apache/paimon/format/csv/CsvFileFormatTest.java
 
b/paimon-format/src/test/java/org/apache/paimon/format/csv/CsvFileFormatTest.java
index 2254931327..5e47ebae95 100644
--- 
a/paimon-format/src/test/java/org/apache/paimon/format/csv/CsvFileFormatTest.java
+++ 
b/paimon-format/src/test/java/org/apache/paimon/format/csv/CsvFileFormatTest.java
@@ -371,9 +371,11 @@ public class CsvFileFormatTest extends FormatReadWriteTest 
{
             // Verify results
             assertThat(result).hasSize(3);
             assertThat(result.get(0).getInt(0)).isEqualTo(1);
+            
assertThat(result.get(0).getString(1).toString()).isEqualTo("Value\"With\"Quotes");
             assertThat(result.get(1).getInt(0)).isEqualTo(2);
             
assertThat(result.get(1).getString(1).toString()).isEqualTo("Normal Value");
             assertThat(result.get(2).getInt(0)).isEqualTo(3);
+            
assertThat(result.get(2).getString(1).toString()).isEqualTo("Special\\Characters");
         }
     }
 
@@ -826,6 +828,31 @@ public class CsvFileFormatTest extends FormatReadWriteTest 
{
      * Performs a complete write-read test with the given options and test 
data. Returns the data
      * that was read back for further verification.
      */
+    @Test
+    public void testFieldsContainingTheEscapeCharacterRoundTrip() throws 
IOException {
+        RowType rowType = DataTypes.ROW(DataTypes.INT().notNull(), 
DataTypes.STRING());
+        // every one of these is written unquoted or half-quoted unless the 
escape character is
+        // itself escaped, and CsvParser then drops it
+        String[] inputs = {
+            "Special\\Characters", "trailing\\", "a,b\\", "\\\\double", 
"\\\"quoteAfterEscape"
+        };
+
+        List<InternalRow> testData = new ArrayList<>();
+        for (int i = 0; i < inputs.length; i++) {
+            testData.add(GenericRow.of(i, BinaryString.fromString(inputs[i])));
+        }
+
+        List<InternalRow> result =
+                writeThenRead(new Options(), rowType, rowType, testData, 
"escape_round_trip");
+
+        assertThat(result).hasSize(inputs.length);
+        for (int i = 0; i < inputs.length; i++) {
+            assertThat(result.get(i).getInt(0)).isEqualTo(i);
+            assertThat(result.get(i).getString(1)).isNotNull();
+            
assertThat(result.get(i).getString(1).toString()).isEqualTo(inputs[i]);
+        }
+    }
+
     private List<InternalRow> writeThenRead(
             Options options,
             RowType fullRowType,

Reply via email to