jeanlyn created ORC-2226:
----------------------------

             Summary: UncompressedStream.seek throws IllegalArgumentException 
when SARG selects only an all-null trailing row group
                 Key: ORC-2226
                 URL: https://issues.apache.org/jira/browse/ORC-2226
             Project: ORC
          Issue Type: Bug
          Components: Java, Reader
    Affects Versions: 2.1.2
            Reporter: jeanlyn


h2. Description

Reading an *uncompressed* ORC file with a SearchArgument that selects *only the 
last row group of a stripe* fails with:

{noformat}
java.io.IOException: Problem opening stripe 0 footer in orc-seek-eof-repro.orc.
Caused by: java.lang.IllegalArgumentException: Seek in column 2 kind DATA to 
8192 is outside of the data
    at org.apache.orc.impl.InStream$UncompressedStream.seek(InStream.java:...)
    at org.apache.orc.impl.TreeReaderFactory$StringDirectTreeReader.seek(...)
    at org.apache.orc.impl.RecordReaderImpl.seekToRowEntry(...)
{noformat}

This happens when a (string) column is entirely NULL in that trailing row 
group: its row-group index records the DATA position exactly at the *end of the 
DATA stream*. The same file reads fine with a full scan (no SARG) and with the 
C++ reader. Reproduced with orc-core *2.1.2*.

h2. Root cause

Two parts:

# {{StripePlanner.processStream}} (2.x; 
{{RecordReaderImpl.planReadPartialDataStreams}} in 1.x): when the selected 
row-group run ends at the stripe's last row group, the planned read range is 
{{[stream.offset + pos, stream.offset + stream.length)}}. For an all-null 
trailing row group, {{pos == stream.length}}, so the planned range is *0 
bytes*, {{stream.firstChunk}} stays null, and the stream is created with 
{{bytes == null}}.
# {{InStream.UncompressedStream.seek(long)}}: the empty-stream shortcut only 
handles {{desired == 0 && bytes == null}}. A seek to {{desired == length}} — a 
legitimate EOF position that both the Java and C++ writers record for all-null 
trailing row groups — falls through the range loop and throws 
{{IllegalArgumentException}}.

Related historical issues: HIVE-9471, HIVE-12360 (fixed the Hive-era bundled 
reader; this standalone-ORC path is still present in 2.1.2).

h2. Minimal reproduction

Self-contained program (writes a tiny synthetic file, then reads it with a 
SARG). Only needs orc-core + its deps on the classpath; tested with the 
{{orc-tools}} 2.1.2 uber jar:

{code:java}
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.orc.CompressionKind;
import org.apache.orc.OrcFile;
import org.apache.orc.Reader;
import org.apache.orc.RecordReader;
import org.apache.orc.TypeDescription;
import org.apache.orc.Writer;
import org.apache.orc.storage.ql.exec.vector.BytesColumnVector;
import org.apache.orc.storage.ql.exec.vector.VectorizedRowBatch;
import org.apache.orc.storage.ql.io.sarg.PredicateLeaf;
import org.apache.orc.storage.ql.io.sarg.SearchArgument;
import org.apache.orc.storage.ql.io.sarg.SearchArgumentFactory;

import java.nio.charset.StandardCharsets;

/**
 * Minimal repro: reading an *uncompressed* ORC file with a SARG that selects
 * ONLY the last row group of a stripe, where a sparse string column is
 * entirely NULL in that row group, fails with:
 *   IllegalArgumentException: Seek in column 2 kind DATA to 8192 is outside of 
the data
 *
 * Layout crafted by this program (single stripe, row index stride 1024):
 *   row group 0: 1024 rows, k='a', v=distinct 8-byte strings (DATA stream of v 
= 8192 bytes)
 *   row group 1:  512 rows, k='c', v=ALL NULL   (row-index DATA position = 
8192 = stream end)
 * SARG k='c' selects only row group 1 -> the read plan assigns 0 bytes to the
 * DATA stream of v -> UncompressedStream.seek(8192) falls off the end and 
throws.
 */
