Skip to content

Commit e6495c7

Browse files
committed
keep an escaped value that equals the null string
1 parent 8379c94 commit e6495c7

5 files changed

Lines changed: 41 additions & 5 deletions

File tree

src/main/java/org/apache/commons/csv/CSVParser.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -806,8 +806,10 @@ private String handleNull(final String input) {
806806
final String nullString = format.getNullString();
807807
final boolean strictQuoteMode = isStrictQuoteMode();
808808
if (input.equals(nullString)) {
809+
// An escaped value is the null string itself, not the null marker: printing null writes the null string
810+
// verbatim, so "\\N" for nullString "\N" can only have come from a value that really is "\N".
809811
// nullString = NULL(String), distinguish between "NULL" and NULL in ALL_NON_NULL or NON_NUMERIC quote mode
810-
return strictQuoteMode && isQuoted ? input : null;
812+
return reusableToken.isEscaped || strictQuoteMode && isQuoted ? input : null;
811813
}
812814
// don't set nullString, distinguish between "" and ,, (absent values) in All_NON_NULL or NON_NUMERIC quote mode
813815
return strictQuoteMode && nullString == null && input.isEmpty() && !isQuoted ? null : input;

src/main/java/org/apache/commons/csv/Lexer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ final class Lexer implements Closeable {
7676
private void appendNextEscapedCharacterToToken(final Token token) throws IOException {
7777
if (isEscapeDelimiter()) {
7878
token.content.append(delimiter);
79+
token.isEscaped = true;
7980
} else {
8081
final int unescaped = readEscape();
8182
if (unescaped == EOF) { // unexpected char after escape
83+
// The escape character is kept verbatim, so nothing was translated.
8284
token.content.append((char) escape).append((char) reader.getLastChar());
8385
} else {
8486
token.content.append((char) unescaped);
87+
token.isEscaped = true;
8588
}
8689
}
8790
}

src/main/java/org/apache/commons/csv/Token.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,15 @@ enum Type {
6161

6262
boolean isQuoted;
6363

64+
/** True when an escape sequence in the input was translated while building {@link #content}. */
65+
boolean isEscaped;
66+
6467
void reset() {
6568
content.setLength(0);
6669
type = INVALID;
6770
isReady = false;
6871
isQuoted = false;
72+
isEscaped = false;
6973
}
7074

7175
/**

src/test/java/org/apache/commons/csv/CSVParserTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,22 @@ void testEndOfFileBehaviorExcel() throws Exception {
584584
}
585585
}
586586

587+
@Test
588+
void testEscapedNullStringIsAValue() throws Exception {
589+
// "\N" is the MySQL and PostgreSQL null marker, "\\N" is the value "\N", which is what the printer writes for it.
590+
for (final CSVFormat format : new CSVFormat[] { CSVFormat.MYSQL, CSVFormat.POSTGRESQL_TEXT, CSVFormat.ORACLE }) {
591+
final StringWriter writer = new StringWriter();
592+
try (CSVPrinter printer = new CSVPrinter(writer, format)) {
593+
printer.printRecord("\\N", null);
594+
}
595+
try (CSVParser parser = CSVParser.parse(writer.toString(), format)) {
596+
final CSVRecord record = parser.nextRecord();
597+
assertEquals("\\N", record.get(0), format.toString());
598+
assertNull(record.get(1), format.toString());
599+
}
600+
}
601+
}
602+
587603
@Test
588604
void testExcelFormat1() throws IOException {
589605
final String code = "value1,value2,value3,value4\r\na,b,c,d\r\n x,,,\r\n\r\n\"\"\"hello\"\"\",\" \"\"world\"\"\",\"abc\ndef\",\r\n";

src/test/java/org/apache/commons/csv/CSVPrinterTest.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,17 @@ private void doRandom(final CSVFormat format, final int iter) throws Exception {
157157
}
158158

159159
/**
160-
* Converts an input CSV array into expected output values, including NULLs. NULL strings are converted to null values because the parser will convert
161-
* these strings to null.
160+
* Converts an input CSV array into expected output values, including NULLs. A value equal to the null string is converted to null only when the printer
161+
* writes that value unchanged; when the printer escapes or quotes it, the parser reads it back as the value it is.
162162
*/
163-
private <T> T[] expectNulls(final T[] original, final CSVFormat csvFormat) {
163+
private <T> T[] expectNulls(final T[] original, final CSVFormat csvFormat) throws IOException {
164164
final T[] fixed = original.clone();
165+
final String nullString = csvFormat.getNullString();
166+
if (nullString == null || !printsVerbatim(csvFormat, nullString)) {
167+
return fixed;
168+
}
165169
for (int i = 0; i < fixed.length; i++) {
166-
if (Objects.equals(csvFormat.getNullString(), fixed[i])) {
170+
if (Objects.equals(nullString, fixed[i])) {
167171
fixed[i] = null;
168172
}
169173
}
@@ -187,6 +191,13 @@ private Connection getH2Connection() throws SQLException, ClassNotFoundException
187191
return DriverManager.getConnection("jdbc:h2:mem:my_test;", "sa", "");
188192
}
189193

194+
/** Tests whether the format prints the given value unchanged, in which case the parser cannot tell it from the null string. */
195+
private boolean printsVerbatim(final CSVFormat csvFormat, final String value) throws IOException {
196+
final StringBuilder sb = new StringBuilder();
197+
csvFormat.print(value, sb, true);
198+
return value.contentEquals(sb);
199+
}
200+
190201
private CSVPrinter printWithHeaderComments(final StringWriter sw, final Date now, final CSVFormat baseFormat) throws IOException {
191202
// Use withHeaderComments first to test CSV-145
192203
// @formatter:off

0 commit comments

Comments
 (0)