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

domgarguilo pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/accumulo.git


The following commit(s) were added to refs/heads/main by this push:
     new b2c01a2532 Add RowRange object (#3342)
b2c01a2532 is described below

commit b2c01a2532d4c0d123e3d4128f8a5a1b97a30de2
Author: Dom G. <[email protected]>
AuthorDate: Wed Oct 1 14:00:13 2025 -0400

    Add RowRange object (#3342)
    
    * Add RowRange object which represents a range of rows with inclusive or 
exclusive bounds
    
    ---------
    
    Co-authored-by: Keith Turner <[email protected]>
    Co-authored-by: Christopher Tubbs <[email protected]>
---
 .../apache/accumulo/core/client/admin/FindMax.java |  50 +-
 .../core/client/admin/TableOperations.java         |  26 +-
 .../core/client/rfile/RFileSummariesRetriever.java |   4 +-
 .../core/clientImpl/TableOperationsImpl.java       |  11 +-
 .../org/apache/accumulo/core/data/RowRange.java    | 656 +++++++++++++++
 .../org/apache/accumulo/core/summary/Gatherer.java |  68 +-
 .../accumulo/core/summary/SummaryReader.java       |   2 +-
 .../accumulo/core/summary/SummarySerializer.java   |  52 +-
 .../core/clientImpl/TableOperationsHelperTest.java |   4 +-
 .../apache/accumulo/core/data/RowRangeTest.java    | 910 +++++++++++++++++++++
 .../server/compaction/CompactionPluginUtils.java   |   5 +-
 .../accumulo/shell/commands/MaxRowCommand.java     |  11 +-
 .../apache/accumulo/test/ComprehensiveITBase.java  |   3 +-
 ...ComprehensiveTableOperationsIT_SimpleSuite.java |   3 +-
 .../java/org/apache/accumulo/test/FindMaxIT.java   |  76 +-
 .../accumulo/test/NamespacesIT_SimpleSuite.java    |   3 +-
 16 files changed, 1764 insertions(+), 120 deletions(-)

diff --git 
a/core/src/main/java/org/apache/accumulo/core/client/admin/FindMax.java 
b/core/src/main/java/org/apache/accumulo/core/client/admin/FindMax.java
index a953d7d46f..71570e4acf 100644
--- a/core/src/main/java/org/apache/accumulo/core/client/admin/FindMax.java
+++ b/core/src/main/java/org/apache/accumulo/core/client/admin/FindMax.java
@@ -25,10 +25,10 @@ import java.util.Map.Entry;
 
 import org.apache.accumulo.core.client.IteratorSetting;
 import org.apache.accumulo.core.client.Scanner;
-import org.apache.accumulo.core.client.TableNotFoundException;
 import org.apache.accumulo.core.data.Key;
 import org.apache.accumulo.core.data.PartialKey;
 import org.apache.accumulo.core.data.Range;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.data.Value;
 import org.apache.accumulo.core.iterators.SortedKeyIterator;
 import org.apache.hadoop.io.Text;
@@ -93,18 +93,17 @@ public class FindMax {
     return ret;
   }
 
-  private static Text _findMax(Scanner scanner, Text start, boolean inclStart, 
Text end,
-      boolean inclEnd) {
+  private static Text _findMax(Scanner scanner, RowRange rowRange) {
+    final Text lowerBound = rowRange.getLowerBound();
+    final Text upperBound = rowRange.getUpperBound();
+    final boolean lowerBoundInclusive = rowRange.isLowerBoundInclusive();
+    final boolean upperBoundInclusive = rowRange.isUpperBoundInclusive();
 
-    // System.out.printf("findMax(%s, %s, %s, %s)%n", 
Key.toPrintableString(start.getBytes(), 0,
-    // start.getLength(), 1000), inclStart,
-    // Key.toPrintableString(end.getBytes(), 0, end.getLength(), 1000), 
inclEnd);
-
-    int cmp = start.compareTo(end);
+    int cmp = lowerBound.compareTo(upperBound);
 
     if (cmp >= 0) {
-      if (inclStart && inclEnd && cmp == 0) {
-        scanner.setRange(new Range(start, true, end, true));
+      if (lowerBoundInclusive && upperBoundInclusive && cmp == 0) {
+        scanner.setRange(new Range(lowerBound, true, upperBound, true));
         Iterator<Entry<Key,Value>> iter = scanner.iterator();
         if (iter.hasNext()) {
           return iter.next().getKey().getRow();
@@ -114,11 +113,12 @@ public class FindMax {
       return null;
     }
 
-    Text mid = findMidPoint(start, end);
+    Text mid = findMidPoint(lowerBound, upperBound);
     // System.out.println("mid = :"+Key.toPrintableString(mid.getBytes(), 0, 
mid.getLength(),
     // 1000)+":");
 
-    scanner.setRange(new Range(mid, mid.equals(start) ? inclStart : true, end, 
inclEnd));
+    scanner.setRange(new Range(mid, mid.equals(lowerBound) ? 
lowerBoundInclusive : true, upperBound,
+        upperBoundInclusive));
 
     Iterator<Entry<Key,Value>> iter = scanner.iterator();
 
@@ -135,7 +135,8 @@ public class FindMax {
         return next.getRow();
       }
 
-      Text ret = _findMax(scanner, next.followingKey(PartialKey.ROW).getRow(), 
true, end, inclEnd);
+      Text ret = _findMax(scanner, 
RowRange.range(next.followingKey(PartialKey.ROW).getRow(), true,
+          upperBound, upperBoundInclusive));
       if (ret == null) {
         return next.getRow();
       } else {
@@ -143,7 +144,8 @@ public class FindMax {
       }
     } else {
 
-      return _findMax(scanner, start, inclStart, mid, mid.equals(start) ? 
inclStart : false);
+      return _findMax(scanner, RowRange.range(lowerBound, lowerBoundInclusive, 
mid,
+          mid.equals(lowerBound) ? lowerBoundInclusive : false));
     }
   }
 
@@ -163,22 +165,26 @@ public class FindMax {
     return end;
   }
 
-  public static Text findMax(Scanner scanner, Text start, boolean is, Text 
end, boolean ie)
-      throws TableNotFoundException {
+  public static Text findMax(Scanner scanner, RowRange rowRange) {
 
     scanner.setBatchSize(12);
     IteratorSetting cfg = new IteratorSetting(Integer.MAX_VALUE, 
SortedKeyIterator.class);
     scanner.addScanIterator(cfg);
 
-    if (start == null) {
-      start = new Text();
-      is = true;
+    Text lowerBound = rowRange.getLowerBound();
+    boolean lowerBoundInclusive = rowRange.isLowerBoundInclusive();
+    if (lowerBound == null) {
+      lowerBound = new Text();
+      lowerBoundInclusive = true;
     }
 
-    if (end == null) {
-      end = findInitialEnd(scanner);
+    Text upperBound = rowRange.getUpperBound();
+    final boolean upperBoundInclusive = rowRange.isUpperBoundInclusive();
+    if (upperBound == null) {
+      upperBound = findInitialEnd(scanner);
     }
 
-    return _findMax(scanner, start, is, end, ie);
+    return _findMax(scanner,
+        RowRange.range(lowerBound, lowerBoundInclusive, upperBound, 
upperBoundInclusive));
   }
 }
diff --git 
a/core/src/main/java/org/apache/accumulo/core/client/admin/TableOperations.java 
b/core/src/main/java/org/apache/accumulo/core/client/admin/TableOperations.java
index d08e6978d8..147383df11 100644
--- 
a/core/src/main/java/org/apache/accumulo/core/client/admin/TableOperations.java
+++ 
b/core/src/main/java/org/apache/accumulo/core/client/admin/TableOperations.java
@@ -44,6 +44,7 @@ import org.apache.accumulo.core.client.summary.Summarizer;
 import org.apache.accumulo.core.client.summary.SummarizerConfiguration;
 import org.apache.accumulo.core.data.LoadPlan;
 import org.apache.accumulo.core.data.Range;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.iterators.IteratorUtil.IteratorScope;
 import org.apache.accumulo.core.security.Authorizations;
 import org.apache.accumulo.core.security.TablePermission;
@@ -249,6 +250,7 @@ public interface TableOperations {
    * Finds the max row within a given range. To find the max row in a table, 
pass null for start and
    * end row.
    *
+   * @param tableName the table to search
    * @param auths find the max row that can seen with these auths
    * @param startRow row to start looking at, null means -Infinity
    * @param startInclusive determines if the start row is included
@@ -256,9 +258,29 @@ public interface TableOperations {
    * @param endInclusive determines if the end row is included
    *
    * @return The max row in the range, or null if there is no visible data in 
the range.
+   *
+   * @deprecated since 4.0.0, use {@link #getMaxRow(String, Authorizations, 
RowRange)} instead
+   */
+  @Deprecated(since = "4.0.0")
+  default Text getMaxRow(String tableName, Authorizations auths, Text startRow,
+      boolean startInclusive, Text endRow, boolean endInclusive)
+      throws TableNotFoundException, AccumuloException, 
AccumuloSecurityException {
+    return getMaxRow(tableName, auths,
+        RowRange.range(startRow, startInclusive, endRow, endInclusive));
+  }
+
+  /**
+   * Finds the max row within a given row range. To find the max row in the 
whole table, pass
+   * {@link RowRange#all()} as the row range.
+   *
+   * @param tableName the table to search
+   * @param auths find the max row that can seen with these auths
+   * @param range the range of rows to search
+   *
+   * @return The max row in the range, or null if there is no visible data in 
the range.
+   * @since 4.0.0
    */
-  Text getMaxRow(String tableName, Authorizations auths, Text startRow, 
boolean startInclusive,
-      Text endRow, boolean endInclusive)
+  Text getMaxRow(String tableName, Authorizations auths, RowRange range)
       throws TableNotFoundException, AccumuloException, 
AccumuloSecurityException;
 
   /**
diff --git 
a/core/src/main/java/org/apache/accumulo/core/client/rfile/RFileSummariesRetriever.java
 
b/core/src/main/java/org/apache/accumulo/core/client/rfile/RFileSummariesRetriever.java
index 69b03c4a14..b53f7042b3 100644
--- 
a/core/src/main/java/org/apache/accumulo/core/client/rfile/RFileSummariesRetriever.java
+++ 
b/core/src/main/java/org/apache/accumulo/core/client/rfile/RFileSummariesRetriever.java
@@ -33,9 +33,9 @@ import 
org.apache.accumulo.core.client.rfile.RFileScannerBuilder.InputArgs;
 import org.apache.accumulo.core.client.summary.SummarizerConfiguration;
 import org.apache.accumulo.core.client.summary.Summary;
 import org.apache.accumulo.core.crypto.CryptoFactoryLoader;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.spi.crypto.CryptoEnvironment;
 import org.apache.accumulo.core.spi.crypto.CryptoService;
-import org.apache.accumulo.core.summary.Gatherer;
 import org.apache.accumulo.core.summary.SummarizerFactory;
 import org.apache.accumulo.core.summary.SummaryCollection;
 import org.apache.accumulo.core.summary.SummaryReader;
@@ -94,7 +94,7 @@ class RFileSummariesRetriever implements 
SummaryInputArguments, SummaryFSOptions
         SummaryReader fileSummary = 
SummaryReader.load(in.getFileSystem().getConf(), sources[i],
             "source-" + i, summarySelector, factory, cservice);
         SummaryCollection sc = fileSummary
-            .getSummaries(Collections.singletonList(new 
Gatherer.RowRange(startRow, endRow)));
+            .getSummaries(Collections.singletonList(RowRange.range(startRow, 
false, endRow, true)));
         all.merge(sc, factory);
       }
       return all.getSummaries();
diff --git 
a/core/src/main/java/org/apache/accumulo/core/clientImpl/TableOperationsImpl.java
 
b/core/src/main/java/org/apache/accumulo/core/clientImpl/TableOperationsImpl.java
index 082b57169c..a326f2336a 100644
--- 
a/core/src/main/java/org/apache/accumulo/core/clientImpl/TableOperationsImpl.java
+++ 
b/core/src/main/java/org/apache/accumulo/core/clientImpl/TableOperationsImpl.java
@@ -131,6 +131,7 @@ import org.apache.accumulo.core.data.ByteSequence;
 import org.apache.accumulo.core.data.Key;
 import org.apache.accumulo.core.data.PartialKey;
 import org.apache.accumulo.core.data.Range;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.data.TableId;
 import org.apache.accumulo.core.data.TabletId;
 import org.apache.accumulo.core.data.constraints.Constraint;
@@ -1571,12 +1572,12 @@ public class TableOperationsImpl extends 
TableOperationsHelper {
   }
 
   @Override
-  public Text getMaxRow(String tableName, Authorizations auths, Text startRow,
-      boolean startInclusive, Text endRow, boolean endInclusive) throws 
TableNotFoundException {
+  public Text getMaxRow(String tableName, Authorizations auths, RowRange 
rowRange)
+      throws TableNotFoundException {
     EXISTING_TABLE_NAME.validate(tableName);
-
-    Scanner scanner = context.createScanner(tableName, auths);
-    return FindMax.findMax(scanner, startRow, startInclusive, endRow, 
endInclusive);
+    try (Scanner scanner = context.createScanner(tableName, auths)) {
+      return FindMax.findMax(scanner, rowRange);
+    }
   }
 
   @Override
diff --git a/core/src/main/java/org/apache/accumulo/core/data/RowRange.java 
b/core/src/main/java/org/apache/accumulo/core/data/RowRange.java
new file mode 100644
index 0000000000..56553a7c7e
--- /dev/null
+++ b/core/src/main/java/org/apache/accumulo/core/data/RowRange.java
@@ -0,0 +1,656 @@
+/*
+ * 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
+ *
+ *   https://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.accumulo.core.data;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 4.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+
+  private static final Comparator<Text> LOWER_BOUND_COMPARATOR =
+      Comparator.nullsFirst(Text::compareTo);
+  private static final Comparator<Text> UPPER_BOUND_COMPARATOR =
+      Comparator.nullsLast(Text::compareTo);
+  private static final Comparator<RowRange> ROW_RANGE_COMPARATOR = (r1, r2) -> 
{
+    if (r1.lowerBound == null && r2.lowerBound == null) {
+      return 0;
+    } else if (r1.lowerBound == null) {
+      return -1;
+    } else if (r2.lowerBound == null) {
+      return 1;
+    }
+    return r1.compareTo(r2);
+  };
+
+  final private Text lowerBound;
+  final private Text upperBound;
+  final private boolean lowerBoundInclusive;
+  final private boolean upperBoundInclusive;
+  final private boolean infiniteLowerBound;
+  final private boolean infiniteUpperBound;
+
+  /**
+   * Creates a range that includes all possible rows.
+   */
+  public static RowRange all() {
+    return range((Text) null, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows from lowerBound exclusive to upperBound exclusive.
+   *
+   * @param lowerBound starting row
+   * @param upperBound ending row
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  public static RowRange open(Text lowerBound, Text upperBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.lessThan(row)?");
+    requireNonNull(upperBound, "Did you mean to use 
RowRange.greaterThan(row)?");
+    return range(lowerBound, false, upperBound, false);
+  }
+
+  /**
+   * Creates a range of rows from lowerBound exclusive to upperBound exclusive.
+   *
+   * @param lowerBound starting row
+   * @param upperBound ending row
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  public static RowRange open(CharSequence lowerBound, CharSequence 
upperBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.lessThan(row)?");
+    requireNonNull(upperBound, "Did you mean to use 
RowRange.greaterThan(row)?");
+    return range(lowerBound, false, upperBound, false);
+  }
+
+  /**
+   * Creates a range of rows from lowerBound inclusive to upperBound inclusive.
+   *
+   * @param lowerBound starting row
+   * @param upperBound ending row
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  public static RowRange closed(Text lowerBound, Text upperBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.atMost(row)?");
+    requireNonNull(upperBound, "Did you mean to use RowRange.atLeast(row)?");
+    return range(lowerBound, true, upperBound, true);
+  }
+
+  /**
+   * Creates a range of rows from lowerBound inclusive to upperBound inclusive.
+   *
+   * @param lowerBound starting row
+   * @param upperBound ending row
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  public static RowRange closed(CharSequence lowerBound, CharSequence 
upperBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.atMost(row)?");
+    requireNonNull(upperBound, "Did you mean to use RowRange.atLeast(row)?");
+    return range(lowerBound, true, upperBound, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover
+   */
+  public static RowRange closed(Text row) {
+    requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover
+   */
+  public static RowRange closed(CharSequence row) {
+    requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from lowerBound exclusive to upperBound inclusive.
+   *
+   * @param lowerBound starting row
+   * @param upperBound ending row
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  public static RowRange openClosed(Text lowerBound, Text upperBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.atMost(row)?");
+    requireNonNull(upperBound, "Did you mean to use 
RowRange.greaterThan(row)?");
+    return range(lowerBound, false, upperBound, true);
+  }
+
+  /**
+   * Creates a range of rows from lowerBound exclusive to upperBound inclusive.
+   *
+   * @param lowerBound starting row
+   * @param upperBound ending row
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  public static RowRange openClosed(CharSequence lowerBound, CharSequence 
upperBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.atMost(row)?");
+    requireNonNull(upperBound, "Did you mean to use 
RowRange.greaterThan(row)?");
+    return range(lowerBound, false, upperBound, true);
+  }
+
+  /**
+   * Creates a range of rows from lowerBound inclusive to upperBound exclusive.
+   *
+   * @param lowerBound starting row
+   * @param upperBound ending row
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  public static RowRange closedOpen(Text lowerBound, Text upperBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.lessThan(row)?");
+    requireNonNull(upperBound, "Did you mean to use RowRange.atLeast(row)?");
+    return range(lowerBound, true, upperBound, false);
+  }
+
+  /**
+   * Creates a range of rows from lowerBound inclusive to upperBound exclusive.
+   *
+   * @param lowerBound starting row
+   * @param upperBound ending row
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  public static RowRange closedOpen(CharSequence lowerBound, CharSequence 
upperBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.lessThan(row)?");
+    requireNonNull(upperBound, "Did you mean to use RowRange.atLeast(row)?");
+    return range(lowerBound, true, upperBound, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than the given row.
+   *
+   * @param lowerBound starting row
+   */
+  public static RowRange greaterThan(Text lowerBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.all()?");
+    return range(lowerBound, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than the given row.
+   *
+   * @param lowerBound starting row
+   */
+  public static RowRange greaterThan(CharSequence lowerBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.all()?");
+    return range(lowerBound, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to the given row.
+   *
+   * @param lowerBound starting row
+   */
+  public static RowRange atLeast(Text lowerBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.all()?");
+    return range(lowerBound, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to the given row.
+   *
+   * @param lowerBound starting row
+   */
+  public static RowRange atLeast(CharSequence lowerBound) {
+    requireNonNull(lowerBound, "Did you mean to use RowRange.all()?");
+    return range(lowerBound, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly less than the given row.
+   *
+   * @param upperBound ending row
+   */
+  public static RowRange lessThan(Text upperBound) {
+    requireNonNull(upperBound, "Did you mean to use RowRange.all()?");
+    return range(null, true, upperBound, false);
+  }
+
+  /**
+   * Creates a range of rows strictly less than the given row.
+   *
+   * @param upperBound ending row
+   */
+  public static RowRange lessThan(CharSequence upperBound) {
+    requireNonNull(upperBound, "Did you mean to use RowRange.all()?");
+    return range(null, true, upperBound, false);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to the given row.
+   *
+   * @param upperBound ending row
+   */
+  public static RowRange atMost(Text upperBound) {
+    requireNonNull(upperBound, "Did you mean to use RowRange.all()?");
+    return range(null, true, upperBound, true);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to the given row.
+   *
+   * @param upperBound ending row
+   */
+  public static RowRange atMost(CharSequence upperBound) {
+    requireNonNull(upperBound, "Did you mean to use RowRange.all()?");
+    return range(null, true, upperBound, true);
+  }
+
+  /**
+   * Creates a range of rows from the given lowerBound to the given upperBound.
+   *
+   * @param lowerBound starting row; set to null for the smallest possible row 
(an empty one)
+   * @param lowerBoundInclusive set to true to include lower bound, false to 
skip
+   * @param upperBound ending row; set to null for positive infinity
+   * @param upperBoundInclusive set to true to include upper bound, false to 
skip
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  public static RowRange range(Text lowerBound, boolean lowerBoundInclusive, 
Text upperBound,
+      boolean upperBoundInclusive) {
+    return new RowRange(lowerBound, lowerBoundInclusive, upperBound, 
upperBoundInclusive);
+  }
+
+  /**
+   * Creates a range of rows from the given lowerBound to the given upperBound.
+   *
+   * @param lowerBound starting row; set to null for the smallest possible row 
(an empty one)
+   * @param lowerBoundInclusive set to true to include lower bound, false to 
skip
+   * @param upperBound ending row; set to null for positive infinity
+   * @param upperBoundInclusive set to true to include upper bound, false to 
skip
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  public static RowRange range(CharSequence lowerBound, boolean 
lowerBoundInclusive,
+      CharSequence upperBound, boolean upperBoundInclusive) {
+    return new RowRange(lowerBound == null ? null : new 
Text(lowerBound.toString()),
+        lowerBoundInclusive, upperBound == null ? null : new 
Text(upperBound.toString()),
+        upperBoundInclusive);
+  }
+
+  /**
+   * Creates a range of rows from the given lowerBound to the given upperBound.
+   *
+   * @param lowerBound starting row; set to null for the smallest possible row 
(an empty one)
+   * @param lowerBoundInclusive set to true to include lower bound, false to 
skip
+   * @param upperBound ending row; set to null for positive infinity
+   * @param upperBoundInclusive set to true to include upper bound, false to 
skip
+   * @throws IllegalArgumentException if upper bound is before lower bound
+   */
+  private RowRange(Text lowerBound, boolean lowerBoundInclusive, Text 
upperBound,
+      boolean upperBoundInclusive) {
+    this.lowerBound = lowerBound == null ? null : new Text(lowerBound);
+    this.lowerBoundInclusive = lowerBoundInclusive;
+    this.infiniteLowerBound = lowerBound == null;
+    this.upperBound = upperBound == null ? null : new Text(upperBound);
+    this.upperBoundInclusive = upperBoundInclusive;
+    this.infiniteUpperBound = upperBound == null;
+
+    if (!infiniteLowerBound && !infiniteUpperBound && isAfterImpl(upperBound)) 
{
+      throw new IllegalArgumentException(
+          "Lower bound must be less than upper bound in row range " + this);
+    }
+  }
+
+  public Text getLowerBound() {
+    return lowerBound;
+  }
+
+  /**
+   * @return true if the lower bound is inclusive or null, otherwise false
+   */
+  public boolean isLowerBoundInclusive() {
+    return lowerBoundInclusive || lowerBound == null;
+  }
+
+  public Text getUpperBound() {
+    return upperBound;
+  }
+
+  /**
+   * @return true if the upper bound is inclusive or null, otherwise false
+   */
+  public boolean isUpperBoundInclusive() {
+    return upperBoundInclusive || upperBound == null;
+  }
+
+  /**
+   * Converts this row range to a {@link Range} object.
+   */
+  public Range asRange() {
+    return new Range(lowerBound, lowerBoundInclusive, upperBound, 
upperBoundInclusive);
+  }
+
+  /**
+   * @param that row to check
+   * @return true if this row range's lower bound is greater than the given 
row, otherwise false
+   */
+  public boolean isAfter(Text that) {
+    return isAfterImpl(that);
+  }
+
+  /**
+   * @param that row to check
+   * @return true if this row range's upper bound is less than the given row, 
otherwise false
+   */
+  public boolean isBefore(Text that) {
+    if (infiniteUpperBound) {
+      return false;
+    }
+
+    if (upperBoundInclusive) {
+      return that.compareTo(upperBound) > 0;
+    }
+    return that.compareTo(upperBound) >= 0;
+  }
+
+  /**
+   * Implements logic of {@link #isAfter(Text)}, but in a private method, so 
that it can be safely
+   * used by constructors if a subclass overrides that {@link #isAfter(Text)}
+   */
+  private boolean isAfterImpl(Text row) {
+    if (this.infiniteLowerBound) {
+      return false;
+    }
+
+    if (lowerBoundInclusive) {
+      return row.compareTo(lowerBound) < 0;
+    }
+    return row.compareTo(lowerBound) <= 0;
+  }
+
+  @Override
+  public String toString() {
+    final String startRangeSymbol = (lowerBoundInclusive && lowerBound != 
null) ? "[" : "(";
+    final String lowerBound = this.lowerBound == null ? "-inf" : 
this.lowerBound.toString();
+    final String upperBound = this.upperBound == null ? "+inf" : 
this.upperBound.toString();
+    final String endRangeSymbol = (upperBoundInclusive && this.upperBound != 
null) ? "]" : ")";
+    return startRangeSymbol + lowerBound + "," + upperBound + endRangeSymbol;
+  }
+
+  @Override
+  public int hashCode() {
+    int lowerHash = infiniteLowerBound ? 0 : lowerBound.hashCode() + 
(lowerBoundInclusive ? 1 : 0);
+    int upperHash = infiniteUpperBound ? 0 : upperBound.hashCode() + 
(upperBoundInclusive ? 1 : 0);
+
+    return lowerHash + upperHash;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (!(other instanceof RowRange)) {
+      return false;
+    }
+    return equals((RowRange) other);
+  }
+
+  /**
+   * Determines if this row range equals another.
+   *
+   * @param other range to compare
+   * @return true if row ranges are equal, otherwise false
+   */
+  public boolean equals(RowRange other) {
+    return compareTo(other) == 0;
+  }
+
+  /**
+   * Compares this row range to another row range. Compares in order: lower 
bound, inclusiveness of
+   * lower bound, upper bound, inclusiveness of upper bound. Infinite rows 
sort last, and
+   * non-infinite rows are compared with {@link 
Text#compareTo(BinaryComparable)}. Inclusive sorts
+   * before non-inclusive.
+   *
+   * @param other row range to compare
+   * @return comparison result
+   */
+  @Override
+  public int compareTo(RowRange other) {
+    // Compare infinite lower bounds
+    int comp = Boolean.compare(this.infiniteLowerBound, 
other.infiniteLowerBound);
+
+    if (comp == 0) {
+      // Compare non-infinite lower bounds and lower bound inclusiveness
+      if (!this.infiniteLowerBound) {
+        comp = LOWER_BOUND_COMPARATOR.compare(this.lowerBound, 
other.lowerBound);
+        if (comp == 0) {
+          comp = Boolean.compare(other.lowerBoundInclusive, 
this.lowerBoundInclusive);
+        }
+      }
+    }
+
+    if (comp == 0) {
+      // Compare infinite upper bounds
+      comp = Boolean.compare(this.infiniteUpperBound, 
other.infiniteUpperBound);
+
+      // Compare non-infinite upper bounds and upper bound inclusiveness
+      if (comp == 0 && !this.infiniteUpperBound) {
+        comp = UPPER_BOUND_COMPARATOR.compare(this.upperBound, 
other.upperBound);
+        if (comp == 0) {
+          comp = Boolean.compare(this.upperBoundInclusive, 
other.upperBoundInclusive);
+        }
+      }
+    }
+
+    return comp;
+  }
+
+  /**
+   * Determines if this row range contains the given row.
+   *
+   * @param row row to check
+   * @return true if the row is contained in the row range, otherwise false
+   */
+  public boolean contains(Text row) {
+    if (infiniteLowerBound) {
+      return !isBefore(row);
+    } else if (infiniteUpperBound) {
+      return !isAfter(row);
+    } else {
+      return !isAfter(row) && !isBefore(row);
+    }
+  }
+
+  /**
+   * Merges overlapping and adjacent row ranges. Adjacent ranges are those 
that share an endpoint
+   * where at least one range includes that endpoint. For example, given the 
following input:
+   *
+   * <pre>
+   * [a,c], (c, d], (g,m), (j,t]
+   * </pre>
+   *
+   * the following row ranges would be returned:
+   *
+   * <pre>
+   * [a,d], (g,t]
+   * </pre>
+   *
+   * @param rowRanges the collection of row ranges to merge
+   * @return a list of merged row ranges
+   */
+  public static List<RowRange> mergeOverlapping(Collection<RowRange> 
rowRanges) {
+    if (rowRanges.isEmpty()) {
+      return Collections.emptyList();
+    }
+    if (rowRanges.size() == 1) {
+      return Collections.singletonList(rowRanges.iterator().next());
+    }
+
+    List<RowRange> sortedRowRanges = new ArrayList<>(rowRanges);
+    // Sort row ranges by their lowerBound values
+    sortedRowRanges.sort(ROW_RANGE_COMPARATOR);
+
+    ArrayList<RowRange> mergedRowRanges = new ArrayList<>(rowRanges.size());
+
+    // Initialize the current range for merging
+    RowRange currentRange = sortedRowRanges.get(0);
+    boolean currentLowerBoundInclusive = 
sortedRowRanges.get(0).lowerBoundInclusive;
+
+    // Iterate through the sorted row ranges, merging overlapping and adjacent 
ranges
+    for (int i = 1; i < sortedRowRanges.size(); i++) {
+      if (currentRange.infiniteLowerBound && currentRange.infiniteUpperBound) {
+        // The current range covers all possible rows, no further merging 
needed
+        break;
+      }
+
+      RowRange nextRange = sortedRowRanges.get(i);
+
+      boolean lowerBoundsEqual = (currentRange.lowerBound == null && 
nextRange.lowerBound == null)
+          || (currentRange.lowerBound != null
+              && currentRange.lowerBound.equals(nextRange.lowerBound));
+
+      boolean shouldMerge = lowerBoundsEqual || 
currentRange.infiniteUpperBound;
+
+      if (!shouldMerge) {
+        if (nextRange.lowerBound == null) {
+          shouldMerge = true;
+        } else if (!currentRange.isBefore(nextRange.lowerBound)) {
+          shouldMerge = true;
+        } else if (currentRange.upperBound != null
+            && currentRange.upperBound.equals(nextRange.lowerBound)
+            && nextRange.lowerBoundInclusive) {
+          shouldMerge = true;
+        }
+      }
+
+      if (shouldMerge) {
+        int comparison;
+        if (nextRange.infiniteUpperBound) {
+          comparison = 1;
+        } else if (currentRange.upperBound == null) {
+          comparison = -1;
+        } else {
+          comparison = nextRange.upperBound.compareTo(currentRange.upperBound);
+        }
+        if (comparison > 0 || (comparison == 0 && 
nextRange.upperBoundInclusive)) {
+          currentRange = RowRange.range(currentRange.lowerBound, 
currentLowerBoundInclusive,
+              nextRange.upperBound, nextRange.upperBoundInclusive);
+        } // else current range contains the next range
+      } else {
+        // No overlap or adjacency
+        // add the current range to the merged list and update the current 
range
+        mergedRowRanges.add(currentRange);
+        currentRange = nextRange;
+        currentLowerBoundInclusive = nextRange.lowerBoundInclusive;
+      }
+    }
+
+    // Add the final current range to the merged list
+    mergedRowRanges.add(currentRange);
+
+    return mergedRowRanges;
+  }
+
+  /**
+   * Creates a row range which represents the intersection of this row range 
and the given row
+   * range. The following example will print true.
+   *
+   * <pre>
+   * RowRange rowRange1 = RowRange.closed(&quot;a&quot;, &quot;f&quot;);
+   * RowRange rowRange2 = RowRange.closed(&quot;c&quot;, &quot;n&quot;);
+   * RowRange rowRange3 = rowRange1.clip(rowRange2);
+   * System.out.println(rowRange3.equals(RowRange.closed(&quot;c&quot;, 
&quot;f&quot;)));
+   * </pre>
+   *
+   * @param rowRange row range to clip to
+   * @return the intersection of this row range and the given row range
+   * @throws IllegalArgumentException if row ranges do not overlap
+   */
+  public RowRange clip(RowRange rowRange) {
+    return clip(rowRange, false);
+  }
+
+  /**
+   * Creates a row range which represents the intersection of this row range 
and the given row
+   * range. Unlike {@link #clip(RowRange)}, this method can optionally return 
null if the row ranges
+   * do not overlap, instead of throwing an exception. The 
returnNullIfDisjoint parameter controls
+   * this behavior.
+   *
+   * @param rowRange row range to clip to
+   * @param returnNullIfDisjoint true to return null if row ranges are 
disjoint, false to throw an
+   *        exception
+   * @return the intersection of this row range and the given row range, or 
null if row ranges do
+   *         not overlap and returnNullIfDisjoint is true
+   * @throws IllegalArgumentException if row ranges do not overlap and 
returnNullIfDisjoint is false
+   * @see #clip(RowRange)
+   */
+  public RowRange clip(RowRange rowRange, boolean returnNullIfDisjoint) {
+    // Initialize lower bound and upper bound values with the current 
instance's values
+    Text lowerBound = this.lowerBound;
+    boolean lowerBoundInclusive = this.lowerBoundInclusive;
+    Text upperBound = this.upperBound;
+    boolean upperBoundInclusive = this.upperBoundInclusive;
+
+    // If the input rowRange has a defined lowerBound, update lowerBound and 
lowerBoundInclusive if
+    // needed
+    if (rowRange.lowerBound != null) {
+      // If the input rowRange's lowerBound is after this instance's 
upperBound or equal but not
+      // inclusive, they do not overlap
+      if (isBefore(rowRange.lowerBound) || 
(rowRange.lowerBound.equals(this.upperBound)
+          && !(rowRange.lowerBoundInclusive && this.upperBoundInclusive))) {
+        if (returnNullIfDisjoint) {
+          return null;
+        }
+        throw new IllegalArgumentException("RowRange " + rowRange + " does not 
overlap " + this);
+      } else if (!isAfter(rowRange.lowerBound)) {
+        // If the input rowRange's lowerBound is within this instance's range, 
use it as the new
+        // lowerBound
+        lowerBound = rowRange.lowerBound;
+        lowerBoundInclusive = rowRange.lowerBoundInclusive;
+      }
+    }
+
+    // If the input rowRange has a defined upperBound, update upperBound and 
upperBoundInclusive if
+    // needed
+    if (rowRange.upperBound != null) {
+      // If the input rowRange's upperBound is before this instance's 
lowerBound or equal but not
+      // inclusive, they do not overlap
+      if (isAfter(rowRange.upperBound) || 
(rowRange.upperBound.equals(this.lowerBound)
+          && !(rowRange.upperBoundInclusive && this.lowerBoundInclusive))) {
+        if (returnNullIfDisjoint) {
+          return null;
+        }
+        throw new IllegalArgumentException("RowRange " + rowRange + " does not 
overlap " + this);
+      } else if (!isBefore(rowRange.upperBound)) {
+        // If the input rowRange's upperBound is within this instance's range, 
use it as the new
+        // upperBound
+        upperBound = rowRange.upperBound;
+        upperBoundInclusive = rowRange.upperBoundInclusive;
+      }
+    }
+
+    // Return a new RowRange instance representing the intersection of the two 
ranges
+    return RowRange.range(lowerBound, lowerBoundInclusive, upperBound, 
upperBoundInclusive);
+  }
+
+}
diff --git a/core/src/main/java/org/apache/accumulo/core/summary/Gatherer.java 
b/core/src/main/java/org/apache/accumulo/core/summary/Gatherer.java
index d9258a1130..c67965a926 100644
--- a/core/src/main/java/org/apache/accumulo/core/summary/Gatherer.java
+++ b/core/src/main/java/org/apache/accumulo/core/summary/Gatherer.java
@@ -57,8 +57,8 @@ import org.apache.accumulo.core.conf.AccumuloConfiguration;
 import org.apache.accumulo.core.data.ByteSequence;
 import org.apache.accumulo.core.data.Key;
 import org.apache.accumulo.core.data.Range;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.data.TableId;
-import org.apache.accumulo.core.dataImpl.KeyExtent;
 import org.apache.accumulo.core.dataImpl.thrift.TRowRange;
 import org.apache.accumulo.core.dataImpl.thrift.TSummaries;
 import org.apache.accumulo.core.dataImpl.thrift.TSummaryRequest;
@@ -216,15 +216,16 @@ public class Gatherer {
         location = tservers.get(idx).toHostPortString();
       }
 
+      Function<RowRange,TRowRange> toTRowRange =
+          r -> new TRowRange(TextUtil.getByteBuffer(r.getLowerBound()),
+              TextUtil.getByteBuffer(r.getUpperBound()));
+
       // merge contiguous ranges
       List<Range> merged = Range.mergeOverlapping(entry.getValue().stream()
           .map(tm -> 
tm.getExtent().toDataRange()).collect(Collectors.toList()));
+      // clip ranges to queried range
       List<TRowRange> ranges =
-          merged.stream().map(r -> 
toClippedExtent(r).toThrift()).collect(Collectors.toList()); // clip
-                                                                               
                 // ranges
-                                                                               
                 // to
-                                                                               
                 // queried
-                                                                               
                 // range
+          
merged.stream().map(this::toClippedExtent).map(toTRowRange).collect(Collectors.toList());
 
       locations.computeIfAbsent(location, s -> new 
HashMap<>()).put(entry.getKey(), ranges);
     }
@@ -434,11 +435,15 @@ public class Gatherer {
   public Future<SummaryCollection> processFiles(FileSystemResolver volMgr,
       Map<String,List<TRowRange>> files, BlockCache summaryCache, BlockCache 
indexCache,
       Cache<String,Long> fileLenCache, ExecutorService srp) {
+    Function<TRowRange,RowRange> fromThrift = tRowRange -> {
+      Text lowerBound = ByteBufferUtil.toText(tRowRange.startRow);
+      Text upperBound = ByteBufferUtil.toText(tRowRange.endRow);
+      return RowRange.range(lowerBound, false, upperBound, true);
+    };
     List<CompletableFuture<SummaryCollection>> futures = new ArrayList<>();
     for (Entry<String,List<TRowRange>> entry : files.entrySet()) {
       futures.add(CompletableFuture.supplyAsync(() -> {
-        List<RowRange> rrl =
-            
entry.getValue().stream().map(RowRange::new).collect(Collectors.toList());
+        List<RowRange> rrl = 
entry.getValue().stream().map(fromThrift).collect(Collectors.toList());
         return getSummaries(volMgr, entry.getKey(), rrl, summaryCache, 
indexCache, fileLenCache);
       }, srp));
     }
@@ -537,51 +542,10 @@ public class Gatherer {
   private RowRange toClippedExtent(Range r) {
     r = clipRange.clip(r);
 
-    Text startRow = removeTrailingZeroFromRow(r.getStartKey());
-    Text endRow = removeTrailingZeroFromRow(r.getEndKey());
-
-    return new RowRange(startRow, endRow);
-  }
-
-  public static class RowRange {
-    private final Text startRow;
-    private final Text endRow;
-
-    public RowRange(KeyExtent ke) {
-      this.startRow = ke.prevEndRow();
-      this.endRow = ke.endRow();
-    }
-
-    public RowRange(TRowRange trr) {
-      this.startRow = ByteBufferUtil.toText(trr.startRow);
-      this.endRow = ByteBufferUtil.toText(trr.endRow);
-    }
-
-    public RowRange(Text startRow, Text endRow) {
-      this.startRow = startRow;
-      this.endRow = endRow;
-    }
+    final Text lowerBound = removeTrailingZeroFromRow(r.getStartKey());
+    final Text upperBound = removeTrailingZeroFromRow(r.getEndKey());
 
-    public Range toRange() {
-      return new Range(startRow, false, endRow, true);
-    }
-
-    public TRowRange toThrift() {
-      return new TRowRange(TextUtil.getByteBuffer(startRow), 
TextUtil.getByteBuffer(endRow));
-    }
-
-    public Text getStartRow() {
-      return startRow;
-    }
-
-    public Text getEndRow() {
-      return endRow;
-    }
-
-    @Override
-    public String toString() {
-      return startRow + " " + endRow;
-    }
+    return RowRange.range(lowerBound, false, upperBound, true);
   }
 
   private SummaryCollection getSummaries(FileSystemResolver volMgr, String 
file,
diff --git 
a/core/src/main/java/org/apache/accumulo/core/summary/SummaryReader.java 
b/core/src/main/java/org/apache/accumulo/core/summary/SummaryReader.java
index 3a600150b1..7888e80b67 100644
--- a/core/src/main/java/org/apache/accumulo/core/summary/SummaryReader.java
+++ b/core/src/main/java/org/apache/accumulo/core/summary/SummaryReader.java
@@ -31,6 +31,7 @@ import java.util.function.Predicate;
 
 import org.apache.accumulo.core.client.rfile.RFileSource;
 import org.apache.accumulo.core.client.summary.SummarizerConfiguration;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.file.NoSuchMetaStoreException;
 import org.apache.accumulo.core.file.blockfile.impl.BasicCacheProvider;
 import org.apache.accumulo.core.file.blockfile.impl.CachableBlockFile;
@@ -42,7 +43,6 @@ import org.apache.accumulo.core.spi.cache.BlockCache;
 import org.apache.accumulo.core.spi.cache.CacheEntry;
 import org.apache.accumulo.core.spi.cache.CacheType;
 import org.apache.accumulo.core.spi.crypto.CryptoService;
-import org.apache.accumulo.core.summary.Gatherer.RowRange;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.fs.FSDataInputStream;
 import org.apache.hadoop.fs.FileSystem;
diff --git 
a/core/src/main/java/org/apache/accumulo/core/summary/SummarySerializer.java 
b/core/src/main/java/org/apache/accumulo/core/summary/SummarySerializer.java
index 58b6e12191..1d5b257dd8 100644
--- a/core/src/main/java/org/apache/accumulo/core/summary/SummarySerializer.java
+++ b/core/src/main/java/org/apache/accumulo/core/summary/SummarySerializer.java
@@ -24,6 +24,7 @@ import java.io.DataOutputStream;
 import java.io.IOException;
 import java.io.PrintStream;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -35,8 +36,8 @@ import 
org.apache.accumulo.core.client.summary.Summarizer.Collector;
 import org.apache.accumulo.core.client.summary.Summarizer.Combiner;
 import org.apache.accumulo.core.client.summary.SummarizerConfiguration;
 import org.apache.accumulo.core.data.Key;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.data.Value;
-import org.apache.accumulo.core.summary.Gatherer.RowRange;
 import org.apache.hadoop.io.Text;
 import org.apache.hadoop.io.WritableUtils;
 
@@ -97,17 +98,8 @@ class SummarySerializer {
   }
 
   public boolean exceedsRange(List<RowRange> ranges) {
-    boolean er = false;
-    for (LgSummaries lgs : allSummaries) {
-      for (RowRange ke : ranges) {
-        er |= lgs.exceedsRange(ke.getStartRow(), ke.getEndRow());
-        if (er) {
-          return er;
-        }
-      }
-    }
-
-    return er;
+    ranges.forEach(SummarySerializer::validateSummaryRange);
+    return Arrays.stream(allSummaries).anyMatch(lgs -> 
ranges.stream().anyMatch(lgs::exceedsRange));
   }
 
   public boolean exceededMaxSize() {
@@ -431,7 +423,10 @@ class SummarySerializer {
       this.lgroupName = lgroupName;
     }
 
-    boolean exceedsRange(Text startRow, Text endRow) {
+    boolean exceedsRange(RowRange range) {
+
+      Text startRow = range.getLowerBound();
+      Text endRow = range.getUpperBound();
 
       if (summaries.length == 0) {
         return false;
@@ -463,22 +458,23 @@ class SummarySerializer {
     void getSummary(List<RowRange> ranges, Combiner combiner, Map<String,Long> 
summary) {
       boolean[] summariesThatOverlap = new boolean[summaries.length];
 
-      for (RowRange keyExtent : ranges) {
-        Text startRow = keyExtent.getStartRow();
-        Text endRow = keyExtent.getEndRow();
+      for (RowRange rowRange : ranges) {
+        validateSummaryRange(rowRange);
+        Text lowerBound = rowRange.getLowerBound();
+        Text upperBound = rowRange.getUpperBound();
 
-        if (endRow != null && endRow.compareTo(firstRow) < 0) {
+        if (upperBound != null && upperBound.compareTo(firstRow) < 0) {
           continue;
         }
 
         int start = -1;
         int end = summaries.length - 1;
 
-        if (startRow == null) {
+        if (lowerBound == null) {
           start = 0;
         } else {
           for (int i = 0; i < summaries.length; i++) {
-            if (startRow.compareTo(summaries[i].getLastRow()) < 0) {
+            if (lowerBound.compareTo(summaries[i].getLastRow()) < 0) {
               start = i;
               break;
             }
@@ -489,11 +485,11 @@ class SummarySerializer {
           continue;
         }
 
-        if (endRow == null) {
+        if (upperBound == null) {
           end = summaries.length - 1;
         } else {
           for (int i = start; i < summaries.length; i++) {
-            if (endRow.compareTo(summaries[i].getLastRow()) < 0) {
+            if (upperBound.compareTo(summaries[i].getLastRow()) < 0) {
               end = i;
               break;
             }
@@ -513,6 +509,20 @@ class SummarySerializer {
     }
   }
 
+  /**
+   * Ensures that the lower bound is exclusive and the upper bound is inclusive
+   */
+  private static void validateSummaryRange(RowRange rowRange) {
+    if (rowRange.getLowerBound() != null) {
+      Preconditions.checkState(!rowRange.isLowerBoundInclusive(),
+          "Summary row range lower bound must be exclusive: %s", rowRange);
+    }
+    if (rowRange.getUpperBound() != null) {
+      Preconditions.checkState(rowRange.isUpperBoundInclusive(),
+          "Summary row range upper bound must be inclusive: %s", rowRange);
+    }
+  }
+
   private static LgSummaries readLGroup(DataInputStream in, String[] symbols) 
throws IOException {
     String lgroupName = in.readUTF();
 
diff --git 
a/core/src/test/java/org/apache/accumulo/core/clientImpl/TableOperationsHelperTest.java
 
b/core/src/test/java/org/apache/accumulo/core/clientImpl/TableOperationsHelperTest.java
index f801745dc4..040d21fd0a 100644
--- 
a/core/src/test/java/org/apache/accumulo/core/clientImpl/TableOperationsHelperTest.java
+++ 
b/core/src/test/java/org/apache/accumulo/core/clientImpl/TableOperationsHelperTest.java
@@ -53,6 +53,7 @@ import 
org.apache.accumulo.core.client.admin.TabletMergeability;
 import org.apache.accumulo.core.client.sample.SamplerConfiguration;
 import org.apache.accumulo.core.client.summary.SummarizerConfiguration;
 import org.apache.accumulo.core.data.Range;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.iterators.IteratorUtil.IteratorScope;
 import org.apache.accumulo.core.security.Authorizations;
 import org.apache.hadoop.io.Text;
@@ -96,8 +97,7 @@ public class TableOperationsHelperTest {
     }
 
     @Override
-    public Text getMaxRow(String tableName, Authorizations auths, Text 
startRow,
-        boolean startInclusive, Text endRow, boolean endInclusive) {
+    public Text getMaxRow(String tableName, Authorizations auths, RowRange 
range) {
       return null;
     }
 
diff --git a/core/src/test/java/org/apache/accumulo/core/data/RowRangeTest.java 
b/core/src/test/java/org/apache/accumulo/core/data/RowRangeTest.java
new file mode 100644
index 0000000000..4a980a3b4e
--- /dev/null
+++ b/core/src/test/java/org/apache/accumulo/core/data/RowRangeTest.java
@@ -0,0 +1,910 @@
+/*
+ * 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
+ *
+ *   https://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.accumulo.core.data;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.hadoop.io.Text;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class RowRangeTest {
+
+  @Nested
+  class TestBasic {
+
+    @Test
+    void testGetters() {
+      Text lowerBound = new Text("r1");
+      Text upperBound = new Text("r1");
+      boolean isLowerBoundInclusive = true;
+      boolean isUpperBoundInclusive = false;
+
+      RowRange range =
+          RowRange.range(lowerBound, isLowerBoundInclusive, upperBound, 
isUpperBoundInclusive);
+
+      assertEquals(lowerBound, range.getLowerBound());
+      assertEquals(isLowerBoundInclusive, range.isLowerBoundInclusive());
+      assertEquals(upperBound, range.getUpperBound());
+      assertEquals(isUpperBoundInclusive, range.isUpperBoundInclusive());
+
+      lowerBound = new Text("r11");
+      upperBound = new Text("r22");
+      isLowerBoundInclusive = false;
+      isUpperBoundInclusive = true;
+
+      range = RowRange.range(lowerBound, isLowerBoundInclusive, upperBound, 
isUpperBoundInclusive);
+
+      assertEquals(lowerBound, range.getLowerBound());
+      assertEquals(isLowerBoundInclusive, range.isLowerBoundInclusive());
+      assertEquals(upperBound, range.getUpperBound());
+      assertEquals(isUpperBoundInclusive, range.isUpperBoundInclusive());
+    }
+
+    @Test
+    void testDefensiveCopyOfBounds() {
+      Text originalLower = new Text("a");
+      Text originalUpper = new Text("z");
+
+      RowRange range = RowRange.range(originalLower, true, originalUpper, 
false);
+
+      assertNotSame(originalLower, range.getLowerBound());
+      assertNotSame(originalUpper, range.getUpperBound());
+
+      originalLower.set("changed");
+      originalUpper.set("changed");
+
+      assertEquals(new Text("a"), range.getLowerBound());
+      assertEquals(new Text("z"), range.getUpperBound());
+    }
+
+    @Test
+    void testupperBoundBeforelowerBound() {
+      final Text lowerBound = new Text("r1");
+      final Text upperBound = new Text("r0");
+
+      assertTrue(lowerBound.compareTo(upperBound) > 0);
+
+      assertThrows(IllegalArgumentException.class,
+          () -> RowRange.range(lowerBound, true, upperBound, false));
+    }
+
+    @Test
+    void testBeforelowerBoundWithInfinitelowerBound() {
+      RowRange lessThanR1 = RowRange.lessThan("r1");
+
+      assertFalse(lessThanR1.isAfter(new Text("r0")));
+      assertFalse(lessThanR1.isAfter(new Text("r1")));
+      assertFalse(lessThanR1.isAfter(new Text("r2")));
+
+      RowRange rowRangeR1 = RowRange.closed("r1");
+
+      assertTrue(rowRangeR1.isAfter(new Text("r")));
+      assertTrue(rowRangeR1.isAfter(new Text("r0")));
+      assertFalse(rowRangeR1.isAfter(new Text("r1")));
+      assertFalse(rowRangeR1.isAfter(new Text("r2")));
+    }
+
+    @Test
+    public void testAfterupperBoundWithupperBoundInclusive() {
+      RowRange range = RowRange.closed(new Text("a"), new Text("c"));
+      assertFalse(range.isBefore(new Text("c")));
+      assertFalse(range.isBefore(new Text("b")));
+      assertFalse(range.isBefore(new Text("a")));
+      assertTrue(range.isBefore(new Text("d")));
+    }
+
+    @Test
+    public void testAfterupperBoundWithupperBoundExclusive() {
+      RowRange range = RowRange.open(new Text("a"), new Text("c"));
+      assertTrue(range.isBefore(new Text("c")));
+      assertFalse(range.isBefore(new Text("b")));
+      assertFalse(range.isBefore(new Text("a")));
+    }
+
+    @Test
+    public void testAfterupperBoundWithInfiniteupperBound() {
+      RowRange range = RowRange.greaterThan(new Text("a"));
+      assertFalse(range.isBefore(new Text("a")));
+      assertFalse(range.isBefore(new Text("b")));
+    }
+
+    @Test
+    public void testToRange() {
+      // Closed range
+      RowRange closedRange = RowRange.closed(new Text("a"), new Text("c"));
+      Range expectedClosedRange = new Range(new Text("a"), true, new 
Text("c"), true);
+      assertEquals(expectedClosedRange, closedRange.asRange());
+
+      // Open range
+      RowRange openRange = RowRange.open(new Text("a"), new Text("c"));
+      Range expectedOpenRange = new Range(new Text("a"), false, new Text("c"), 
false);
+      assertEquals(expectedOpenRange, openRange.asRange());
+
+      // Range with infinite upper bound
+      RowRange infiniteUpperBound = RowRange.greaterThan(new Text("a"));
+      Range expectedInfiniteUpperBound = new Range(new Text("a"), false, null, 
true);
+      assertEquals(expectedInfiniteUpperBound, infiniteUpperBound.asRange());
+
+      // Range with no lower bound
+      RowRange infiniteLowerBound = RowRange.lessThan(new Text("c"));
+      Range expectedInfiniteLowerBound = new Range(null, true, new Text("c"), 
false);
+      assertEquals(expectedInfiniteLowerBound, infiniteLowerBound.asRange());
+
+      // All rows range
+      RowRange allRange = RowRange.all();
+      Range expectedAllRange = new Range();
+      assertEquals(expectedAllRange, allRange.asRange());
+    }
+
+    @Test
+    public void testStaticMethodsThrowExceptionOnNullArgument() {
+      Stream<Runnable> methods = Stream.of(() -> RowRange.open(null, ""),
+          () -> RowRange.open("", null), () -> RowRange.closed(null, ""),
+          () -> RowRange.closed("", null), () -> RowRange.closed((Text) null),
+          () -> RowRange.openClosed(null, ""), () -> RowRange.openClosed("", 
null),
+          () -> RowRange.closedOpen(null, ""), () -> RowRange.closedOpen("", 
null),
+          () -> RowRange.greaterThan((Text) null), () -> 
RowRange.atLeast((Text) null),
+          () -> RowRange.lessThan((Text) null), () -> RowRange.atMost((Text) 
null));
+
+      methods.forEach(method -> assertThrows(NullPointerException.class, 
method::run));
+    }
+
+  }
+
+  @Nested
+  class StaticEntryPointTests {
+
+    @Test
+    void testOpenEquality() {
+      RowRange range1 = RowRange.open("r1", "row5");
+      RowRange range2 = RowRange.open(new Text("r1"), new Text("row5"));
+      RowRange range3 = RowRange.range(new Text("r1"), false, new 
Text("row5"), false);
+
+      assertTrue(range1.equals(range2));
+      assertTrue(range1.equals(range3));
+      assertTrue(range2.equals(range3));
+    }
+
+    @Test
+    void testClosedEquality() {
+      RowRange range1 = RowRange.closed("r1", "row5");
+      RowRange range2 = RowRange.closed(new Text("r1"), new Text("row5"));
+      RowRange range3 = RowRange.range(new Text("r1"), true, new Text("row5"), 
true);
+
+      assertTrue(range1.equals(range2));
+      assertTrue(range1.equals(range3));
+      assertTrue(range2.equals(range3));
+    }
+
+    @Test
+    void testClosedSingleRowEquality() {
+      RowRange range1 = RowRange.closed("r1");
+      RowRange range2 = RowRange.closed(new Text("r1"));
+      RowRange range3 = RowRange.range(new Text("r1"), true, new Text("r1"), 
true);
+
+      assertTrue(range1.equals(range2));
+      assertTrue(range1.equals(range3));
+      assertTrue(range2.equals(range3));
+    }
+
+    @Test
+    void testClosedOpenEquality() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen(new Text("r1"), new Text("row5"));
+      RowRange range3 = RowRange.range(new Text("r1"), true, new Text("row5"), 
false);
+
+      assertTrue(range1.equals(range2));
+      assertTrue(range1.equals(range3));
+      assertTrue(range2.equals(range3));
+    }
+
+    @Test
+    void testOpenClosedEquality() {
+      RowRange range1 = RowRange.openClosed("r1", "row5");
+      RowRange range2 = RowRange.openClosed(new Text("r1"), new Text("row5"));
+      RowRange range3 = RowRange.range(new Text("r1"), false, new 
Text("row5"), true);
+
+      assertTrue(range1.equals(range2));
+      assertTrue(range1.equals(range3));
+      assertTrue(range2.equals(range3));
+    }
+
+    @Test
+    void testGreaterThanEquality() {
+      RowRange range1 = RowRange.greaterThan("r1");
+      RowRange range2 = RowRange.greaterThan(new Text("r1"));
+      RowRange range3 = RowRange.range(new Text("r1"), false, null, false);
+
+      assertTrue(range1.equals(range2));
+      assertTrue(range1.equals(range3));
+      assertTrue(range2.equals(range3));
+    }
+
+    @Test
+    void testAtLeastEquality() {
+      RowRange range1 = RowRange.atLeast("r1");
+      RowRange range2 = RowRange.atLeast(new Text("r1"));
+      RowRange range3 = RowRange.range(new Text("r1"), true, null, false);
+
+      assertTrue(range1.equals(range2));
+      assertTrue(range1.equals(range3));
+      assertTrue(range2.equals(range3));
+    }
+
+    @Test
+    void testLessThanEquality() {
+      RowRange range1 = RowRange.lessThan("row5");
+      RowRange range2 = RowRange.lessThan(new Text("row5"));
+      RowRange range3 = RowRange.range(null, false, new Text("row5"), false);
+
+      assertTrue(range1.equals(range2));
+      assertTrue(range1.equals(range3));
+      assertTrue(range2.equals(range3));
+    }
+
+    @Test
+    void testAtMostEquality() {
+      RowRange range1 = RowRange.atMost("row5");
+      RowRange range2 = RowRange.atMost(new Text("row5"));
+      RowRange range3 = RowRange.range(null, false, new Text("row5"), true);
+
+      assertTrue(range1.equals(range2));
+      assertTrue(range1.equals(range3));
+      assertTrue(range2.equals(range3));
+    }
+
+    @Test
+    void testAllEquality() {
+      RowRange range1 = RowRange.all();
+      RowRange range2 = RowRange.range((Text) null, false, null, false);
+
+      assertTrue(range1.equals(range2));
+    }
+  }
+
+  @Nested
+  class EqualsTests {
+
+    @Test
+    void testEqualsWithObject() {
+      Object rowRange = RowRange.closedOpen("r1", "row5");
+      Object rowRange1 = RowRange.closedOpen("r1", "row5");
+      RowRange rowRange2 = RowRange.closedOpen("r1", "row5");
+
+      assertEquals(rowRange, rowRange1);
+      assertEquals(rowRange, rowRange2);
+
+      String badRange = "r1";
+      assertNotEquals(rowRange, badRange);
+    }
+
+    @Test
+    void testEqualsWithDifferentRanges() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen("r2", "row4");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithSameRange() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen("r1", "row5");
+      assertTrue(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentlowerBoundInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.openClosed("r1", "row5");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentupperBoundInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen("r1", "row4");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentlowerBoundAndupperBoundInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.openClosed("r1", "row4");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testOverloadEquality() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen(new Text("r1"), new Text("row5"));
+      RowRange range3 = RowRange.atLeast("row8");
+      RowRange range4 = RowRange.atLeast(new Text("row8"));
+      RowRange range5 = RowRange.lessThan("r2");
+      RowRange range6 = RowRange.lessThan(new Text("r2"));
+      RowRange range7 = RowRange.atMost("r3");
+      RowRange range8 = RowRange.atMost(new Text("r3"));
+
+      // Test that all ranges created using different entry point methods are 
equal
+      assertTrue(range1.equals(range2));
+      assertTrue(range3.equals(range4));
+      assertTrue(range5.equals(range6));
+      assertTrue(range7.equals(range8));
+
+      // Test that ranges with different properties are not equal
+      assertFalse(range1.equals(range3));
+      assertFalse(range1.equals(range5));
+      assertFalse(range1.equals(range7));
+      assertFalse(range3.equals(range5));
+      assertFalse(range3.equals(range7));
+      assertFalse(range5.equals(range7));
+    }
+  }
+
+  @Nested
+  class CompareToTests {
+
+    @Test
+    void testCompareWithSameRange() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r1", "r3");
+
+      assertEquals(0, range1.compareTo(range2));
+      assertEquals(0, range2.compareTo(range1));
+
+      range1 = RowRange.closed("r1", "r3");
+      range2 = RowRange.closed("r1", "r3");
+
+      assertEquals(0, range1.compareTo(range2));
+      assertEquals(0, range2.compareTo(range1));
+
+      range1 = RowRange.closedOpen("r1", "r3");
+      range2 = RowRange.closedOpen("r1", "r3");
+
+      assertEquals(0, range1.compareTo(range2));
+      assertEquals(0, range2.compareTo(range1));
+
+      range1 = RowRange.closed("r1");
+      range2 = RowRange.closed("r1");
+
+      assertEquals(0, range1.compareTo(range2));
+      assertEquals(0, range2.compareTo(range1));
+
+      range1 = RowRange.openClosed("r1", "r3");
+      range2 = RowRange.openClosed("r1", "r3");
+
+      assertEquals(0, range1.compareTo(range2));
+      assertEquals(0, range2.compareTo(range1));
+
+      range1 = RowRange.greaterThan("r1");
+      range2 = RowRange.greaterThan("r1");
+
+      assertEquals(0, range1.compareTo(range2));
+      assertEquals(0, range2.compareTo(range1));
+
+      range1 = RowRange.atLeast("r1");
+      range2 = RowRange.atLeast("r1");
+
+      assertEquals(0, range1.compareTo(range2));
+      assertEquals(0, range2.compareTo(range1));
+
+      range1 = RowRange.lessThan("r1");
+      range2 = RowRange.lessThan("r1");
+
+      assertEquals(0, range1.compareTo(range2));
+      assertEquals(0, range2.compareTo(range1));
+
+      range1 = RowRange.atMost("r1");
+      range2 = RowRange.atMost("r1");
+
+      assertEquals(0, range1.compareTo(range2));
+      assertEquals(0, range2.compareTo(range1));
+
+      range1 = RowRange.all();
+      range2 = RowRange.all();
+
+      assertEquals(0, range1.compareTo(range2));
+      assertEquals(0, range2.compareTo(range1));
+    }
+
+    @Test
+    void testCompareWithDifferentlowerBound() {
+      RowRange open_r1_r3 = RowRange.open("r1", "r3");
+      RowRange open_r2_r3 = RowRange.open("r2", "r3");
+
+      assertTrue(open_r1_r3.compareTo(open_r2_r3) < 0);
+      assertTrue(open_r2_r3.compareTo(open_r1_r3) > 0);
+    }
+
+    @Test
+    void testCompareWithDifferentupperBound() {
+      RowRange open_r1_r3 = RowRange.open("r1", "r3");
+      RowRange open_r1_r4 = RowRange.open("r1", "r4");
+
+      assertTrue(open_r1_r3.compareTo(open_r1_r4) < 0);
+      assertTrue(open_r1_r4.compareTo(open_r1_r3) > 0);
+    }
+
+    @Test
+    void testCompareWithDifferentlowerBoundInclusiveness() {
+      RowRange open = RowRange.open("r1", "r3");
+      RowRange closedOpen = RowRange.closedOpen("r1", "r3");
+
+      assertTrue(open.compareTo(closedOpen) > 0);
+      assertTrue(closedOpen.compareTo(open) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentupperBoundInclusiveness() {
+      RowRange open = RowRange.open("r1", "r3");
+      RowRange openClosed = RowRange.openClosed("r1", "r3");
+
+      assertTrue(open.compareTo(openClosed) < 0);
+      assertTrue(openClosed.compareTo(open) > 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteupperBound() {
+      RowRange atLeast_r1 = RowRange.atLeast("r1");
+      RowRange greaterThan_r1 = RowRange.greaterThan("r1");
+      RowRange all = RowRange.all();
+
+      assertTrue(atLeast_r1.compareTo(all) < 0);
+      assertTrue(all.compareTo(atLeast_r1) > 0);
+
+      assertTrue(greaterThan_r1.compareTo(all) < 0);
+      assertTrue(all.compareTo(greaterThan_r1) > 0);
+
+      assertTrue(atLeast_r1.compareTo(greaterThan_r1) < 0);
+      assertTrue(greaterThan_r1.compareTo(atLeast_r1) > 0);
+    }
+
+    @Test
+    void testCompareWithInfinitelowerBound() {
+      RowRange atMost_r1 = RowRange.atMost("r1");
+      RowRange lessThan_r1 = RowRange.lessThan("r1");
+      RowRange all = RowRange.all();
+
+      assertTrue(atMost_r1.compareTo(all) < 0);
+      assertTrue(all.compareTo(atMost_r1) > 0);
+
+      assertTrue(lessThan_r1.compareTo(all) < 0);
+      assertTrue(all.compareTo(lessThan_r1) > 0);
+
+      assertTrue(atMost_r1.compareTo(lessThan_r1) > 0);
+      assertTrue(lessThan_r1.compareTo(atMost_r1) < 0);
+    }
+  }
+
+  @Nested
+  class ContainsTests {
+
+    @Test
+    void testContainsWithAllRange() {
+      RowRange range = RowRange.all();
+      assertTrue(range.contains(new Text(new byte[] {})));
+      assertTrue(range.contains(new Text("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertTrue(range.contains(new Text("row3")));
+    }
+
+    @Test
+    void testContainsWithOpenRange() {
+      RowRange range = RowRange.open("r1", "r3");
+      assertFalse(range.contains(new Text(new byte[] {})));
+      assertFalse(range.contains(new Text("r0")));
+      assertFalse(range.contains(new Text("r1")));
+      assertTrue(range.contains(new Text(new byte[] {'r', '1', 0})));
+      assertTrue(range.contains(new Text("r11")));
+      assertTrue(range.contains(new Text("r2")));
+      assertFalse(range.contains(new Text(new byte[] {'r', '3', 0})));
+      assertFalse(range.contains(new Text("r3")));
+    }
+
+    @Test
+    void testContainsWithClosedRange() {
+      RowRange range = RowRange.closed("r1", "r3");
+      assertFalse(range.contains(new Text("r0")));
+      assertTrue(range.contains(new Text("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertTrue(range.contains(new Text("r3")));
+    }
+
+    @Test
+    void testContainsWithOpenClosedRange() {
+      RowRange range = RowRange.openClosed("r1", "r3");
+      assertFalse(range.contains(new Text("r0")));
+      assertFalse(range.contains(new Text("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertTrue(range.contains(new Text("r3")));
+      assertFalse(range.contains(new Text("r30")));
+    }
+
+    @Test
+    void testContainsWithClosedOpenRange() {
+      RowRange range = RowRange.closedOpen("r1", "r3");
+      assertFalse(range.contains(new Text("r0")));
+      assertTrue(range.contains(new Text("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertFalse(range.contains(new Text("r3")));
+    }
+
+    @Test
+    void testContainsWithSingleRowRange() {
+      RowRange range = RowRange.closed("r1");
+      assertTrue(range.contains(new Text("r1")));
+      assertFalse(range.contains(new Text("r2")));
+    }
+
+    @Test
+    void testContainsWithAtLeastRange() {
+      RowRange range = RowRange.atLeast("r1");
+      assertTrue(range.contains(new Text("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertFalse(range.contains(new Text("")));
+    }
+
+    @Test
+    void testContainsWithAtMostRange() {
+      RowRange range = RowRange.atMost("r1");
+      assertTrue(range.contains(new Text(new byte[] {})));
+      assertTrue(range.contains(new Text("r1")));
+      assertFalse(range.contains(new Text("r10")));
+      assertFalse(range.contains(new Text("r2")));
+      assertTrue(range.contains(new Text("")));
+    }
+  }
+
+  @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+  @Nested
+  class MergeOverlappingTests {
+
+    @ParameterizedTest
+    @MethodSource({"rowRangeProvider", "rowRangeProvider1"})
+    public void testMergeOverlapping(List<RowRange> rowRangesToMerge, 
List<RowRange> expected) {
+      List<RowRange> actual = RowRange.mergeOverlapping(rowRangesToMerge);
+      verifyMerge(expected, actual);
+    }
+
+    private void verifyMerge(List<RowRange> expectedList, List<RowRange> 
actualList) {
+      HashSet<RowRange> expectedSet = new HashSet<>(expectedList);
+      HashSet<RowRange> actualSet = new HashSet<>(actualList);
+      assertEquals(expectedSet, actualSet, "Expected: " + expectedSet + " 
Actual: " + actualSet);
+    }
+
+    Stream<Arguments> rowRangeProvider() {
+      return Stream.of(
+          // [a,c] [a,b] -> [a,c]
+          Arguments.of(List.of(RowRange.closed("a", "c"), RowRange.closed("a", 
"b")),
+              List.of(RowRange.closed("a", "c"))),
+          // [a,c] [d,f] -> [a,c] [d,f]
+          Arguments.of(List.of(RowRange.closed("a", "c"), RowRange.closed("d", 
"f")),
+              List.of(RowRange.closed("a", "c"), RowRange.closed("d", "f"))),
+          // [a,e] [b,f] [c,r] [g,j] [t,x] -> [a,r] [t,x]
+          Arguments.of(
+              List.of(RowRange.closed("a", "e"), RowRange.closed("b", "f"),
+                  RowRange.closed("c", "r"), RowRange.closed("g", "j"), 
RowRange.closed("t", "x")),
+              List.of(RowRange.closed("a", "r"), RowRange.closed("t", "x"))),
+          // [a,e] [b,f] [c,r] [g,j] -> [a,r]
+          Arguments.of(
+              List.of(RowRange.closed("a", "e"), RowRange.closed("b", "f"),
+                  RowRange.closed("c", "r"), RowRange.closed("g", "j")),
+              List.of(RowRange.closed("a", "r"))),
+          // [a,e] -> [a,e]
+          Arguments.of(List.of(RowRange.closed("a", "e")), 
List.of(RowRange.closed("a", "e"))),
+          // [] -> []
+          Arguments.of(List.of(), List.of()),
+          // [a,e] [g,q] [r,z] -> [a,e] [g,q] [r,z]
+          Arguments.of(
+              List.of(RowRange.closed("a", "e"), RowRange.closed("g", "q"),
+                  RowRange.closed("r", "z")),
+              List.of(RowRange.closed("a", "e"), RowRange.closed("g", "q"),
+                  RowRange.closed("r", "z"))),
+          // [a,c] [a,c] -> [a,c]
+          Arguments.of(List.of(RowRange.closed("a", "c"), RowRange.closed("a", 
"c")),
+              List.of(RowRange.closed("a", "c"))),
+          // [ALL] -> [ALL]
+          Arguments.of(List.of(RowRange.all()), List.of(RowRange.all())),
+          // [ALL] [a,c] -> [ALL]
+          Arguments.of(List.of(RowRange.all(), RowRange.closed("a", "c")), 
List.of(RowRange.all())),
+          // [a,c] [ALL] -> [ALL]
+          Arguments.of(List.of(RowRange.closed("a", "c"), RowRange.all()), 
List.of(RowRange.all())),
+          // [b,d] [c,+inf) -> [b,+inf)
+          Arguments.of(List.of(RowRange.closed("b", "d"), 
RowRange.atLeast("c")),
+              List.of(RowRange.atLeast("b"))),
+          // [b,d] [a,+inf) -> [a,+inf)
+          Arguments.of(List.of(RowRange.closed("b", "d"), 
RowRange.atLeast("a")),
+              List.of(RowRange.atLeast("a"))),
+          // [b,d] [e,+inf) -> [b,d] [e,+inf)
+          Arguments.of(List.of(RowRange.closed("b", "d"), 
RowRange.atLeast("e")),
+              List.of(RowRange.closed("b", "d"), RowRange.atLeast("e"))),
+          // [b,d] [e,+inf) [c,f] -> [b,+inf)
+          Arguments.of(
+              List.of(RowRange.closed("b", "d"), RowRange.atLeast("e"), 
RowRange.closed("c", "f")),
+              List.of(RowRange.atLeast("b"))),
+          // [b,d] [f,+inf) [c,e] -> [b,e] [f,+inf)
+          Arguments.of(
+              List.of(RowRange.closed("b", "d"), RowRange.atLeast("f"), 
RowRange.closed("c", "e")),
+              List.of(RowRange.closed("b", "e"), RowRange.atLeast("f"))),
+          // [b,d] [r,+inf) [c,e] [g,t] -> [b,e] [g,+inf)
+          Arguments.of(
+              List.of(RowRange.closed("b", "d"), RowRange.atLeast("r"), 
RowRange.closed("c", "e"),
+                  RowRange.closed("g", "t")),
+              List.of(RowRange.closed("b", "e"), RowRange.atLeast("g"))),
+          // (-inf,d] [r,+inf) [c,e] [g,t] -> (-inf,e] [g,+inf)
+          Arguments.of(List.of(RowRange.atMost("d"), RowRange.atLeast("r"),
+              RowRange.closed("c", "e"), RowRange.closed("g", "t")),
+              List.of(RowRange.atMost("e"), RowRange.atLeast("g"))),
+          // (-inf,d] [r,+inf) [c,e] [g,t] [d,h] -> (-inf,+inf)
+          Arguments.of(List.of(RowRange.atMost("d"), RowRange.atLeast("r"),
+              RowRange.closed("c", "e"), RowRange.closed("g", "t"), 
RowRange.closed("d", "h")),
+              List.of(RowRange.all())),
+          // [a,b) (b,c) -> [a,b), (b,c)
+          Arguments.of(List.of(RowRange.closedOpen("a", "b"), 
RowRange.open("b", "c")),
+              List.of(RowRange.closedOpen("a", "b"), RowRange.open("b", "c"))),
+          // [a,b) [b,c) -> [a,c)
+          Arguments.of(List.of(RowRange.closedOpen("a", "b"), 
RowRange.closedOpen("b", "c")),
+              List.of(RowRange.closedOpen("a", "c"))),
+          // [a,b] (b,c) -> [a,c)
+          Arguments.of(List.of(RowRange.closed("a", "b"), RowRange.open("b", 
"c")),
+              List.of(RowRange.closedOpen("a", "c"))),
+          // [a,b] [b,c) -> [a,c)
+          Arguments.of(List.of(RowRange.closed("a", "b"), 
RowRange.closedOpen("b", "c")),
+              List.of(RowRange.closedOpen("a", "c"))),
+          // (-inf,b] (-inf,c] -> (-inf,c]
+          Arguments.of(List.of(RowRange.atMost("b"), RowRange.atMost("c")),
+              List.of(RowRange.atMost("c"))),
+          // (-inf,b] (-inf,c] [a,d] -> (-inf,d]
+          Arguments.of(
+              List.of(RowRange.atMost("b"), RowRange.atMost("c"), 
RowRange.closed("a", "d")),
+              List.of(RowRange.atMost("d"))),
+          // (-inf,b] (-inf,c] [a,d] [e,f] -> (-inf,d] [e,f]
+          Arguments.of(
+              List.of(RowRange.atMost("b"), RowRange.atMost("c"), 
RowRange.closed("a", "d"),
+                  RowRange.closed("e", "f")),
+              List.of(RowRange.atMost("d"), RowRange.closed("e", "f"))),
+          // [a] [b] -> [a] [b]
+          Arguments.of(List.of(RowRange.closed("a"), RowRange.closed("b")),
+              List.of(RowRange.closed("a"), RowRange.closed("b"))),
+          // (a,b] [b,c] -> (a,c]
+          Arguments.of(List.of(RowRange.openClosed("a", "b"), 
RowRange.closed("b", "c")),
+              List.of(RowRange.openClosed("a", "c"))),
+          // (a,b] (b,+inf) -> (a,+inf)
+          Arguments.of(List.of(RowRange.openClosed("a", "b"), 
RowRange.atLeast("b")),
+              List.of(RowRange.greaterThan("a"))),
+          // (-inf,b) (-inf,c) -> (-inf,c)
+          Arguments.of(List.of(RowRange.lessThan("b"), RowRange.lessThan("c")),
+              List.of(RowRange.lessThan("c"))));
+    }
+
+    Stream<Arguments> rowRangeProvider1() {
+      Stream.Builder<Arguments> builder = Stream.builder();
+
+      for (boolean b1 : new boolean[] {true, false}) {
+        for (boolean b2 : new boolean[] {true, false}) {
+          for (boolean b3 : new boolean[] {true, false}) {
+            for (boolean b4 : new boolean[] {true, false}) {
+              List<RowRange> rl =
+                  List.of(RowRange.range("a", b1, "m", b2), 
RowRange.range("b", b3, "n", b4));
+              List<RowRange> expected = List.of(RowRange.range("a", b1, "n", 
b4));
+              builder.add(Arguments.of(rl, expected));
+
+              rl = List.of(RowRange.range("a", b1, "m", b2), 
RowRange.range("a", b3, "n", b4));
+              expected = List.of(RowRange.range("a", b1 || b3, "n", b4));
+              builder.add(Arguments.of(rl, expected));
+
+              rl = List.of(RowRange.range("a", b1, "n", b2), 
RowRange.range("b", b3, "n", b4));
+              expected = List.of(RowRange.range("a", b1, "n", b2 || b4));
+              builder.add(Arguments.of(rl, expected));
+
+              rl = List.of(RowRange.range("a", b1, "n", b2), 
RowRange.range("a", b3, "n", b4));
+              expected = List.of(RowRange.range("a", b1 || b3, "n", b2 || b4));
+              builder.add(Arguments.of(rl, expected));
+            }
+          }
+        }
+      }
+
+      return builder.build();
+    }
+
+  }
+
+  @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+  @Nested
+  class ClipTests {
+
+    @ParameterizedTest
+    @MethodSource("clipTestArguments")
+    void testClip(RowRange fence, RowRange range, RowRange expected) {
+      if (expected != null) {
+        RowRange clipped = fence.clip(range);
+        assertEquals(expected, clipped);
+      } else {
+        assertThrows(IllegalArgumentException.class, () -> fence.clip(range));
+      }
+    }
+
+    private Stream<Arguments> clipTestArguments() {
+      RowRange fenceOpen = RowRange.open("a", "c");
+      RowRange fenceClosedOpen = RowRange.closedOpen("a", "c");
+      RowRange fenceOpenClosed = RowRange.openClosed("a", "c");
+      RowRange fenceClosed = RowRange.closed("a", "c");
+
+      RowRange fenceOpenCN = RowRange.open("c", "n");
+      RowRange fenceClosedCN = RowRange.closed("c", "n");
+      RowRange fenceClosedB = RowRange.closed("b");
+
+      return Stream.of(
+          // (a,c) (a,c) -> (a,c)
+          Arguments.of(fenceOpen, RowRange.open("a", "c"), RowRange.open("a", 
"c")),
+          // (a,c) [a,c) -> (a,c)
+          Arguments.of(fenceOpen, RowRange.closedOpen("a", "c"), 
RowRange.open("a", "c")),
+          // (a,c) (a,c] -> (a,c)
+          Arguments.of(fenceOpen, RowRange.openClosed("a", "c"), 
RowRange.open("a", "c")),
+          // (a,c) [a,c] -> (a,c)
+          Arguments.of(fenceOpen, RowRange.closed("a", "c"), 
RowRange.open("a", "c")),
+
+          // [a,c) (a,c) -> (a,c)
+          Arguments.of(fenceClosedOpen, RowRange.open("a", "c"), 
RowRange.open("a", "c")),
+          // [a,c) [a,c) -> [a,c)
+          Arguments.of(fenceClosedOpen, RowRange.closedOpen("a", "c"),
+              RowRange.closedOpen("a", "c")),
+          // [a,c) (a,c] -> (a,c)
+          Arguments.of(fenceClosedOpen, RowRange.openClosed("a", "c"), 
RowRange.open("a", "c")),
+          // [a,c) [a,c] -> [a,c)
+          Arguments.of(fenceClosedOpen, RowRange.closed("a", "c"), 
RowRange.closedOpen("a", "c")),
+
+          // (a,c] (a,c) -> (a,c)
+          Arguments.of(fenceOpenClosed, RowRange.open("a", "c"), 
RowRange.open("a", "c")),
+          // (a,c] [a,c) -> (a,c)
+          Arguments.of(fenceOpenClosed, RowRange.closedOpen("a", "c"), 
RowRange.open("a", "c")),
+          // (a,c] (a,c] -> (a,c]
+          Arguments.of(fenceOpenClosed, RowRange.openClosed("a", "c"),
+              RowRange.openClosed("a", "c")),
+          // (a,c] [a,c] -> (a,c]
+          Arguments.of(fenceOpenClosed, RowRange.closed("a", "c"), 
RowRange.openClosed("a", "c")),
+          // (a,c] (-inf, c) -> (a,c)
+          Arguments.of(fenceOpenClosed, RowRange.lessThan("c"), 
RowRange.open("a", "c")),
+          // (a,c] (-inf, a) -> empty
+          Arguments.of(fenceOpenClosed, RowRange.lessThan("a"), null),
+
+          // [a,c] (a,c) -> (a,c)
+          Arguments.of(fenceClosed, RowRange.open("a", "c"), 
RowRange.open("a", "c")),
+          // [a,c] [a,c) -> [a,c)
+          Arguments.of(fenceClosed, RowRange.closedOpen("a", "c"), 
RowRange.closedOpen("a", "c")),
+          // [a,c] (a,c] -> (a,c]
+          Arguments.of(fenceClosed, RowRange.openClosed("a", "c"), 
RowRange.openClosed("a", "c")),
+          // [a,c] [a,c] -> [a,c]
+          Arguments.of(fenceClosed, RowRange.closed("a", "c"), 
RowRange.closed("a", "c")),
+
+          // (a,c) (-inf, +inf) -> (a,c)
+          Arguments.of(fenceOpen, RowRange.all(), fenceOpen),
+          // (a,c) [a, +inf) -> (a,c)
+          Arguments.of(fenceOpen, RowRange.atLeast("a"), fenceOpen),
+          // (a,c) (-inf, c] -> (a,c)
+          Arguments.of(fenceOpen, RowRange.atMost("c"), fenceOpen),
+          // (a,c) [a,c] -> (a,c)
+          Arguments.of(fenceOpen, RowRange.closed("a", "c"), fenceOpen),
+
+          // (a,c) (0,z) -> (a,c)
+          Arguments.of(fenceOpen, RowRange.open("0", "z"), fenceOpen),
+          // (a,c) [0,z) -> (a,c)
+          Arguments.of(fenceOpen, RowRange.closedOpen("0", "z"), fenceOpen),
+          // (a,c) (0,z] -> (a,c)
+          Arguments.of(fenceOpen, RowRange.openClosed("0", "z"), fenceOpen),
+          // (a,c) [0,z] -> (a,c)
+          Arguments.of(fenceOpen, RowRange.closed("0", "z"), fenceOpen),
+
+          // (a,c) (0,b) -> (a,b)
+          Arguments.of(fenceOpen, RowRange.open("0", "b"), RowRange.open("a", 
"b")),
+          // (a,c) [0,b) -> (a,b)
+          Arguments.of(fenceOpen, RowRange.closedOpen("0", "b"), 
RowRange.open("a", "b")),
+          // (a,c) (0,b] -> (a,b]
+          Arguments.of(fenceOpen, RowRange.openClosed("0", "b"), 
RowRange.openClosed("a", "b")),
+          // (a,c) [0,b] -> (a,b]
+          Arguments.of(fenceOpen, RowRange.closed("0", "b"), 
RowRange.openClosed("a", "b")),
+
+          // (a,c) (a1,z) -> (a1,c)
+          Arguments.of(fenceOpen, RowRange.open("a1", "z"), 
RowRange.open("a1", "c")),
+          // (a,c) [a1,z) -> [a1,c)
+          Arguments.of(fenceOpen, RowRange.closedOpen("a1", "z"), 
RowRange.closedOpen("a1", "c")),
+          // (a,c) (a1,z] -> (a1,c)
+          Arguments.of(fenceOpen, RowRange.openClosed("a1", "z"), 
RowRange.open("a1", "c")),
+          // (a,c) [a1,z] -> [a1,c)
+          Arguments.of(fenceOpen, RowRange.closed("a1", "z"), 
RowRange.closedOpen("a1", "c")),
+
+          // (a,c) (a1,b) -> (a1,b)
+          Arguments.of(fenceOpen, RowRange.open("a1", "b"), 
RowRange.open("a1", "b")),
+          // (a,c) [a1,b) -> [a1,b)
+          Arguments.of(fenceOpen, RowRange.closedOpen("a1", "b"), 
RowRange.closedOpen("a1", "b")),
+          // (a,c) (a1,b] -> (a1,b]
+          Arguments.of(fenceOpen, RowRange.openClosed("a1", "b"), 
RowRange.openClosed("a1", "b")),
+          // (a,c) [a1,b] -> [a1,b]
+          Arguments.of(fenceOpen, RowRange.closed("a1", "b"), 
RowRange.closed("a1", "b")),
+          // (a,c) (a,+inf) -> (a,c)
+          Arguments.of(fenceOpen, RowRange.greaterThan("a"), 
RowRange.open("a", "c")),
+          // (a,c) (1,+inf) -> (a,c)
+          Arguments.of(fenceOpen, RowRange.greaterThan("1"), 
RowRange.open("a", "c")),
+
+          // (c,n) (a,c) -> empty
+          Arguments.of(fenceOpenCN, RowRange.open("a", "c"), null),
+          // (c,n) (a,c] -> empty
+          Arguments.of(fenceOpenCN, RowRange.closedOpen("a", "c"), null),
+          // (c,n) (n,r) -> empty
+          Arguments.of(fenceOpenCN, RowRange.open("n", "r"), null),
+          // (c,n) [n,r) -> empty
+          Arguments.of(fenceOpenCN, RowRange.closedOpen("n", "r"), null),
+          // (c,n) (a,b) -> empty
+          Arguments.of(fenceOpenCN, RowRange.open("a", "b"), null),
+          // (c,n) (a,b] -> empty
+          Arguments.of(fenceOpenCN, RowRange.closedOpen("a", "b"), null),
+
+          // [c,n] (a,c) -> empty
+          Arguments.of(fenceClosedCN, RowRange.open("a", "c"), null),
+          // [c,n] (a,c] -> (c,c)
+          Arguments.of(fenceClosedCN, RowRange.openClosed("a", "c"), 
RowRange.closed("c")),
+          // [c,n] (n,r) -> (n,n)
+          Arguments.of(fenceClosedCN, RowRange.open("n", "r"), null),
+          // [c,n] [n,r) -> (n,n)
+          Arguments.of(fenceClosedCN, RowRange.closedOpen("n", "r"), 
RowRange.closed("n")),
+          // [c,n] (q,r) -> empty
+          Arguments.of(fenceClosedCN, RowRange.open("q", "r"), null),
+          // [c,n] [q,r) -> empty
+          Arguments.of(fenceClosedCN, RowRange.closedOpen("q", "r"), null),
+
+          // [b] (b,c) -> empty
+          Arguments.of(fenceClosedB, RowRange.open("b", "c"), null),
+          // [b] [b,c) -> [b]
+          Arguments.of(fenceClosedB, RowRange.closedOpen("b", "c"), 
RowRange.closed("b")),
+          // [b] (a,b) -> empty
+          Arguments.of(fenceClosedB, RowRange.open("a", "b"), null),
+          // [b] (a,b] -> [b]
+          Arguments.of(fenceClosedB, RowRange.openClosed("a", "b"), 
RowRange.closed("b")));
+    }
+
+    @Test
+    public void clipReturnsNull() {
+      RowRange range = RowRange.open("a", "b");
+      RowRange afterRange = RowRange.closed("c");
+      RowRange beforeRange = RowRange.closed("a");
+
+      RowRange clipped = range.clip(afterRange, true);
+      assertNull(clipped);
+
+      clipped = range.clip(beforeRange, true);
+      assertNull(clipped);
+    }
+  }
+
+}
diff --git 
a/server/base/src/main/java/org/apache/accumulo/server/compaction/CompactionPluginUtils.java
 
b/server/base/src/main/java/org/apache/accumulo/server/compaction/CompactionPluginUtils.java
index c62a7bcc00..854f096ebd 100644
--- 
a/server/base/src/main/java/org/apache/accumulo/server/compaction/CompactionPluginUtils.java
+++ 
b/server/base/src/main/java/org/apache/accumulo/server/compaction/CompactionPluginUtils.java
@@ -47,6 +47,7 @@ import org.apache.accumulo.core.conf.AccumuloConfiguration;
 import org.apache.accumulo.core.conf.ConfigurationTypeHelper;
 import org.apache.accumulo.core.conf.Property;
 import org.apache.accumulo.core.data.Key;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.data.TableId;
 import org.apache.accumulo.core.data.TabletId;
 import org.apache.accumulo.core.data.Value;
@@ -61,7 +62,6 @@ import org.apache.accumulo.core.metadata.schema.DataFileValue;
 import org.apache.accumulo.core.sample.impl.SamplerConfigurationImpl;
 import org.apache.accumulo.core.spi.common.ServiceEnvironment;
 import org.apache.accumulo.core.spi.compaction.CompactionDispatcher;
-import org.apache.accumulo.core.summary.Gatherer;
 import org.apache.accumulo.core.summary.SummarizerFactory;
 import org.apache.accumulo.core.summary.SummaryCollection;
 import org.apache.accumulo.core.summary.SummaryReader;
@@ -161,7 +161,8 @@ public class CompactionPluginUtils {
                 SummaryCollection fsc = SummaryReader
                     .load(conf, source, file.getFileName(), summarySelector, 
factory,
                         tableConf.getCryptoService())
-                    .getSummaries(Collections.singletonList(new 
Gatherer.RowRange(extent)));
+                    .getSummaries(Collections.singletonList(
+                        RowRange.range(extent.prevEndRow(), false, 
extent.endRow(), true)));
 
                 sc.merge(fsc, factory);
               }
diff --git 
a/shell/src/main/java/org/apache/accumulo/shell/commands/MaxRowCommand.java 
b/shell/src/main/java/org/apache/accumulo/shell/commands/MaxRowCommand.java
index cd967f445d..dca9aa7969 100644
--- a/shell/src/main/java/org/apache/accumulo/shell/commands/MaxRowCommand.java
+++ b/shell/src/main/java/org/apache/accumulo/shell/commands/MaxRowCommand.java
@@ -19,6 +19,7 @@
 package org.apache.accumulo.shell.commands;
 
 import org.apache.accumulo.core.data.Range;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.security.Authorizations;
 import org.apache.accumulo.shell.Shell;
 import org.apache.commons.cli.CommandLine;
@@ -33,12 +34,14 @@ public class MaxRowCommand extends ScanCommand {
 
     final Range range = getRange(cl);
     final Authorizations auths = getAuths(cl, shellState);
-    final Text startRow = range.getStartKey() == null ? null : 
range.getStartKey().getRow();
-    final Text endRow = range.getEndKey() == null ? null : 
range.getEndKey().getRow();
+    final Text lowerBound = range.getStartKey() == null ? null : 
range.getStartKey().getRow();
+    final Text upperBound = range.getEndKey() == null ? null : 
range.getEndKey().getRow();
+    final RowRange rowRange = RowRange.range(lowerBound, 
range.isStartKeyInclusive(), upperBound,
+        range.isEndKeyInclusive());
 
     try {
-      final Text max = 
shellState.getAccumuloClient().tableOperations().getMaxRow(tableName, auths,
-          startRow, range.isStartKeyInclusive(), endRow, 
range.isEndKeyInclusive());
+      final Text max =
+          
shellState.getAccumuloClient().tableOperations().getMaxRow(tableName, auths, 
rowRange);
       if (max != null) {
         shellState.getWriter().println(max);
       }
diff --git 
a/test/src/main/java/org/apache/accumulo/test/ComprehensiveITBase.java 
b/test/src/main/java/org/apache/accumulo/test/ComprehensiveITBase.java
index 02b8bb253b..df1fdf03b9 100644
--- a/test/src/main/java/org/apache/accumulo/test/ComprehensiveITBase.java
+++ b/test/src/main/java/org/apache/accumulo/test/ComprehensiveITBase.java
@@ -86,6 +86,7 @@ import org.apache.accumulo.core.data.ConditionalMutation;
 import org.apache.accumulo.core.data.Key;
 import org.apache.accumulo.core.data.Mutation;
 import org.apache.accumulo.core.data.Range;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.data.Value;
 import org.apache.accumulo.core.data.constraints.Constraint;
 import org.apache.accumulo.core.data.constraints.DefaultKeySizeConstraint;
@@ -656,7 +657,7 @@ public abstract class ComprehensiveITBase extends 
SharedMiniClusterBase {
             
.stream().flatMap(Set::stream).collect(MoreCollectors.onlyElement()));
         // ensure no new data was written
         assertEquals(new Text(row(99)), 
client.tableOperations().getMaxRow(rootsTable,
-            AUTHORIZATIONS, new Text(row(98)), true, new Text(row(110)), 
true));
+            AUTHORIZATIONS, RowRange.closed(new Text(row(98)), new 
Text(row(110)))));
 
         client.securityOperations().grantTablePermission("user1", rootsTable,
             TablePermission.WRITE);
diff --git 
a/test/src/main/java/org/apache/accumulo/test/ComprehensiveTableOperationsIT_SimpleSuite.java
 
b/test/src/main/java/org/apache/accumulo/test/ComprehensiveTableOperationsIT_SimpleSuite.java
index 8605352c8b..bb929ad151 100644
--- 
a/test/src/main/java/org/apache/accumulo/test/ComprehensiveTableOperationsIT_SimpleSuite.java
+++ 
b/test/src/main/java/org/apache/accumulo/test/ComprehensiveTableOperationsIT_SimpleSuite.java
@@ -59,6 +59,7 @@ import 
org.apache.accumulo.core.clientImpl.TabletMergeabilityUtil;
 import org.apache.accumulo.core.conf.Property;
 import org.apache.accumulo.core.data.Key;
 import org.apache.accumulo.core.data.Range;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.data.TableId;
 import org.apache.accumulo.core.data.Value;
 import org.apache.accumulo.core.iterators.Filter;
@@ -325,7 +326,7 @@ public class ComprehensiveTableOperationsIT_SimpleSuite 
extends SharedMiniCluste
     createFateTableRow(userTable);
     createScanRefTableRow();
     for (var sysTable : SystemTables.tableNames()) {
-      var maxRow = ops.getMaxRow(sysTable, Authorizations.EMPTY, null, true, 
null, true);
+      var maxRow = ops.getMaxRow(sysTable, Authorizations.EMPTY, 
RowRange.all());
       log.info("Max row of {} : {}", sysTable, maxRow);
       assertNotNull(maxRow);
     }
diff --git a/test/src/main/java/org/apache/accumulo/test/FindMaxIT.java 
b/test/src/main/java/org/apache/accumulo/test/FindMaxIT.java
index e6eacdd0c0..c71bd4b308 100644
--- a/test/src/main/java/org/apache/accumulo/test/FindMaxIT.java
+++ b/test/src/main/java/org/apache/accumulo/test/FindMaxIT.java
@@ -30,6 +30,7 @@ import org.apache.accumulo.core.client.BatchWriter;
 import org.apache.accumulo.core.client.Scanner;
 import org.apache.accumulo.core.data.Key;
 import org.apache.accumulo.core.data.Mutation;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.data.Value;
 import org.apache.accumulo.core.security.Authorizations;
 import org.apache.accumulo.harness.AccumuloClusterHarness;
@@ -50,6 +51,7 @@ public class FindMaxIT extends AccumuloClusterHarness {
     return m;
   }
 
+  @SuppressWarnings("deprecation")
   @Test
   public void test1() throws Exception {
     try (AccumuloClient client = 
Accumulo.newClient().from(getClientProps()).build()) {
@@ -90,10 +92,6 @@ public class FindMaxIT extends AccumuloClusterHarness {
               true, rows.get(i), false);
           assertEquals(rows.get(i - 1), max);
 
-          max = client.tableOperations().getMaxRow(tableName, 
Authorizations.EMPTY, rows.get(i - 1),
-              false, rows.get(i), false);
-          assertNull(max);
-
           max = client.tableOperations().getMaxRow(tableName, 
Authorizations.EMPTY, null, true,
               rows.get(i), true);
           assertEquals(rows.get(i), max);
@@ -122,4 +120,74 @@ public class FindMaxIT extends AccumuloClusterHarness {
       }
     }
   }
+
+  @Test
+  public void testRowRange() throws Exception {
+    try (AccumuloClient client = 
Accumulo.newClient().from(getClientProps()).build()) {
+      String tableName = getUniqueNames(1)[0];
+
+      client.tableOperations().create(tableName);
+
+      try (BatchWriter bw = client.createBatchWriter(tableName)) {
+        bw.addMutation(nm(new byte[] {0}));
+        bw.addMutation(nm(new byte[] {0, 0}));
+        bw.addMutation(nm(new byte[] {0, 1}));
+        bw.addMutation(nm(new byte[] {0, 1, 0}));
+        bw.addMutation(nm(new byte[] {1, 0}));
+        bw.addMutation(nm(new byte[] {'a', 'b', 'c'}));
+        bw.addMutation(nm(new byte[] {(byte) 0xff}));
+        bw.addMutation(nm(new byte[] {(byte) 0xff, (byte) 0xff, (byte) 0xff, 
(byte) 0xff,
+            (byte) 0xff, (byte) 0xff}));
+
+        for (int i = 0; i < 1000; i += 5) {
+          bw.addMutation(nm(String.format("r%05d", i)));
+        }
+      }
+
+      try (Scanner scanner = client.createScanner(tableName, 
Authorizations.EMPTY)) {
+
+        ArrayList<Text> rows = new ArrayList<>();
+
+        for (Entry<Key,Value> entry : scanner) {
+          rows.add(entry.getKey().getRow());
+        }
+
+        for (int i = rows.size() - 1; i > 0; i--) {
+          RowRange range = RowRange.closedOpen(rows.get(i - 1), rows.get(i));
+          Text max = client.tableOperations().getMaxRow(tableName, 
Authorizations.EMPTY, range);
+          assertEquals(rows.get(i - 1), max);
+
+          range = RowRange.closed(rows.get(i - 1), rows.get(i));
+          max = client.tableOperations().getMaxRow(tableName, 
Authorizations.EMPTY, range);
+          assertEquals(rows.get(i), max);
+
+          range = RowRange.atMost(rows.get(i));
+          max = client.tableOperations().getMaxRow(tableName, 
Authorizations.EMPTY, range);
+          assertEquals(rows.get(i), max);
+
+          range = RowRange.closed(rows.get(i));
+          max = client.tableOperations().getMaxRow(tableName, 
Authorizations.EMPTY, range);
+          assertEquals(rows.get(i), max);
+
+          range = RowRange.openClosed(rows.get(i - 1), rows.get(i));
+          max = client.tableOperations().getMaxRow(tableName, 
Authorizations.EMPTY, range);
+          assertEquals(rows.get(i), max);
+
+        }
+
+        RowRange range = RowRange.all();
+        Text max = client.tableOperations().getMaxRow(tableName, 
Authorizations.EMPTY, range);
+        assertEquals(rows.get(rows.size() - 1), max);
+
+        range = RowRange.lessThan(new Text(new byte[] {0}));
+        max = client.tableOperations().getMaxRow(tableName, 
Authorizations.EMPTY, range);
+        assertNull(max);
+
+        range = RowRange.atMost(new Text(new byte[] {0}));
+        max = client.tableOperations().getMaxRow(tableName, 
Authorizations.EMPTY, range);
+        assertEquals(rows.get(0), max);
+      }
+    }
+
+  }
 }
diff --git 
a/test/src/main/java/org/apache/accumulo/test/NamespacesIT_SimpleSuite.java 
b/test/src/main/java/org/apache/accumulo/test/NamespacesIT_SimpleSuite.java
index 6dbc831195..2b04ea8839 100644
--- a/test/src/main/java/org/apache/accumulo/test/NamespacesIT_SimpleSuite.java
+++ b/test/src/main/java/org/apache/accumulo/test/NamespacesIT_SimpleSuite.java
@@ -68,6 +68,7 @@ import org.apache.accumulo.core.conf.Property;
 import org.apache.accumulo.core.data.Key;
 import org.apache.accumulo.core.data.Mutation;
 import org.apache.accumulo.core.data.Range;
+import org.apache.accumulo.core.data.RowRange;
 import org.apache.accumulo.core.data.Value;
 import org.apache.accumulo.core.iterators.Filter;
 import org.apache.accumulo.core.iterators.IteratorUtil.IteratorScope;
@@ -1044,7 +1045,7 @@ public class NamespacesIT_SimpleSuite extends 
SharedMiniClusterBase {
     assertNoTableNoNamespace(() -> ops.getIteratorSetting(tableName, "a", 
IteratorScope.scan));
     assertNoTableNoNamespace(() -> ops.getLocalityGroups(tableName));
     assertNoTableNoNamespace(
-        () -> ops.getMaxRow(tableName, Authorizations.EMPTY, a, true, z, 
true));
+        () -> ops.getMaxRow(tableName, Authorizations.EMPTY, 
RowRange.closed(a, z)));
     assertNoTableNoNamespace(() -> ops.getConfiguration(tableName));
     assertNoTableNoNamespace(() -> 
ops.importDirectory("").to(tableName).load());
     assertNoTableNoNamespace(() -> ops.testClassLoad(tableName, 
VersioningIterator.class.getName(),

Reply via email to