public class OrcSeekEofRepro {
  public static void main(String[] args) throws Exception {
    Path path = new Path(args.length > 0 ? args[0] : "orc-seek-eof-repro.orc");
    Configuration conf = new Configuration();
    TypeDescription schema = 
TypeDescription.fromString("struct<k:string,v:string>");

    try (Writer w = OrcFile.createWriter(path, OrcFile.writerOptions(conf)
        .setSchema(schema)
        .compress(CompressionKind.NONE)   // must be uncompressed
        .rowIndexStride(1024))) {
      VectorizedRowBatch batch = schema.createRowBatchV2();
      BytesColumnVector k = (BytesColumnVector) batch.cols[0];
      BytesColumnVector v = (BytesColumnVector) batch.cols[1];
      batch.reset();
      batch.size = 1024;
      for (int i = 0; i < batch.size; i++) {
        k.setVal(i, "a".getBytes(StandardCharsets.UTF_8));
        // distinct 8-byte values so the writer picks DIRECT encoding (no 
dictionary)
        v.setVal(i, String.format("%07dx", i).getBytes(StandardCharsets.UTF_8));
      }
      w.addRowBatch(batch);
      batch.reset();
      batch.size = 512;
      v.noNulls = false;
      v.isRepeating = true;
      v.isNull[0] = true;
      for (int i = 0; i < batch.size; i++) {
        k.setVal(i, "c".getBytes(StandardCharsets.UTF_8));
      }
      w.addRowBatch(batch);
    }
    System.out.println("written: " + path);

    Reader reader = OrcFile.createReader(path, OrcFile.readerOptions(conf));
    SearchArgument sarg = SearchArgumentFactory.newBuilder()
        .equals("k", PredicateLeaf.Type.STRING, args.length > 1 ? args[1] : 
"c").build();
    try (RecordReader rows = reader.rows(reader.options()
        .searchArgument(sarg, new String[]{"k"}))) {
      VectorizedRowBatch batch = reader.getSchema().createRowBatchV2();
      long total = 0;
      while (rows.nextBatch(batch)) {
        total += batch.size;
      }
      System.out.println("READ OK, candidate rows = " + total + " (bug NOT 
present)");
    } catch (Exception e) {
      System.out.println("REPRODUCED: " + e);
      if (e.getCause() != null) {
        System.out.println("caused by: " + e.getCause());
      }
      System.exit(1);
    }
  }
}
{code}

Run:

{noformat}
java -cp orc-tools-2.1.2-uber.jar OrcSeekEofRepro.java
{noformat}

Output on ORC 2.1.2:

{noformat}
REPRODUCED: java.io.IOException: Problem opening stripe 0 footer in 
orc-seek-eof-repro.orc.
caused by: java.lang.IllegalArgumentException: Seek in column 2 kind DATA to 
8192 is outside of the data
{noformat}

Control case ({{k = 'a'}}, selects only row group 0) reads fine. File metadata 
confirms the layout ({{8192}} == the DATA stream length; the row-group index of 
the all-null trailing row group points exactly at the end of the stream):

{noformat}
Compression: NONE
    Stream: column 2 section DATA start: 153 length 8192
    Encoding column 2: DIRECT_V2
      Entry 0: count: 1024 hasNull: false ... positions: 0,0,0,0,0,0
      Entry 1: count: 0 hasNull: true positions: 0,128,0,8192,8,0
{noformat}

h2. Proposed fix

In {{InStream.UncompressedStream.seek(long)}} (and analogously 
{{CompressedStream.seek(long)}}), accept {{desired == length}} as a valid EOF 
position instead of throwing:

{code:java}
public void seek(long desired) throws IOException {
  if (desired == 0 && bytes == null) {
    return;
  }
  // NEW: seeking to end-of-stream is legitimate — the row-group index of an
  // all-null trailing row group points here, and the read planner may have
  // assigned zero bytes to this stream (bytes == null). Park at EOF;
  // read() already returns -1 when position == length.
  if (desired == length) {
    position = desired;
    currentRange = null;
    decrypted = null;
    return;
  }
  ...
}
{code}

{{read()}} already returns {{-1}} at {{position == length}} and {{available()}} 
returns {{0}}, so parking at EOF is safe. With this change the repro prints 
{{READ OK, candidate rows = 512}}.




--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to