This is an automated email from the ASF dual-hosted git repository.

wgtmac pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/parquet-java.git


The following commit(s) were added to refs/heads/master by this push:
     new 06ed267b9 GH-3596: Add `RowRanges.Builder` for incremental 
construction from selected row indices (#3597)
06ed267b9 is described below

commit 06ed267b9abdcf6aee2e04fca4d02d04494f7b1a
Author: Peter Toth <[email protected]>
AuthorDate: Tue Jul 14 04:31:44 2026 +0200

    GH-3596: Add `RowRanges.Builder` for incremental construction from selected 
row indices (#3597)
    
    ### Rationale for this change
    
    Opening up APIs needed by a later materialization feature in Spark. 
External readers need to assemble a RowRanges incrementally from a stream of 
selected row indices (e.g. produced by a downstream filter or join) without 
having to know page boundaries ahead of time.
    
    ### What changes are included in this PR?
    
    Adds a Builder to RowRanges that takes a strictly-increasing sequence of 
selected row indices via addSelected(long) and coalesces consecutive indices 
into Range entries. Out-of-order or duplicate calls throw 
IllegalArgumentException.
    
    Recreate org.apache.parquet.internal.filter2.columnindex.RowRanges as a
    deprecated subclass of the relocated 
org.apache.parquet.filter2.columnindex.RowRanges
    so the released ParquetFileReader#readFilteredRowGroup(int, RowRanges) 
signature
    keeps linking. Add a deprecated readFilteredRowGroup overload taking the old
    internal type that delegates to the new one, and drop the japicmp 
exclusions for
    that method now that it is compatible. Other internal-only surfaces 
(RowRanges
    statics, Range, ColumnIndexFilter return types) remain relocated without a 
bridge.
    
    Adds a range-level Builder.addSelectedRange(from, to) (addSelectedRow now
    delegates to it), so RowRanges can be built from page ranges through the
    public API. create() is reimplemented on top of the builder and moved to
    the deprecated internal.RowRanges shim, where the released 1.17
    internal.RowRanges.create(...) signature lived. The public RowRanges no
    longer references the internal OffsetIndex, and ColumnIndexFilter now builds
    via the public builder rather than a deprecated method.
    
    Behavior note: the builder rejects overlapping ranges instead of silently
    unioning them (as the old add() did). Offset-index pages never overlap, so
    the filter path is unaffected; TestRowRanges#testCreate is updated to feed
    realistic adjacent (non-overlapping) page ranges.
    
    ### Are these changes tested?
    
    Yes. TestRowRanges covers single/multiple/coalesced ranges, the empty 
builder case, and the out-of-order/duplicate rejection paths.
    
    ### Are there any user-facing changes?
    
    No.
    
    Closes #3596
    
    ---------
    
    Co-authored-by: Matt Butrovich <[email protected]>
    Co-authored-by: Peter Toth <[email protected]>
---
 .../filter2/columnindex/RowRanges.java             | 136 ++++++--
 .../filter2/columnindex/ColumnIndexFilter.java     |   8 +-
 .../internal/filter2/columnindex/RowRanges.java    | 310 ++-----------------
 .../parquet/filter2/columnindex/TestRowRanges.java | 341 +++++++++++++++++++++
 .../filter2/columnindex/TestColumnIndexFilter.java |   1 +
 .../filter2/columnindex/TestRowRanges.java         | 155 ----------
 .../parquet/hadoop/ColumnChunkPageReadStore.java   |   2 +-
 .../parquet/hadoop/ColumnIndexFilterUtils.java     |   2 +-
 .../apache/parquet/hadoop/ParquetFileReader.java   |  18 +-
 .../hadoop/TestParquetFileReaderRowRanges.java     |   2 +-
 pom.xml                                            |   9 +
 11 files changed, 506 insertions(+), 478 deletions(-)

diff --git 
a/parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/RowRanges.java
 
b/parquet-column/src/main/java/org/apache/parquet/filter2/columnindex/RowRanges.java
similarity index 66%
copy from 
parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/RowRanges.java
copy to 
parquet-column/src/main/java/org/apache/parquet/filter2/columnindex/RowRanges.java
index 0b2257a6b..0557b5f9d 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/RowRanges.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/filter2/columnindex/RowRanges.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.parquet.internal.filter2.columnindex;
+package org.apache.parquet.filter2.columnindex;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -26,14 +26,14 @@ import java.util.NoSuchElementException;
 import java.util.PrimitiveIterator;
 import java.util.Set;
 import org.apache.parquet.filter2.compat.FilterCompat.Filter;
-import org.apache.parquet.internal.column.columnindex.OffsetIndex;
 
 /**
  * Class representing row ranges in a row-group. These row ranges are 
calculated as a result of the column index based
  * filtering. To be used iterate over the matching row indexes to be read from 
a row-group, retrieve the count of the
  * matching rows or check overlapping of a row index range.
  *
- * @see ColumnIndexFilter#calculateRowRanges(Filter, ColumnIndexStore, Set, 
long)
+ * @see 
org.apache.parquet.internal.filter2.columnindex.ColumnIndexFilter#calculateRowRanges(Filter,
+ *     org.apache.parquet.internal.filter2.columnindex.ColumnIndexStore, Set, 
long)
  */
 public class RowRanges {
   // Make it public because some uppler layer application need to access it
@@ -95,7 +95,10 @@ public class RowRanges {
 
   private final List<Range> ranges;
 
-  private RowRanges() {
+  // Visible for the deprecated 
org.apache.parquet.internal.filter2.columnindex.RowRanges shim,
+  // which subclasses this type so the released 
ParquetFileReader#readFilteredRowGroup(int, RowRanges)
+  // signature keeps working. Remove once that shim is dropped (2.0).
+  protected RowRanges() {
     this(new ArrayList<>());
   }
 
@@ -118,34 +121,6 @@ public class RowRanges {
     return new RowRanges(new Range(0L, rowCount - 1L));
   }
 
-  /**
-   * Creates a mutable RowRanges object with the following ranges:
-   * <pre>
-   * [firstRowIndex[0], lastRowIndex[0]],
-   * [firstRowIndex[1], lastRowIndex[1]],
-   * ...,
-   * [firstRowIndex[n], lastRowIndex[n]]
-   * </pre>
-   * (See OffsetIndex.getFirstRowIndex and OffsetIndex.getLastRowIndex for 
details.)
-   * <p>
-   * The union of the ranges are calculated so the result ranges always 
contain the disjunct ranges. See union for
-   * details.
-   *
-   * @param rowCount    row count
-   * @param pageIndexes pageIndexes
-   * @param offsetIndex offsetIndex
-   * @return a mutable RowRanges
-   */
-  public static RowRanges create(long rowCount, PrimitiveIterator.OfInt 
pageIndexes, OffsetIndex offsetIndex) {
-    RowRanges ranges = new RowRanges();
-    while (pageIndexes.hasNext()) {
-      int pageIndex = pageIndexes.nextInt();
-      ranges.add(new Range(
-          offsetIndex.getFirstRowIndex(pageIndex), 
offsetIndex.getLastRowIndex(pageIndex, rowCount)));
-    }
-    return ranges;
-  }
-
   /**
    * Calculates the union of the two specified RowRanges object. The union of 
two range is calculated if there are no
    * elements between them. Otherwise, the two disjunct ranges are stored 
separately.
@@ -316,4 +291,101 @@ public class RowRanges {
   public String toString() {
     return ranges.toString();
   }
+
+  /**
+   * @return a new {@link Builder} for constructing a {@link RowRanges} from a 
sequence of
+   *     selected row indices.
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * Constructs a {@link RowRanges} by appending selected row indices in 
strictly increasing
+   * order. Consecutive indices are coalesced into a single {@link Range}; 
gaps close the
+   * current run and start a new one.
+   *
+   * <p>Usage:
+   * <pre>{@code
+   * RowRanges.Builder builder = RowRanges.builder();
+   * for (long row : selectedRowsInOrder) {
+   *   builder.addSelectedRow(row);
+   * }
+   * RowRanges ranges = builder.build();
+   * }</pre>
+   */
+  public static final class Builder {
+    private final List<Range> ranges = new ArrayList<>();
+    private long runStart = -1; // -1 = no active run
+    private long runEnd = -1; // valid iff runStart >= 0
+
+    private Builder() {}
+
+    /**
+     * Marks {@code rowIndex} as selected. The value is a 0-based row index 
within the current row
+     * group. Must be called in strictly increasing order; calling with a 
value less than or equal
+     * to the previous call's value throws {@link IllegalArgumentException}.
+     *
+     * @param rowIndex the 0-based row index to mark selected (must be {@code 
>} the last value
+     *     passed and non-negative)
+     * @return this builder for chaining
+     */
+    public Builder addSelectedRow(long rowIndex) {
+      return addSelectedRange(rowIndex, rowIndex);
+    }
+
+    /**
+     * Marks the inclusive range {@code [from, to]} of 0-based row indices as 
selected. Ranges must be
+     * appended in strictly increasing order: {@code from} must be greater 
than the last selected row index.
+     * A range directly adjacent to the current run (where {@code from} is 
exactly one past the previous
+     * {@code to}) is coalesced into a single {@link Range}; a gap closes the 
current run and starts a new one.
+     *
+     * @param from the first 0-based row index of the range (inclusive; must 
be {@code >} the last selected
+     *     row index and non-negative)
+     * @param to the last 0-based row index of the range (inclusive; must be 
{@code >=} {@code from})
+     * @return this builder for chaining
+     */
+    public Builder addSelectedRange(long from, long to) {
+      if (from < 0) {
+        throw new IllegalArgumentException("addSelectedRange requires a 
non-negative row index; got " + from);
+      }
+      if (from > to) {
+        throw new IllegalArgumentException(
+            "addSelectedRange requires from <= to; got [" + from + ", " + to + 
"]");
+      }
+      if (runStart < 0) {
+        runStart = from;
+        runEnd = to;
+      } else if (from <= runEnd) {
+        throw new IllegalArgumentException("addSelectedRange requires strictly 
increasing ranges; got [" + from
+            + ", " + to + "] after " + runEnd);
+      } else if (from == runEnd + 1) {
+        // from > runEnd is guaranteed here, so runEnd < Long.MAX_VALUE and 
runEnd + 1 cannot overflow.
+        runEnd = to;
+      } else {
+        ranges.add(new Range(runStart, runEnd));
+        runStart = from;
+        runEnd = to;
+      }
+      return this;
+    }
+
+    /**
+     * Returns a snapshot of the rows selected so far. The returned {@link 
RowRanges} is independent
+     * of this builder, so the builder may continue to be used afterwards 
without affecting it.
+     *
+     * @return the constructed {@link RowRanges}, or {@link RowRanges#EMPTY} 
when no rows were
+     *     selected.
+     */
+    public RowRanges build() {
+      List<Range> snapshot = new ArrayList<>(ranges);
+      if (runStart >= 0) {
+        snapshot.add(new Range(runStart, runEnd));
+      }
+      if (snapshot.isEmpty()) {
+        return RowRanges.EMPTY;
+      }
+      return new RowRanges(snapshot);
+    }
+  }
 }
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/ColumnIndexFilter.java
 
b/parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/ColumnIndexFilter.java
index fd26e54d7..aae8b242e 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/ColumnIndexFilter.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/ColumnIndexFilter.java
@@ -21,6 +21,7 @@ package org.apache.parquet.internal.filter2.columnindex;
 import java.util.PrimitiveIterator;
 import java.util.Set;
 import java.util.function.Function;
+import org.apache.parquet.filter2.columnindex.RowRanges;
 import org.apache.parquet.filter2.compat.FilterCompat;
 import org.apache.parquet.filter2.compat.FilterCompat.FilterPredicateCompat;
 import org.apache.parquet.filter2.compat.FilterCompat.NoOpFilter;
@@ -197,7 +198,12 @@ public class ColumnIndexFilter implements 
Visitor<RowRanges> {
     }
 
     PrimitiveIterator.OfInt pageIndexes = func.apply(ci);
-    return RowRanges.create(rowCount, pageIndexes, oi);
+    RowRanges.Builder rangesBuilder = RowRanges.builder();
+    while (pageIndexes.hasNext()) {
+      int pageIndex = pageIndexes.nextInt();
+      rangesBuilder.addSelectedRange(oi.getFirstRowIndex(pageIndex), 
oi.getLastRowIndex(pageIndex, rowCount));
+    }
+    return rangesBuilder.build();
   }
 
   @Override
diff --git 
a/parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/RowRanges.java
 
b/parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/RowRanges.java
index 0b2257a6b..f040ec317 100644
--- 
a/parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/RowRanges.java
+++ 
b/parquet-column/src/main/java/org/apache/parquet/internal/filter2/columnindex/RowRanges.java
@@ -18,302 +18,40 @@
  */
 package org.apache.parquet.internal.filter2.columnindex;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.NoSuchElementException;
 import java.util.PrimitiveIterator;
-import java.util.Set;
-import org.apache.parquet.filter2.compat.FilterCompat.Filter;
+import org.apache.parquet.filter2.columnindex.RowRanges.Builder;
 import org.apache.parquet.internal.column.columnindex.OffsetIndex;
 
 /**
- * Class representing row ranges in a row-group. These row ranges are 
calculated as a result of the column index based
- * filtering. To be used iterate over the matching row indexes to be read from 
a row-group, retrieve the count of the
- * matching rows or check overlapping of a row index range.
- *
- * @see ColumnIndexFilter#calculateRowRanges(Filter, ColumnIndexStore, Set, 
long)
+ * @deprecated moved to {@link 
org.apache.parquet.filter2.columnindex.RowRanges}. This type is
+ *     retained only so that the released
+ *     {@code ParquetFileReader#readFilteredRowGroup(int, RowRanges)} 
signature keeps linking; it
+ *     will be removed in 2.0. Use {@link 
org.apache.parquet.filter2.columnindex.RowRanges} instead.
  */
-public class RowRanges {
-  // Make it public because some uppler layer application need to access it
-  public static class Range {
-
-    // Returns the union of the two ranges or null if there are elements 
between them.
-    private static Range union(Range left, Range right) {
-      if (left.from <= right.from) {
-        if (left.to + 1 >= right.from) {
-          return new Range(left.from, Math.max(left.to, right.to));
-        }
-      } else if (right.to + 1 >= left.from) {
-        return new Range(right.from, Math.max(left.to, right.to));
-      }
-      return null;
-    }
-
-    // Returns the intersection of the two ranges of null if they are not 
overlapped.
-    private static Range intersection(Range left, Range right) {
-      if (left.from <= right.from) {
-        if (left.to >= right.from) {
-          return new Range(right.from, Math.min(left.to, right.to));
-        }
-      } else if (right.to >= left.from) {
-        return new Range(left.from, Math.min(left.to, right.to));
-      }
-      return null;
-    }
-
-    public final long from;
-    public final long to;
-
-    // Creates a range of [from, to] (from and to are inclusive; empty ranges 
are not valid)
-    Range(long from, long to) {
-      assert from <= to;
-      this.from = from;
-      this.to = to;
-    }
-
-    long count() {
-      return to - from + 1;
-    }
-
-    boolean isBefore(Range other) {
-      return to < other.from;
-    }
-
-    boolean isAfter(Range other) {
-      return from > other.to;
-    }
-
-    @Override
-    public String toString() {
-      return "[" + from + ", " + to + ']';
-    }
-  }
-
-  public static final RowRanges EMPTY = new RowRanges(Collections.emptyList());
-
-  private final List<Range> ranges;
-
-  private RowRanges() {
-    this(new ArrayList<>());
-  }
-
-  private RowRanges(Range range) {
-    this(Collections.singletonList(range));
-  }
-
-  private RowRanges(List<Range> ranges) {
-    this.ranges = ranges;
-  }
-
-  /**
-   * Creates an immutable RowRanges object with the single range [0, rowCount -
-   * 1].
-   *
-   * @param rowCount a single row count
-   * @return an immutable RowRanges
-   */
-  public static RowRanges createSingle(long rowCount) {
-    return new RowRanges(new Range(0L, rowCount - 1L));
-  }
+@Deprecated
+public class RowRanges extends 
org.apache.parquet.filter2.columnindex.RowRanges {
 
   /**
-   * Creates a mutable RowRanges object with the following ranges:
-   * <pre>
-   * [firstRowIndex[0], lastRowIndex[0]],
-   * [firstRowIndex[1], lastRowIndex[1]],
-   * ...,
-   * [firstRowIndex[n], lastRowIndex[n]]
-   * </pre>
-   * (See OffsetIndex.getFirstRowIndex and OffsetIndex.getLastRowIndex for 
details.)
-   * <p>
-   * The union of the ranges are calculated so the result ranges always 
contain the disjunct ranges. See union for
-   * details.
+   * Creates a {@link org.apache.parquet.filter2.columnindex.RowRanges} 
covering the given pages. The row bounds of
+   * each page are read from the offset index (see {@code 
OffsetIndex#getFirstRowIndex} and
+   * {@code OffsetIndex#getLastRowIndex}); adjacent page ranges are coalesced.
    *
-   * @param rowCount    row count
-   * @param pageIndexes pageIndexes
-   * @param offsetIndex offsetIndex
-   * @return a mutable RowRanges
-   */
-  public static RowRanges create(long rowCount, PrimitiveIterator.OfInt 
pageIndexes, OffsetIndex offsetIndex) {
-    RowRanges ranges = new RowRanges();
+   * @param rowCount row count of the row-group
+   * @param pageIndexes ascending iterator of the selected page indexes
+   * @param offsetIndex the offset index providing the row bounds of each page
+   * @return the row ranges covering the selected pages
+   * @deprecated use {@link 
org.apache.parquet.filter2.columnindex.RowRanges#builder()} together with
+   *     {@link 
org.apache.parquet.filter2.columnindex.RowRanges.Builder#addSelectedRange(long, 
long)}
+   */
+  @Deprecated
+  public static org.apache.parquet.filter2.columnindex.RowRanges create(
+      long rowCount, PrimitiveIterator.OfInt pageIndexes, OffsetIndex 
offsetIndex) {
+    Builder builder = builder();
     while (pageIndexes.hasNext()) {
       int pageIndex = pageIndexes.nextInt();
-      ranges.add(new Range(
-          offsetIndex.getFirstRowIndex(pageIndex), 
offsetIndex.getLastRowIndex(pageIndex, rowCount)));
+      builder.addSelectedRange(
+          offsetIndex.getFirstRowIndex(pageIndex), 
offsetIndex.getLastRowIndex(pageIndex, rowCount));
     }
-    return ranges;
-  }
-
-  /**
-   * Calculates the union of the two specified RowRanges object. The union of 
two range is calculated if there are no
-   * elements between them. Otherwise, the two disjunct ranges are stored 
separately.
-   * <pre>
-   * For example:
-   * [113, 241] ∪ [221, 340] = [113, 340]
-   * [113, 230] ∪ [231, 340] = [113, 340]
-   * while
-   * [113, 230] ∪ [232, 340] = [113, 230], [232, 340]
-   * </pre>
-   * The result RowRanges object will contain all the row indexes that were 
contained in one of the specified objects.
-   *
-   * @param left  left RowRanges
-   * @param right right RowRanges
-   * @return a mutable RowRanges contains all the row indexes that were 
contained in one of the specified objects
-   */
-  public static RowRanges union(RowRanges left, RowRanges right) {
-    RowRanges result = new RowRanges();
-    Iterator<Range> it1 = left.ranges.iterator();
-    Iterator<Range> it2 = right.ranges.iterator();
-    if (it2.hasNext()) {
-      Range range2 = it2.next();
-      while (it1.hasNext()) {
-        Range range1 = it1.next();
-        if (range1.isAfter(range2)) {
-          result.add(range2);
-          range2 = range1;
-          Iterator<Range> tmp = it1;
-          it1 = it2;
-          it2 = tmp;
-        } else {
-          result.add(range1);
-        }
-      }
-      result.add(range2);
-    } else {
-      it2 = it1;
-    }
-    while (it2.hasNext()) {
-      result.add(it2.next());
-    }
-
-    return result;
-  }
-
-  /**
-   * Calculates the intersection of the two specified RowRanges object. Two 
ranges intersect if they have common
-   * elements otherwise the result is empty.
-   * <pre>
-   * For example:
-   * [113, 241] ∩ [221, 340] = [221, 241]
-   * while
-   * [113, 230] ∩ [231, 340] = &lt;EMPTY&gt;
-   * </pre>
-   *
-   * @param left  left RowRanges
-   * @param right right RowRanges
-   * @return a mutable RowRanges contains all the row indexes that were 
contained in both of the specified objects
-   */
-  public static RowRanges intersection(RowRanges left, RowRanges right) {
-    RowRanges result = new RowRanges();
-
-    int rightIndex = 0;
-    for (Range l : left.ranges) {
-      for (int i = rightIndex, n = right.ranges.size(); i < n; ++i) {
-        Range r = right.ranges.get(i);
-        if (l.isBefore(r)) {
-          break;
-        } else if (l.isAfter(r)) {
-          rightIndex = i + 1;
-          continue;
-        }
-        result.add(Range.intersection(l, r));
-      }
-    }
-
-    return result;
-  }
-
-  /*
-   * Adds a range to the end of the list of ranges. It maintains the disjunct 
ascending order(*) of the ranges by
-   * trying to union the specified range to the last ranges in the list. The 
specified range shall be larger(*) than
-   * the last one or might be overlapped with some of the last ones.
-   * (*) [a, b] < [c, d] if b < c
-   */
-  private void add(Range range) {
-    Range rangeToAdd = range;
-    for (int i = ranges.size() - 1; i >= 0; --i) {
-      Range last = ranges.get(i);
-      assert !last.isAfter(range);
-      Range u = Range.union(last, rangeToAdd);
-      if (u == null) {
-        break;
-      }
-      rangeToAdd = u;
-      ranges.remove(i);
-    }
-    ranges.add(rangeToAdd);
-  }
-
-  /**
-   * @return the number of rows in the ranges
-   */
-  public long rowCount() {
-    long cnt = 0;
-    for (Range range : ranges) {
-      cnt += range.count();
-    }
-    return cnt;
-  }
-
-  /**
-   * @return the ascending iterator of the row indexes contained in the ranges
-   */
-  public PrimitiveIterator.OfLong iterator() {
-    return new PrimitiveIterator.OfLong() {
-      private int currentRangeIndex = -1;
-      private Range currentRange;
-      private long next = findNext();
-
-      private long findNext() {
-        if (currentRange == null || next + 1 > currentRange.to) {
-          if (currentRangeIndex + 1 < ranges.size()) {
-            currentRange = ranges.get(++currentRangeIndex);
-            next = currentRange.from;
-          } else {
-            return -1;
-          }
-        } else {
-          ++next;
-        }
-        return next;
-      }
-
-      @Override
-      public boolean hasNext() {
-        return next >= 0;
-      }
-
-      @Override
-      public long nextLong() {
-        long ret = next;
-        if (ret < 0) {
-          throw new NoSuchElementException();
-        }
-        next = findNext();
-        return ret;
-      }
-    };
-  }
-
-  /**
-   * @param from the first row of the range to be checked for connection
-   * @param to   the last row of the range to be checked for connection
-   * @return {@code true} if the specified range is overlapping (have common 
elements) with one of the ranges
-   */
-  public boolean isOverlapping(long from, long to) {
-    return Collections.binarySearch(
-            ranges, new Range(from, to), (r1, r2) -> r1.isBefore(r2) ? -1 : 
r1.isAfter(r2) ? 1 : 0)
-        >= 0;
-  }
-
-  public List<Range> getRanges() {
-    return ranges;
-  }
-
-  @Override
-  public String toString() {
-    return ranges.toString();
+    return builder.build();
   }
 }
diff --git 
a/parquet-column/src/test/java/org/apache/parquet/filter2/columnindex/TestRowRanges.java
 
b/parquet-column/src/test/java/org/apache/parquet/filter2/columnindex/TestRowRanges.java
new file mode 100644
index 000000000..67f65b870
--- /dev/null
+++ 
b/parquet-column/src/test/java/org/apache/parquet/filter2/columnindex/TestRowRanges.java
@@ -0,0 +1,341 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.parquet.filter2.columnindex;
+
+import static org.apache.parquet.filter2.columnindex.RowRanges.intersection;
+import static org.apache.parquet.filter2.columnindex.RowRanges.union;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import it.unimi.dsi.fastutil.longs.LongArrayList;
+import it.unimi.dsi.fastutil.longs.LongList;
+import java.util.Arrays;
+import java.util.PrimitiveIterator;
+import org.apache.parquet.internal.column.columnindex.OffsetIndexBuilder;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link RowRanges}
+ */
+public class TestRowRanges {
+  @SuppressWarnings("deprecation")
+  private static RowRanges buildRanges(long... rowIndexes) {
+    if (rowIndexes.length == 0) {
+      return RowRanges.EMPTY;
+    }
+    OffsetIndexBuilder builder = OffsetIndexBuilder.getBuilder();
+    for (int i = 0, n = rowIndexes.length; i < n; i += 2) {
+      long from = rowIndexes[i];
+      long to = rowIndexes[i + 1];
+      builder.add(0, 0, from);
+      builder.add(0, 0, to + 1);
+    }
+    PrimitiveIterator.OfInt pageIndexes = new PrimitiveIterator.OfInt() {
+      private int index = 0;
+
+      @Override
+      public boolean hasNext() {
+        return index < rowIndexes.length;
+      }
+
+      @Override
+      public int nextInt() {
+        int ret = index;
+        index += 2;
+        return ret;
+      }
+    };
+    return org.apache.parquet.internal.filter2.columnindex.RowRanges.create(
+        rowIndexes[rowIndexes.length - 1], pageIndexes, builder.build());
+  }
+
+  private static void assertAllRowsEqual(PrimitiveIterator.OfLong actualIt, 
long... expectedValues) {
+    LongList actualList = new LongArrayList();
+    actualIt.forEachRemaining((long value) -> actualList.add(value));
+    assertArrayEquals(
+        Arrays.toString(expectedValues) + "!= " + actualList, expectedValues, 
actualList.toLongArray());
+  }
+
+  @Test
+  public void testCreate() {
+    // Offset-index pages never overlap; consecutive pages are at most 
adjacent ([6, 7] then [8, 10]),
+    // which the builder coalesces into [6, 10].
+    RowRanges ranges = buildRanges(
+        1, 2,
+        3, 4,
+        6, 7,
+        8, 10,
+        15, 17);
+    assertAllRowsEqual(ranges.iterator(), 1, 2, 3, 4, 6, 7, 8, 9, 10, 15, 16, 
17);
+    assertEquals(12, ranges.rowCount());
+    assertTrue(ranges.isOverlapping(4, 5));
+    assertFalse(ranges.isOverlapping(5, 5));
+    assertTrue(ranges.isOverlapping(10, 14));
+    assertFalse(ranges.isOverlapping(11, 14));
+    assertFalse(ranges.isOverlapping(18, Long.MAX_VALUE));
+
+    ranges = RowRanges.createSingle(5);
+    assertAllRowsEqual(ranges.iterator(), 0, 1, 2, 3, 4);
+    assertEquals(5, ranges.rowCount());
+    assertTrue(ranges.isOverlapping(0, 100));
+    assertFalse(ranges.isOverlapping(5, Long.MAX_VALUE));
+
+    ranges = RowRanges.EMPTY;
+    assertAllRowsEqual(ranges.iterator());
+    assertEquals(0, ranges.rowCount());
+    assertFalse(ranges.isOverlapping(0, Long.MAX_VALUE));
+  }
+
+  @Test
+  public void testUnion() {
+    RowRanges ranges1 = buildRanges(
+        2, 5,
+        7, 9,
+        14, 14,
+        20, 24);
+    RowRanges ranges2 = buildRanges(
+        1, 2,
+        4, 5,
+        11, 12,
+        14, 15,
+        21, 22);
+    RowRanges empty = buildRanges();
+    assertAllRowsEqual(
+        union(ranges1, ranges2).iterator(), 1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 
14, 15, 20, 21, 22, 23, 24);
+    assertAllRowsEqual(
+        union(ranges2, ranges1).iterator(), 1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 
14, 15, 20, 21, 22, 23, 24);
+    assertAllRowsEqual(union(ranges1, ranges1).iterator(), 2, 3, 4, 5, 7, 8, 
9, 14, 20, 21, 22, 23, 24);
+    assertAllRowsEqual(union(ranges1, empty).iterator(), 2, 3, 4, 5, 7, 8, 9, 
14, 20, 21, 22, 23, 24);
+    assertAllRowsEqual(union(empty, ranges1).iterator(), 2, 3, 4, 5, 7, 8, 9, 
14, 20, 21, 22, 23, 24);
+    assertAllRowsEqual(union(ranges2, ranges2).iterator(), 1, 2, 4, 5, 11, 12, 
14, 15, 21, 22);
+    assertAllRowsEqual(union(ranges2, empty).iterator(), 1, 2, 4, 5, 11, 12, 
14, 15, 21, 22);
+    assertAllRowsEqual(union(empty, ranges2).iterator(), 1, 2, 4, 5, 11, 12, 
14, 15, 21, 22);
+    assertAllRowsEqual(union(empty, empty).iterator());
+  }
+
+  @Test
+  public void testIntersection() {
+    RowRanges ranges1 = buildRanges(
+        2, 5,
+        7, 9,
+        14, 14,
+        20, 24);
+    RowRanges ranges2 = buildRanges(
+        1, 2,
+        6, 7,
+        9, 9,
+        11, 12,
+        14, 15,
+        21, 22);
+    RowRanges empty = buildRanges();
+    assertAllRowsEqual(intersection(ranges1, ranges2).iterator(), 2, 7, 9, 14, 
21, 22);
+    assertAllRowsEqual(intersection(ranges2, ranges1).iterator(), 2, 7, 9, 14, 
21, 22);
+    assertAllRowsEqual(intersection(ranges1, ranges1).iterator(), 2, 3, 4, 5, 
7, 8, 9, 14, 20, 21, 22, 23, 24);
+    assertAllRowsEqual(intersection(ranges1, empty).iterator());
+    assertAllRowsEqual(intersection(empty, ranges1).iterator());
+    assertAllRowsEqual(intersection(ranges2, ranges2).iterator(), 1, 2, 6, 7, 
9, 11, 12, 14, 15, 21, 22);
+    assertAllRowsEqual(intersection(ranges2, empty).iterator());
+    assertAllRowsEqual(intersection(empty, ranges2).iterator());
+    assertAllRowsEqual(intersection(empty, empty).iterator());
+  }
+
+  @Test
+  public void testBuilderBasic() {
+    // Select rows 2, 3, 4, 5 (one contiguous run)
+    RowRanges ranges = RowRanges.builder()
+        .addSelectedRow(2)
+        .addSelectedRow(3)
+        .addSelectedRow(4)
+        .addSelectedRow(5)
+        .build();
+    assertAllRowsEqual(ranges.iterator(), 2, 3, 4, 5);
+    assertEquals(4, ranges.rowCount());
+  }
+
+  @Test
+  public void testBuilderMultipleRanges() {
+    // Two runs: 1-2 and 5-7
+    RowRanges ranges = RowRanges.builder()
+        .addSelectedRow(1)
+        .addSelectedRow(2)
+        .addSelectedRow(5)
+        .addSelectedRow(6)
+        .addSelectedRow(7)
+        .build();
+    assertAllRowsEqual(ranges.iterator(), 1, 2, 5, 6, 7);
+    assertEquals(5, ranges.rowCount());
+    assertTrue(ranges.isOverlapping(1, 2));
+    assertTrue(ranges.isOverlapping(5, 7));
+    assertFalse(ranges.isOverlapping(3, 4));
+  }
+
+  @Test
+  public void testBuilderEmpty() {
+    // No rows selected
+    RowRanges ranges = RowRanges.builder().build();
+    assertEquals(RowRanges.EMPTY, ranges);
+    assertEquals(0, ranges.rowCount());
+    assertAllRowsEqual(ranges.iterator());
+  }
+
+  @Test
+  public void testBuilderAllSelected() {
+    // Five contiguous rows starting at 0
+    RowRanges.Builder builder = RowRanges.builder();
+    for (long i = 0; i < 5; i++) {
+      builder.addSelectedRow(i);
+    }
+    RowRanges ranges = builder.build();
+    assertAllRowsEqual(ranges.iterator(), 0, 1, 2, 3, 4);
+    assertEquals(5, ranges.rowCount());
+  }
+
+  @Test
+  public void testBuilderSingleRow() {
+    RowRanges ranges = RowRanges.builder().addSelectedRow(3).build();
+    assertAllRowsEqual(ranges.iterator(), 3);
+    assertEquals(1, ranges.rowCount());
+    assertTrue(ranges.isOverlapping(3, 3));
+    assertFalse(ranges.isOverlapping(0, 2));
+    assertFalse(ranges.isOverlapping(4, 10));
+  }
+
+  @Test
+  public void testBuilderAlternating() {
+    // Every other row selected: 0, 2, 4, 6, 8 — five singleton runs.
+    RowRanges.Builder builder = RowRanges.builder();
+    for (long i = 0; i < 10; i += 2) {
+      builder.addSelectedRow(i);
+    }
+    RowRanges ranges = builder.build();
+    assertAllRowsEqual(ranges.iterator(), 0, 2, 4, 6, 8);
+    assertEquals(5, ranges.rowCount());
+  }
+
+  @Test
+  public void testBuilderFirstAndLast() {
+    RowRanges ranges =
+        RowRanges.builder().addSelectedRow(0).addSelectedRow(99).build();
+    assertAllRowsEqual(ranges.iterator(), 0, 99);
+    assertEquals(2, ranges.rowCount());
+  }
+
+  @Test
+  public void testBuilderRejectsOutOfOrder() {
+    RowRanges.Builder builder = 
RowRanges.builder().addSelectedRow(5).addSelectedRow(7);
+    try {
+      builder.addSelectedRow(6);
+      org.junit.Assert.fail("expected IllegalArgumentException for 
out-of-order index");
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testBuilderRejectsDuplicate() {
+    RowRanges.Builder builder = RowRanges.builder().addSelectedRow(3);
+    try {
+      builder.addSelectedRow(3);
+      org.junit.Assert.fail("expected IllegalArgumentException for duplicate 
index");
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testBuilderRejectsNegativeRow() {
+    RowRanges.Builder builder = RowRanges.builder();
+    try {
+      builder.addSelectedRow(-1);
+      org.junit.Assert.fail("expected IllegalArgumentException for negative 
index");
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testBuilderRejectsFollowUpAfterMaxValue() {
+    // After Long.MAX_VALUE, runEnd + 1 would overflow; the 
strictly-increasing guard must still
+    // reject any follow-up index rather than silently starting a new run.
+    RowRanges.Builder builder = 
RowRanges.builder().addSelectedRow(Long.MAX_VALUE);
+    try {
+      builder.addSelectedRow(5);
+      org.junit.Assert.fail("expected IllegalArgumentException for index after 
Long.MAX_VALUE");
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+    // Long.MAX_VALUE alone is a valid single-row selection.
+    assertAllRowsEqual(builder.build().iterator(), Long.MAX_VALUE);
+  }
+
+  @Test
+  public void testBuilderAddSelectedRangeCoalescesAndSeparates() {
+    RowRanges ranges = RowRanges.builder()
+        .addSelectedRange(0, 2)
+        .addSelectedRange(3, 5) // adjacent to the previous run -> coalesced 
into [0, 5]
+        .addSelectedRange(8, 9) // gap -> separate run
+        .build();
+    assertAllRowsEqual(ranges.iterator(), 0, 1, 2, 3, 4, 5, 8, 9);
+    assertEquals(8, ranges.rowCount());
+  }
+
+  @Test
+  public void testBuilderAddSelectedRangeRejectsOverlap() {
+    RowRanges.Builder builder = RowRanges.builder().addSelectedRange(0, 10);
+    try {
+      builder.addSelectedRange(5, 15);
+      org.junit.Assert.fail("expected IllegalArgumentException for overlapping 
range");
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testBuilderAddSelectedRangeRejectsFromGreaterThanTo() {
+    RowRanges.Builder builder = RowRanges.builder();
+    try {
+      builder.addSelectedRange(5, 3);
+      org.junit.Assert.fail("expected IllegalArgumentException for from > to");
+    } catch (IllegalArgumentException expected) {
+      // expected
+    }
+  }
+
+  @Test
+  public void testBuilderBuildReturnsSnapshot() {
+    // build() must return a snapshot: continuing to use the builder 
afterwards must not
+    // mutate a previously built result.
+    RowRanges.Builder builder = 
RowRanges.builder().addSelectedRow(0).addSelectedRow(1);
+    RowRanges first = builder.build();
+    assertAllRowsEqual(first.iterator(), 0, 1);
+    assertEquals(2, first.rowCount());
+
+    builder.addSelectedRow(5);
+    RowRanges second = builder.build();
+
+    // The first result is unchanged.
+    assertAllRowsEqual(first.iterator(), 0, 1);
+    assertEquals(2, first.rowCount());
+    // The second result reflects the additional row.
+    assertAllRowsEqual(second.iterator(), 0, 1, 5);
+    assertEquals(3, second.rowCount());
+  }
+}
diff --git 
a/parquet-column/src/test/java/org/apache/parquet/internal/filter2/columnindex/TestColumnIndexFilter.java
 
b/parquet-column/src/test/java/org/apache/parquet/internal/filter2/columnindex/TestColumnIndexFilter.java
index cf91ef4d7..91327b335 100644
--- 
a/parquet-column/src/test/java/org/apache/parquet/internal/filter2/columnindex/TestColumnIndexFilter.java
+++ 
b/parquet-column/src/test/java/org/apache/parquet/internal/filter2/columnindex/TestColumnIndexFilter.java
@@ -61,6 +61,7 @@ import java.util.List;
 import java.util.Set;
 import java.util.stream.LongStream;
 import org.apache.parquet.bytes.BytesUtils;
+import org.apache.parquet.filter2.columnindex.RowRanges;
 import org.apache.parquet.filter2.compat.FilterCompat;
 import org.apache.parquet.filter2.predicate.Statistics;
 import org.apache.parquet.filter2.predicate.UserDefinedPredicate;
diff --git 
a/parquet-column/src/test/java/org/apache/parquet/internal/filter2/columnindex/TestRowRanges.java
 
b/parquet-column/src/test/java/org/apache/parquet/internal/filter2/columnindex/TestRowRanges.java
deleted file mode 100644
index 9c6b9f737..000000000
--- 
a/parquet-column/src/test/java/org/apache/parquet/internal/filter2/columnindex/TestRowRanges.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.parquet.internal.filter2.columnindex;
-
-import static 
org.apache.parquet.internal.filter2.columnindex.RowRanges.intersection;
-import static org.apache.parquet.internal.filter2.columnindex.RowRanges.union;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-import it.unimi.dsi.fastutil.longs.LongArrayList;
-import it.unimi.dsi.fastutil.longs.LongList;
-import java.util.Arrays;
-import java.util.PrimitiveIterator;
-import org.apache.parquet.internal.column.columnindex.OffsetIndexBuilder;
-import org.junit.Test;
-
-/**
- * Unit test for {@link RowRanges}
- */
-public class TestRowRanges {
-  private static RowRanges buildRanges(long... rowIndexes) {
-    if (rowIndexes.length == 0) {
-      return RowRanges.EMPTY;
-    }
-    OffsetIndexBuilder builder = OffsetIndexBuilder.getBuilder();
-    for (int i = 0, n = rowIndexes.length; i < n; i += 2) {
-      long from = rowIndexes[i];
-      long to = rowIndexes[i + 1];
-      builder.add(0, 0, from);
-      builder.add(0, 0, to + 1);
-    }
-    PrimitiveIterator.OfInt pageIndexes = new PrimitiveIterator.OfInt() {
-      private int index = 0;
-
-      @Override
-      public boolean hasNext() {
-        return index < rowIndexes.length;
-      }
-
-      @Override
-      public int nextInt() {
-        int ret = index;
-        index += 2;
-        return ret;
-      }
-    };
-    return RowRanges.create(rowIndexes[rowIndexes.length - 1], pageIndexes, 
builder.build());
-  }
-
-  private static void assertAllRowsEqual(PrimitiveIterator.OfLong actualIt, 
long... expectedValues) {
-    LongList actualList = new LongArrayList();
-    actualIt.forEachRemaining((long value) -> actualList.add(value));
-    assertArrayEquals(
-        Arrays.toString(expectedValues) + "!= " + actualList, expectedValues, 
actualList.toLongArray());
-  }
-
-  @Test
-  public void testCreate() {
-    RowRanges ranges = buildRanges(
-        1, 2,
-        3, 4,
-        6, 7,
-        7, 10,
-        15, 17);
-    assertAllRowsEqual(ranges.iterator(), 1, 2, 3, 4, 6, 7, 8, 9, 10, 15, 16, 
17);
-    assertEquals(12, ranges.rowCount());
-    assertTrue(ranges.isOverlapping(4, 5));
-    assertFalse(ranges.isOverlapping(5, 5));
-    assertTrue(ranges.isOverlapping(10, 14));
-    assertFalse(ranges.isOverlapping(11, 14));
-    assertFalse(ranges.isOverlapping(18, Long.MAX_VALUE));
-
-    ranges = RowRanges.createSingle(5);
-    assertAllRowsEqual(ranges.iterator(), 0, 1, 2, 3, 4);
-    assertEquals(5, ranges.rowCount());
-    assertTrue(ranges.isOverlapping(0, 100));
-    assertFalse(ranges.isOverlapping(5, Long.MAX_VALUE));
-
-    ranges = RowRanges.EMPTY;
-    assertAllRowsEqual(ranges.iterator());
-    assertEquals(0, ranges.rowCount());
-    assertFalse(ranges.isOverlapping(0, Long.MAX_VALUE));
-  }
-
-  @Test
-  public void testUnion() {
-    RowRanges ranges1 = buildRanges(
-        2, 5,
-        7, 9,
-        14, 14,
-        20, 24);
-    RowRanges ranges2 = buildRanges(
-        1, 2,
-        4, 5,
-        11, 12,
-        14, 15,
-        21, 22);
-    RowRanges empty = buildRanges();
-    assertAllRowsEqual(
-        union(ranges1, ranges2).iterator(), 1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 
14, 15, 20, 21, 22, 23, 24);
-    assertAllRowsEqual(
-        union(ranges2, ranges1).iterator(), 1, 2, 3, 4, 5, 7, 8, 9, 11, 12, 
14, 15, 20, 21, 22, 23, 24);
-    assertAllRowsEqual(union(ranges1, ranges1).iterator(), 2, 3, 4, 5, 7, 8, 
9, 14, 20, 21, 22, 23, 24);
-    assertAllRowsEqual(union(ranges1, empty).iterator(), 2, 3, 4, 5, 7, 8, 9, 
14, 20, 21, 22, 23, 24);
-    assertAllRowsEqual(union(empty, ranges1).iterator(), 2, 3, 4, 5, 7, 8, 9, 
14, 20, 21, 22, 23, 24);
-    assertAllRowsEqual(union(ranges2, ranges2).iterator(), 1, 2, 4, 5, 11, 12, 
14, 15, 21, 22);
-    assertAllRowsEqual(union(ranges2, empty).iterator(), 1, 2, 4, 5, 11, 12, 
14, 15, 21, 22);
-    assertAllRowsEqual(union(empty, ranges2).iterator(), 1, 2, 4, 5, 11, 12, 
14, 15, 21, 22);
-    assertAllRowsEqual(union(empty, empty).iterator());
-  }
-
-  @Test
-  public void testIntersection() {
-    RowRanges ranges1 = buildRanges(
-        2, 5,
-        7, 9,
-        14, 14,
-        20, 24);
-    RowRanges ranges2 = buildRanges(
-        1, 2,
-        6, 7,
-        9, 9,
-        11, 12,
-        14, 15,
-        21, 22);
-    RowRanges empty = buildRanges();
-    assertAllRowsEqual(intersection(ranges1, ranges2).iterator(), 2, 7, 9, 14, 
21, 22);
-    assertAllRowsEqual(intersection(ranges2, ranges1).iterator(), 2, 7, 9, 14, 
21, 22);
-    assertAllRowsEqual(intersection(ranges1, ranges1).iterator(), 2, 3, 4, 5, 
7, 8, 9, 14, 20, 21, 22, 23, 24);
-    assertAllRowsEqual(intersection(ranges1, empty).iterator());
-    assertAllRowsEqual(intersection(empty, ranges1).iterator());
-    assertAllRowsEqual(intersection(ranges2, ranges2).iterator(), 1, 2, 6, 7, 
9, 11, 12, 14, 15, 21, 22);
-    assertAllRowsEqual(intersection(ranges2, empty).iterator());
-    assertAllRowsEqual(intersection(empty, ranges2).iterator());
-    assertAllRowsEqual(intersection(empty, empty).iterator());
-  }
-}
diff --git 
a/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ColumnChunkPageReadStore.java
 
b/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ColumnChunkPageReadStore.java
index 5ddd6e443..dfdd6c5d8 100644
--- 
a/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ColumnChunkPageReadStore.java
+++ 
b/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ColumnChunkPageReadStore.java
@@ -43,9 +43,9 @@ import org.apache.parquet.column.page.PageReader;
 import 
org.apache.parquet.compression.CompressionCodecFactory.BytesInputDecompressor;
 import org.apache.parquet.crypto.AesCipher;
 import org.apache.parquet.crypto.ModuleCipherFactory.ModuleType;
+import org.apache.parquet.filter2.columnindex.RowRanges;
 import org.apache.parquet.format.BlockCipher;
 import org.apache.parquet.internal.column.columnindex.OffsetIndex;
-import org.apache.parquet.internal.filter2.columnindex.RowRanges;
 import org.apache.parquet.io.ParquetDecodingException;
 import org.apache.parquet.util.AutoCloseables;
 import org.slf4j.Logger;
diff --git 
a/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ColumnIndexFilterUtils.java
 
b/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ColumnIndexFilterUtils.java
index e78381574..9c9e2a6cf 100644
--- 
a/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ColumnIndexFilterUtils.java
+++ 
b/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ColumnIndexFilterUtils.java
@@ -28,9 +28,9 @@ import java.util.Arrays;
 import java.util.Formatter;
 import java.util.List;
 import java.util.Optional;
+import org.apache.parquet.filter2.columnindex.RowRanges;
 import org.apache.parquet.hadoop.metadata.ColumnChunkMetaData;
 import org.apache.parquet.internal.column.columnindex.OffsetIndex;
-import org.apache.parquet.internal.filter2.columnindex.RowRanges;
 
 /**
  * Internal utility class to help at column index based filtering.
diff --git 
a/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetFileReader.java 
b/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetFileReader.java
index 8355a1554..9af4b4ac6 100644
--- 
a/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetFileReader.java
+++ 
b/parquet-hadoop/src/main/java/org/apache/parquet/hadoop/ParquetFileReader.java
@@ -85,6 +85,7 @@ import 
org.apache.parquet.crypto.InternalColumnDecryptionSetup;
 import org.apache.parquet.crypto.InternalFileDecryptor;
 import org.apache.parquet.crypto.ModuleCipherFactory.ModuleType;
 import org.apache.parquet.crypto.ParquetCryptoRuntimeException;
+import org.apache.parquet.filter2.columnindex.RowRanges;
 import org.apache.parquet.filter2.compat.FilterCompat;
 import org.apache.parquet.filter2.compat.RowGroupFilter;
 import org.apache.parquet.format.BlockCipher;
@@ -111,7 +112,6 @@ import 
org.apache.parquet.internal.column.columnindex.ColumnIndex;
 import org.apache.parquet.internal.column.columnindex.OffsetIndex;
 import org.apache.parquet.internal.filter2.columnindex.ColumnIndexFilter;
 import org.apache.parquet.internal.filter2.columnindex.ColumnIndexStore;
-import org.apache.parquet.internal.filter2.columnindex.RowRanges;
 import org.apache.parquet.internal.hadoop.metadata.IndexReference;
 import org.apache.parquet.io.InputFile;
 import org.apache.parquet.io.ParquetDecodingException;
@@ -1268,6 +1268,22 @@ public class ParquetFileReader implements Closeable {
     return internalReadFilteredRowGroup(block, rowRanges, 
getColumnIndexStore(blockIndex));
   }
 
+  /**
+   * @param blockIndex the index of the requested block
+   * @param rowRanges  the row ranges to be read from the requested block
+   * @return the PageReadStore which can provide PageReaders for each column 
or null if there are no rows in this block
+   * @throws IOException              if an error occurs while reading
+   * @throws IllegalArgumentException if the {@code blockIndex} is invalid or 
the {@code rowRanges} is null
+   * @deprecated use {@link #readFilteredRowGroup(int, RowRanges)} with
+   *     {@link org.apache.parquet.filter2.columnindex.RowRanges} instead. 
This overload is retained
+   *     for backward compatibility and will be removed in 2.0.
+   */
+  @Deprecated
+  public ColumnChunkPageReadStore readFilteredRowGroup(
+      int blockIndex, 
org.apache.parquet.internal.filter2.columnindex.RowRanges rowRanges) throws 
IOException {
+    return readFilteredRowGroup(blockIndex, (RowRanges) rowRanges);
+  }
+
   /**
    * Read data in all parts via either vectored IO or serial IO.
    * @param allParts all parts to be read.
diff --git 
a/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestParquetFileReaderRowRanges.java
 
b/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestParquetFileReaderRowRanges.java
index e445caf2b..72fdd3718 100644
--- 
a/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestParquetFileReaderRowRanges.java
+++ 
b/parquet-hadoop/src/test/java/org/apache/parquet/hadoop/TestParquetFileReaderRowRanges.java
@@ -31,10 +31,10 @@ import org.apache.parquet.HadoopReadOptions;
 import org.apache.parquet.ParquetReadOptions;
 import org.apache.parquet.example.data.Group;
 import org.apache.parquet.example.data.simple.SimpleGroupFactory;
+import org.apache.parquet.filter2.columnindex.RowRanges;
 import org.apache.parquet.hadoop.example.ExampleParquetWriter;
 import org.apache.parquet.hadoop.metadata.BlockMetaData;
 import org.apache.parquet.hadoop.util.HadoopInputFile;
-import org.apache.parquet.internal.filter2.columnindex.RowRanges;
 import org.apache.parquet.schema.MessageType;
 import org.apache.parquet.schema.MessageTypeParser;
 import org.junit.Before;
diff --git a/pom.xml b/pom.xml
index 831686c59..62bdb3262 100644
--- a/pom.xml
+++ b/pom.xml
@@ -621,6 +621,15 @@
               
<exclude>org.apache.parquet.avro.AvroReadSupport#AVRO_REQUESTED_PROJECTION</exclude>
               
<exclude>org.apache.parquet.avro.AvroReadSupport#AVRO_DATA_SUPPLIER</exclude>
               
<exclude>org.apache.parquet.hadoop.ParquetFileReader#PARQUET_READ_PARALLELISM</exclude>
+              <!-- GH-3596: RowRanges moved to 
org.apache.parquet.filter2.columnindex. A deprecated
+                subclass is kept at the old internal location so the released
+                ParquetFileReader#readFilteredRowGroup(int, RowRanges) 
signature stays compatible.
+                The internal package carries no backward-compatibility 
guarantee, so the removal of
+                the old RowRanges' own members (statics, Range) and the 
ColumnIndexFilter
+                return-type changes are intentional. -->
+              
<exclude>org.apache.parquet.internal.filter2.columnindex.RowRanges</exclude>
+              
<exclude>org.apache.parquet.internal.filter2.columnindex.RowRanges$Range</exclude>
+              
<exclude>org.apache.parquet.internal.filter2.columnindex.ColumnIndexFilter</exclude>
             </excludes>
           </parameter>
         </configuration>

Reply via email to