Skip to content

[Feature] Add an option to skip empty lines when reading CSV #10773

Open

Description

@COLDMANYU

Search before asking

  • I searched in the issues and found nothing similar.

Motivation

The CSV reader turns every empty or whitespace-only line into a row where all columns are NULL, while Spark's built-in CSV source drops these lines. This makes a CSV Format Table return extra rows that a Spark CSV table over the same files does not return, which gets in the way of moving an existing Spark (Hive metastore) CSV table to a Paimon Format Table.

Current behavior in CsvParser#parse (master d95fd18b48):

// empty line results in all null values
if (isNullOrWhitespaceOnly(line) || projectMapping.length == 0) {
    return emptyRow;
}

Spark 3.5 drops these lines before parsing them, and no option turns that off (UnivocityParser.parseIterator -> CSVExprUtils.filterCommentAndEmpty):

iter.filter(_.trim.nonEmpty)

We hit this while moving tab-separated, gzip-compressed log tables from Spark CSV tables to Paimon CSV Format Tables (Spark 3.5, Paimon 2.0). In one hour of logs, some free-text values contain line breaks, so a few records are split across several physical lines and some of those lines contain only a space. Reading the same 3 files gives:

Reader Rows
Spark CSV (sep='\t', quote='') 2,147,944
Paimon CSV Format Table 2,147,950 (+6 rows, every column NULL)

Apart from these 6 rows, all other rows have the same per-column hashes in both readers.

Why blank lines cannot simply be skipped

Paimon's own CsvFormatWriter writes a blank line in some cases, and today those lines are read back correctly. I checked this on master by writing rows with CsvFormatWriter and reading them back with the CSV reader:

Table Row written Line in file Read back today If blank lines were skipped
1 column, default csv.null-literal ("") NULL `` (empty) NULL row lost
1 column, csv.null-literal='\N' '' `` (empty) NULL row lost
1 column, default null literal ' ' NULL row lost
2 columns (NULL, NULL) , (NULL, NULL) unchanged

So unconditionally skipping blank lines would silently drop rows from single-column tables written by Paimon. A table with 2 or more columns is not affected, because an all-NULL row is written with delimiters and is never blank.

Solution

Add an opt-in CSV option that is off by default:

csv.skip-empty-lines = false   (default; current behavior)

When it is true, CsvParser / CsvFileReader skip lines that are empty or contain only whitespace, the same way Spark does. The default stays as it is today, so existing tables and round-trips are not affected. Users moving tables from Spark can enable the option per table. A few details:

  • Whitespace follows StringUtils#isNullOrWhitespaceOnly, which is what the parser already uses, so the option only changes what happens to lines that already become all-NULL rows today.
  • The header line (csv.include-header) and splitting by offset/length are not affected. Skipping happens per line after the line reader.
  • Docs: add the option to the CSV format options table.

Alternatives I considered:

  1. Skip blank lines only when the schema has more than 1 column, with no new option. This avoids the single-column round-trip problem, but it still changes read results for existing multi-column tables that contain blank lines.
  2. Change the default to match Spark. This breaks the single-column round-trip shown above.

I prefer the opt-in option, but I'd like to hear whether the committers prefer another approach before I start.

Anything else?

Are you willing to submit a PR?

  • I'm willing to submit a PR!

Activity

  1. COLDMANYU commented on Oct 27, 2026

    @COLDMANYU
    ContributorAuthor

    I opened #10824 with the opt-in csv.skip-empty-lines option proposed above. It decides whether a line is empty with String#trim, the same test Spark uses, instead of isNullOrWhitespaceOnly, so that the rows match Spark exactly. The PR description explains the difference. Feedback on either point is welcome.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions