Skip to content

HIVE-29785: SkippingTextInputFormat: ClassCastException on skip.header.line.count files with lone-CR (\r) line endings - #6680

Open
srbiswal wants to merge 2 commits into
apache:masterfrom
srbiswal:HIVE-29785
Open

HIVE-29785: SkippingTextInputFormat: ClassCastException on skip.header.line.count files with lone-CR (\r) line endings#6680
srbiswal wants to merge 2 commits into
apache:masterfrom
srbiswal:HIVE-29785

Conversation

@srbiswal

@srbiswal srbiswal commented Aug 6, 2026

Copy link
Copy Markdown

For tables with skip.header.line.count/skip.footer.line.count, SkippingTextInputFormat called readLine() then getPos() on the same FSDataInputStream (in getCachedStartIndex and getCachedEndIndex). When the data contains a lone \r (not followed by \n), DataInputStream.readLine() pushes back its look-ahead byte by wrapping the stream in a non-Seekable PushbackInputStream. The following getPos() then throws ClassCastException during Tez split generation. LF, CRLF, and \r-at-EOF were unaffected.

Replace readLine()+getPos() in both methods with a ByteCountingLineReader that handles \n, \r\n, and lone \r, computing offsets from bytes consumed, so getPos() is never called on a mutated stream. Offset behavior is unchanged. As a safety net, makeSplitInternal now turns any unexpected error from header/footer detection into a clear, file-specific message instead of a raw ClassCastException

Does this PR introduce any user-facing change?

No

How was this patch tested?

Added unit tests in TestSkippingTextInputFormat:

  • testSkipFileSplitsLoneCR — reproduces the ClassCastException on a lone-CR file with skip.header.line.count=1 (fails on master, passes with this fix).
  • testSkipHeaderSplitOffsetsAcrossLineEndings — writes the same content with LF, lone-CR, and CRLF terminators and asserts exact split getStart()/getLength(), proving lone-CR matches LF and there is no boundary regression.
  • testSkipFileSplitsLoneCRHeaderFooter — exercises the footer path (getCachedEndIndex) with lone-CR and skip header/footer, verifying the header and footer rows are skipped.

…r.line.count files with lone-CR (\r) line endings

SkippingTextInputFormat.getCachedStartIndex (header path) and
getCachedEndIndex (footer path) called FSDataInputStream.readLine() and
then getPos() on the same stream. On a lone '\r' not followed by '\n',
DataInputStream.readLine() pushes its look-ahead byte back by replacing
the stream's inner input with a non-Seekable PushbackInputStream, so the
subsequent getPos() throws ClassCastException during Tez split generation.
LF, CRLF and '\r'-at-EOF were unaffected.

Replace readLine()+getPos() in both methods with a ByteCountingLineReader
that recognizes '\n', '\r\n' and lone '\r' and derives offsets from bytes
consumed, so getPos() is never called on a readLine()-mutated stream.
Offset semantics are preserved exactly. As a safety net, makeSplitInternal
now converts any unexpected RuntimeException from header/footer detection
into a clear, file-contextual error instead of a cryptic cast.

Adds tests: a lone-CR repro, exact split-offset assertions across LF/CR/CRLF
proving no boundary regression, and a footer-path lone-CR case.
return new NullRowsInputFormat.DummyInputSplit(file);
} catch (RuntimeException e) {
// Report unexpected detection failures clearly instead of a cryptic cast.
throw new RuntimeException("Failed to detect header/footer boundaries for file "

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.

i don't see much benefit in re-throwing same runtime exception

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

makes sense. removed the catch (RuntimeException)

* {@code '\r\n'} and lone {@code '\r'}; after {@link #readLine()},
* {@link #getBytesConsumed()} is the offset just past the line's terminator.
*/
static final class ByteCountingLineReader {

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.

doesn't hadoop already comes with similar util:

Text headerLine = new Text();
LineReader reader = new LineReader(fis);
int bytesRead = reader.readLine(headerLine);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

replaced ByteCountingLineReader with org.apache.hadoop.util.LineReader

…p redundant RuntimeException wrap

Address review feedback on the lone-CR ClassCastException fix:

- Replace the hand-rolled ByteCountingLineReader with Hadoop's
  org.apache.hadoop.util.LineReader, which counts bytes internally via
  readLine(Text) (no getPos()) and handles '\n', '\r\n' and lone '\r'.
  It is the standard utility, already used in TextRecordReader. Constructed
  as new LineReader(fis) so the existing textinputformat.record.delimiter
  index logic is preserved.
- Decode the last header line as ISO-8859-1 (one char per byte) before the
  delimiter indexOf so the index remains a byte offset, matching the prior
  implementation exactly for non-ASCII header bytes.
- Drop the catch (RuntimeException) re-throw in makeSplitInternal: it wrapped
  a RuntimeException in the same type and only added the file path, while the
  reader change already removes the ClassCastException it guarded against.

Offset semantics unchanged; existing TestSkippingTextInputFormat and
TestLineBuffer pass.

@srbiswal srbiswal left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Implemented the suggestions.

@sonarqubecloud

Copy link
Copy Markdown

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants