Skip to content

Quote null value that starts a record in minimal quote mode - #629

Open
rootvector2 wants to merge 1 commit into
apache:masterfrom
rootvector2:quote-null-starting-record
Open

Quote null value that starts a record in minimal quote mode#629
rootvector2 wants to merge 1 commit into
apache:masterfrom
rootvector2:quote-null-starting-record

Conversation

@rootvector2

Copy link
Copy Markdown
Contributor

print(Object, CharSequence, Appendable, boolean) sends a null value straight to out.append(value) and never reaches printWithQuotes, so the encapsulation MINIMAL does for an empty value at the start of a record is skipped. That rule exists precisely for this case, as its own comment says an empty line has no tokens. The result is that a record whose first value is null, and whose null string renders empty, prints as a blank line. CSVFormat.DEFAULT enables ignoreEmptyLines, so the record is silently dropped when the output is read back: printing [a], [null], [b] gives a\r\n\r\nb\r\n, which parses as 2 records. The same loss hits any DEFAULT-derived format, including MONGODB_CSV and INFORMIX_UNLOAD, and any format using setNullString(""). Found round-tripping printer output with null values back through the parser.

The fix encapsulates the empty null value when it starts a record, a quote character is set, and the quote mode is MINIMAL or unset, which is the same condition printWithQuotes already applies to an empty CharSequence. ALL_NON_NULL and NON_NUMERIC are left alone since they use the bare empty field to encode null, and formats without a quote character have nothing to encapsulate with. Two existing assertions move because a leading null now prints like a leading ""; testPrintRecordsWithObjectArray was pinning six blank lines for six records, which is the bug itself.

  • Read the contribution guidelines for this project.
  • Read the ASF Generative Tooling Guidance if you use Artificial Intelligence (AI).
  • I used AI to create any part of, or all of, this pull request. Which AI tool was used to create this pull request, and to what extent did it contribute?
  • Run a successful build using the default Maven goal with mvn; that's mvn on the command line by itself.
  • Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied. This may not always be possible, but it is a best practice.
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Each commit in the pull request should have a meaningful subject line and body. Note that a maintainer may squash commits during the merge process.

a null value bypasses `printWithQuotes`, so the empty-value encapsulation minimal mode does at the start of a record never runs and a single-column null record prints as a blank line that `ignoreEmptyLines` drops on read.
@garydgregory
garydgregory requested a review from Copilot July 28, 2026 11:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Fixes CSV printing of a leading null value (rendering as empty) so it’s properly quoted in minimal-quoting scenarios, preventing the record from being emitted as an “empty line” that CSVFormat.DEFAULT parsers can drop via ignoreEmptyLines.

Changes:

  • Add quoting behavior for leading null/empty values when quote char is set and quote mode is MINIMAL or unset.
  • Add/adjust unit tests to cover the round-trip regression and updated output expectations for leading nulls.
  • Update an existing format-related assertion to reflect the new quoting of a leading empty field.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
src/main/java/org/apache/commons/csv/CSVFormat.java Adds special-case quoting for leading empty null values under minimal quote policy.
src/test/java/org/apache/commons/csv/CSVPrinterTest.java Adds regression test for dropped-record scenario and updates expectations for object-array printing.
src/test/java/org/apache/commons/csv/CSVFormatTest.java Updates expected output to account for leading empty-field encapsulation.
Comments suppressed due to low confidence (1)

src/main/java/org/apache/commons/csv/CSVFormat.java:2276

  • This fixes the dropped-record case for MINIMAL/unset, but the same dropped-record behavior can still occur for QuoteMode.ALL (which conceptually should quote empty fields too). With quoteMode == ALL, a leading null that renders empty will still be output as a blank line and can be dropped when parsing with ignoreEmptyLines. Consider extending the condition to also encapsulate for QuoteMode.ALL (while still excluding ALL_NON_NULL/NON_NUMERIC as intended), or otherwise aligning null-handling with the quote-mode semantics so ‘quote all’ doesn’t emit an empty record as an empty line.
    private void print(final Object object, final CharSequence value, final Appendable out, final boolean newRecord) throws IOException {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2284 to +2294
if (len == 0 && newRecord && isQuoteCharacterSet() && isMinimalQuoteMode()) {
// Encapsulate like printWithQuotes does for an empty value that starts a record: an
// unquoted one makes the whole line empty, and a parser with ignoreEmptyLines enabled
// then drops the record. ALL_NON_NULL and NON_NUMERIC are excluded because they encode
// null as the bare empty field.
final char quoteChar = quoteCharacter.charValue(); // Explicit unboxing is intentional
out.append(quoteChar);
out.append(quoteChar);
} else {
out.append(value);
}
Comment on lines +1732 to +1733
@Test
void testPrintNullValueStartingRecord() throws IOException {
Comment on lines +1747 to +1752
// An explicit MINIMAL quote mode encapsulates it too.
assertEquals("\"\"" + RECORD_SEPARATOR, printNullRecord(CSVFormat.DEFAULT.builder().setQuoteMode(QuoteMode.MINIMAL).get()));
// ALL_NON_NULL encodes null as the bare empty field, so it must stay unquoted.
assertEquals(RECORD_SEPARATOR, printNullRecord(CSVFormat.DEFAULT.builder().setQuoteMode(QuoteMode.ALL_NON_NULL).get()));
// Without a quote character there is nothing to encapsulate with.
assertEquals(RECORD_SEPARATOR, printNullRecord(CSVFormat.DEFAULT.builder().setQuote(null).get()));
Comment on lines +1888 to +1889
assertEquals(16, charArrayWriter.size());
assertEquals("\"\"\n\"\"\n\"\"\n\n\"\"\n\"\"\n", charArrayWriter.toString());

@garydgregory garydgregory left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rootvector2
Thank you for the PR.
Please review the copilot comments.

@garydgregory garydgregory changed the title quote null value that starts a record in minimal quote mode Quote null value that starts a record in minimal quote mode Jul 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants