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 4 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 4 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.
Comment thread ql/src/java/org/apache/hadoop/hive/ql/io/SkippingTextInputFormat.java Outdated
Comment thread ql/src/java/org/apache/hadoop/hive/ql/io/SkippingTextInputFormat.java Outdated
…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.

- Size the footer-loop LineReader to the 5 KB section instead of the 64 KB
  default, and document that it is not seek-aware and must not be closed
  (closing would close the shared FSDataInputStream).
- Drop the redundant 'consumed' accumulator; advance pos by bytesRead directly.
- Hoist the record-delimiter lookup out of the header loop and fold the
  duplicated currPos update into a single branch (behavior-preserving).
- Remove the unused CompressionCodecFactory import.
- Add testSkipHeaderMultiByteWithRecordDelimiter: a multi-byte UTF-8 header
  before a custom record delimiter, asserting the split starts at the byte
  offset (7) rather than the UTF-8 char index (6), guarding the ISO-8859-1
  decode.

@deniskuzZ deniskuzZ 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.

+1, pending tests

@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