PDGGK opened a new pull request, #9278:
URL: https://github.com/apache/paimon/pull/9278

   ### Purpose
   
   The CSV writer and the CSV reader disagree about the escape character, so a 
value containing one does not survive a round trip through Paimon's own CSV 
format. With default options (`csv.escape-character = \`) — written, then read 
back:
   
   | written | read back |
   |---|---|
   | `Special\Characters` | `SpecialCharacters` |
   | `trailing\` | `trailing` |
   | `a,b\` | **null** |
   | `\\double` | `double` |
   
   Nothing is logged and nothing throws.
   
   `CsvFormatWriter.escapeField` decides whether a field needs quoting, and 
escapes the quote character inside it — but the escape character is in neither 
step:
   
   ```java
   // CsvFormatWriter:103-116
   boolean needsQuoting =
           field.indexOf(csvOptions.fieldDelimiter().charAt(0)) >= 0
                   || field.indexOf(csvOptions.lineDelimiter().charAt(0)) >= 0
                   || field.indexOf(csvOptions.quoteCharacter().charAt(0)) >= 0;
   
   if (!needsQuoting) {
       return field;
   }
   
   String escaped =
           field.replace(
                   csvOptions.quoteCharacter(),
                   csvOptions.escapeCharacter() + csvOptions.quoteCharacter());
   return csvOptions.quoteCharacter() + escaped + csvOptions.quoteCharacter();
   ```
   
   `CsvParser` consumes an escape character unconditionally — it is only ever 
appended to the buffer when the character after it is a quote or another escape:
   
   ```java
   // CsvParser:108-118
   if (c == escapeChar) {
       if (inQuotes || inField) {
           int nextCharacter = peekNextCharacter(line, position);
           if (nextCharacter == quoteChar || nextCharacter == escapeChar) {
               buffer.append(line.charAt(position + 1));
               position++;
           }
       }
   } else if (c == quoteChar) {
   ```
   
   So each row above fails a little differently:
   
   * `Special\Characters` and `trailing\` are written verbatim, because a lone 
escape character does not trigger quoting. The reader then drops it.
   * `\\double` is written verbatim too, and both escape characters are lost — 
at the start of a field `inQuotes || inField` is still false, so the reader 
does not even take the "escaped escape" branch.
   * `a,b\` does get quoted, for the comma. It is written `"a,b\"`, the 
trailing `\` is not doubled, and the reader takes the closing quote to be an 
escaped literal. The field never terminates and the row comes back with a null.
   
   ### Why the existing test is green
   
   `testCsvEscapeCharacterWriteRead` writes exactly this value and then does 
not look at it:
   
   ```java
   GenericRow.of(3, BinaryString.fromString("Special\\Characters"))
   ...
   assertThat(result.get(2).getInt(0)).isEqualTo(3);
   ```
   
   Three rows are written, and only the middle one — `"Normal Value"`, which 
contains nothing that needs escaping — has its string asserted. Rows 1 and 3, 
the two carrying quotes and a backslash, are checked on their `int` column 
alone. This PR adds the two missing assertions; with the writer unchanged, the 
existing test then fails.
   
   ### What changes
   
   Writer only. Quote a field that contains the escape character, and escape 
the escape character before the quotes:
   
   ```java
   String quote = csvOptions.quoteCharacter();
   String escape = csvOptions.escapeCharacter();
   boolean escapable = !escape.isEmpty();
   
   boolean needsQuoting =
           field.indexOf(csvOptions.fieldDelimiter().charAt(0)) >= 0
                   || field.indexOf(csvOptions.lineDelimiter().charAt(0)) >= 0
                   || field.indexOf(quote.charAt(0)) >= 0
                   || (escapable && field.indexOf(escape.charAt(0)) >= 0);
   
   if (!needsQuoting) {
       return field;
   }
   
   String escaped = escapable ? field.replace(escape, escape + escape) : field;
   return quote + escaped.replace(quote, escape + quote) + quote;
   ```
   
   The order matters: escaping the quotes first would then double the escape 
characters that step had just inserted, and the reader would decode `\\"` as a 
literal backslash followed by an unescaped quote.
   
   `escapable` keeps an empty `csv.escape-character` behaving as it does today 
— `field.replace("", ...)` inserts between every character, so the substitution 
has to be skipped rather than run with an empty needle.
   
   **The reader is left alone.** Once a field containing the escape character 
is quoted, `inQuotes` is true by the time the reader reaches it, so it takes 
the branch that already works; the five cases in the new test all round trip 
without touching `CsvParser`. Reading a lone escape character in a CSV file 
that Paimon did not write still drops it, which is the same behaviour as before 
this change.
   
   ### Blast radius
   
   Only fields that contain the escape character are written differently; every 
other field is byte-identical. A `csv.escape-character` set to a character that 
appears often in ordinary data — the `/` that `testCsvEscapeCharacterWriteRead` 
also exercises, say — will now see those fields quoted and the character 
doubled. That is the point: today they come back with the character missing.
   
   Worth stating plainly: CSV files **already written** still contain the 
unescaped form and still read back short. This change stops new files from 
being written that way; it cannot repair existing ones.
   
   ### Test evidence
   
   `testFieldsContainingTheEscapeCharacterRoundTrip` writes five values — 
`Special\Characters`, `trailing\`, `a,b\`, `\\double`, `\"quoteAfterEscape` — 
and asserts each comes back equal to what went in, and non-null.
   
   Mutation control, on a forced clean rebuild of `paimon-format` (`rm -rf 
target/classes target/test-classes`) so this is not an incremental-build 
artefact: with the tests kept and `CsvFormatWriter` reverted, **two fail** — 
the new one, and `testCsvEscapeCharacterWriteRead` with its restored assertions:
   
   ```
   CsvFileFormatTest.testCsvEscapeCharacterWriteRead:378
   expected: "Special\Characters"
    but was: "SpecialCharacters"
   ```
   
   `CsvFileFormatTest` — 28 tests, and the whole of `paimon-format` — 551 
tests, 0 failures.
   
   ### API and Format
   
   No change to any option or public signature. The on-disk bytes change only 
for fields containing the escape character, which are the fields that currently 
do not survive being read back.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to