You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@accumulo.apache.org by "keith-turner (via GitHub)" <gi...@apache.org> on 2023/05/03 19:37:51 UTC

[GitHub] [accumulo] keith-turner commented on a diff in pull request #3342: WIP - Add RowRange object

keith-turner commented on code in PR #3342:
URL: https://github.com/apache/accumulo/pull/3342#discussion_r1184133395


##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {

Review Comment:
   This method was not covered by unit test.



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");

Review Comment:
   This method was not covered by unit test.



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.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; set to null to cover all rows
+   */
+  public static RowRange closed(CharSequence row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(Text startRow, boolean startInclusive, Text endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow, startInclusive, endRow, endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(CharSequence startRow, boolean startInclusive, CharSequence endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow == null ? null : new Text(startRow.toString()), startInclusive,
+        endRow == null ? null : new Text(endRow.toString()), endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  private RowRange(Text startRow, boolean startInclusive, Text endRow, boolean endInclusive) {
+    this.startRow = startRow;
+    this.startRowInclusive = startInclusive;
+    this.infiniteStartRow = startRow == null;
+    this.endRow = endRow;
+    this.endRowInclusive = endInclusive;
+    this.infiniteEndRow = endRow == null;
+
+    if (!infiniteStartRow && !infiniteEndRow && beforeStartRowImpl(endRow)) {
+      throw new IllegalArgumentException(
+          "Start row must be less than end row in row range (" + startRow + ", " + endRow + ")");
+    }
+  }
+
+  public Text getStartRow() {
+    return startRow;
+  }
+
+  public boolean isStartRowInclusive() {
+    return startRowInclusive;
+  }
+
+  public Text getEndRow() {
+    return endRow;
+  }
+
+  public boolean isEndRowInclusive() {
+    return endRowInclusive;
+  }
+
+  /**
+   * Converts this row range to a {@link Range} object.
+   */
+  public Range toRange() {
+    return new Range(startRow, startRowInclusive, endRow, endRowInclusive);
+  }
+
+  /**
+   * Determines if the given row is before the start row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is before the row range, otherwise false
+   */
+  public boolean beforeStartRow(Text row) {
+    return beforeStartRowImpl(row);
+  }
+
+  /**
+   * Determines if the given row is after the end row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is after the row range, otherwise false
+   */
+  public boolean afterEndRow(Text row) {
+    if (infiniteEndRow) {
+      return false;
+    }
+
+    if (endRowInclusive) {
+      return row.compareTo(endRow) > 0;
+    }
+    return row.compareTo(endRow) >= 0;
+  }
+
+  /**
+   * Implements logic of {@link #beforeStartRow(Text)}, but in a private method, so that it can be
+   * safely used by constructors if a subclass overrides that {@link #beforeStartRow(Text)}
+   */
+  private boolean beforeStartRowImpl(Text row) {
+    if (this.infiniteStartRow) {
+      return false;
+    }
+
+    if (startRowInclusive) {
+      return row.compareTo(startRow) < 0;
+    }
+    return row.compareTo(startRow) <= 0;
+  }
+
+  @Override
+  public String toString() {
+    final String startRangeSymbol = (startRowInclusive && startRow != null) ? "[" : "(";
+    final String startRow = this.startRow == null ? "-inf" : this.startRow.toString();
+    final String endRow = this.endRow == null ? "+inf" : this.endRow.toString();
+    final String endRangeSymbol = (endRowInclusive && this.endRow != null) ? "]" : ")";
+    return startRangeSymbol + startRow + "," + endRow + endRangeSymbol;
+  }
+
+  @Override
+  public int hashCode() {
+    int startHash = infiniteStartRow ? 0 : startRow.hashCode() + (startRowInclusive ? 1 : 0);
+    int stopHash = infiniteEndRow ? 0 : endRow.hashCode() + (endRowInclusive ? 1 : 0);
+
+    return startHash + stopHash;
+  }
+
+  @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 equals, false otherwise
+   */
+  public boolean equals(RowRange other) {
+    return compareTo(other) == 0;
+  }
+
+  /**
+   * Compares this row range to another row range. Compares in order: start row, inclusiveness of
+   * start row, end row, inclusiveness of end row. Infinite rows sort first, and non-infinite rows
+   * are compared with {@link Text#compareTo(BinaryComparable)}. Inclusive sorts before
+   * non-inclusive.
+   *
+   * @param other range row to compare
+   * @return comparison result
+   */
+  @Override
+  public int compareTo(RowRange other) {
+    // Compare infinite start rows
+    int comp = Boolean.compare(this.infiniteStartRow, other.infiniteStartRow);
+
+    if (comp == 0) {
+      // Compare non-infinite start rows and start row inclusiveness
+      if (!this.infiniteStartRow) {
+        if (this.startRow == null && other.startRow == null) {
+          comp = 0;
+        } else if (this.startRow == null) {
+          comp = -1;
+        } else if (other.startRow == null) {
+          comp = 1;
+        } else {
+          comp = this.startRow.compareTo(other.startRow);
+        }
+        if (comp == 0) {
+          comp = Boolean.compare(other.startRowInclusive, this.startRowInclusive);
+        }
+      }
+    }
+
+    if (comp == 0) {
+      // Compare infinite end rows
+      comp = Boolean.compare(this.infiniteEndRow, other.infiniteEndRow);
+
+      // Compare non-infinite end rows and end row inclusiveness
+      if (comp == 0 && !this.infiniteEndRow) {
+        if (this.endRow == null && other.endRow == null) {
+          comp = 0;
+        } else if (this.endRow == null) {
+          comp = 1;
+        } else if (other.endRow == null) {
+          comp = -1;
+        } else {
+          comp = this.endRow.compareTo(other.endRow);
+        }
+        if (comp == 0) {
+          comp = Boolean.compare(this.endRowInclusive, other.endRowInclusive);
+        }
+      }
+    }
+
+    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, false otherwise
+   */
+  public boolean contains(Text row) {
+    if (infiniteStartRow) {
+      return !afterEndRow(row);
+    } else if (infiniteEndRow) {
+      return !beforeStartRow(row);
+    } else {
+      return !beforeStartRow(row) && !afterEndRow(row);
+    }
+  }
+
+  /**
+   * Merges overlapping and adjacent row ranges. 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 startRow values
+    sortedRowRanges.sort((r1, r2) -> {
+      if (r1.startRow == null && r2.startRow == null) {

Review Comment:
   Maybe could use the START_ROW_COMPARATOR suggested in another comment here.



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.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; set to null to cover all rows
+   */
+  public static RowRange closed(CharSequence row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(Text startRow, boolean startInclusive, Text endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow, startInclusive, endRow, endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(CharSequence startRow, boolean startInclusive, CharSequence endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow == null ? null : new Text(startRow.toString()), startInclusive,
+        endRow == null ? null : new Text(endRow.toString()), endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  private RowRange(Text startRow, boolean startInclusive, Text endRow, boolean endInclusive) {
+    this.startRow = startRow;
+    this.startRowInclusive = startInclusive;
+    this.infiniteStartRow = startRow == null;
+    this.endRow = endRow;
+    this.endRowInclusive = endInclusive;
+    this.infiniteEndRow = endRow == null;
+
+    if (!infiniteStartRow && !infiniteEndRow && beforeStartRowImpl(endRow)) {
+      throw new IllegalArgumentException(
+          "Start row must be less than end row in row range (" + startRow + ", " + endRow + ")");
+    }
+  }
+
+  public Text getStartRow() {
+    return startRow;
+  }
+
+  public boolean isStartRowInclusive() {
+    return startRowInclusive;
+  }
+
+  public Text getEndRow() {
+    return endRow;
+  }
+
+  public boolean isEndRowInclusive() {
+    return endRowInclusive;
+  }
+
+  /**
+   * Converts this row range to a {@link Range} object.
+   */
+  public Range toRange() {
+    return new Range(startRow, startRowInclusive, endRow, endRowInclusive);
+  }
+
+  /**
+   * Determines if the given row is before the start row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is before the row range, otherwise false
+   */
+  public boolean beforeStartRow(Text row) {
+    return beforeStartRowImpl(row);
+  }
+
+  /**
+   * Determines if the given row is after the end row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is after the row range, otherwise false
+   */
+  public boolean afterEndRow(Text row) {
+    if (infiniteEndRow) {
+      return false;
+    }
+
+    if (endRowInclusive) {
+      return row.compareTo(endRow) > 0;
+    }
+    return row.compareTo(endRow) >= 0;
+  }
+
+  /**
+   * Implements logic of {@link #beforeStartRow(Text)}, but in a private method, so that it can be
+   * safely used by constructors if a subclass overrides that {@link #beforeStartRow(Text)}
+   */
+  private boolean beforeStartRowImpl(Text row) {
+    if (this.infiniteStartRow) {
+      return false;
+    }
+
+    if (startRowInclusive) {
+      return row.compareTo(startRow) < 0;
+    }
+    return row.compareTo(startRow) <= 0;
+  }
+
+  @Override
+  public String toString() {
+    final String startRangeSymbol = (startRowInclusive && startRow != null) ? "[" : "(";
+    final String startRow = this.startRow == null ? "-inf" : this.startRow.toString();
+    final String endRow = this.endRow == null ? "+inf" : this.endRow.toString();
+    final String endRangeSymbol = (endRowInclusive && this.endRow != null) ? "]" : ")";
+    return startRangeSymbol + startRow + "," + endRow + endRangeSymbol;
+  }
+
+  @Override
+  public int hashCode() {
+    int startHash = infiniteStartRow ? 0 : startRow.hashCode() + (startRowInclusive ? 1 : 0);
+    int stopHash = infiniteEndRow ? 0 : endRow.hashCode() + (endRowInclusive ? 1 : 0);
+
+    return startHash + stopHash;
+  }
+
+  @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 equals, false otherwise
+   */
+  public boolean equals(RowRange other) {
+    return compareTo(other) == 0;
+  }
+
+  /**
+   * Compares this row range to another row range. Compares in order: start row, inclusiveness of
+   * start row, end row, inclusiveness of end row. Infinite rows sort first, and non-infinite rows
+   * are compared with {@link Text#compareTo(BinaryComparable)}. Inclusive sorts before
+   * non-inclusive.
+   *
+   * @param other range row to compare
+   * @return comparison result
+   */
+  @Override
+  public int compareTo(RowRange other) {
+    // Compare infinite start rows
+    int comp = Boolean.compare(this.infiniteStartRow, other.infiniteStartRow);
+
+    if (comp == 0) {
+      // Compare non-infinite start rows and start row inclusiveness
+      if (!this.infiniteStartRow) {
+        if (this.startRow == null && other.startRow == null) {
+          comp = 0;
+        } else if (this.startRow == null) {
+          comp = -1;
+        } else if (other.startRow == null) {
+          comp = 1;
+        } else {
+          comp = this.startRow.compareTo(other.startRow);
+        }
+        if (comp == 0) {
+          comp = Boolean.compare(other.startRowInclusive, this.startRowInclusive);
+        }
+      }
+    }
+
+    if (comp == 0) {
+      // Compare infinite end rows
+      comp = Boolean.compare(this.infiniteEndRow, other.infiniteEndRow);
+
+      // Compare non-infinite end rows and end row inclusiveness
+      if (comp == 0 && !this.infiniteEndRow) {
+        if (this.endRow == null && other.endRow == null) {
+          comp = 0;
+        } else if (this.endRow == null) {
+          comp = 1;
+        } else if (other.endRow == null) {
+          comp = -1;
+        } else {
+          comp = this.endRow.compareTo(other.endRow);
+        }
+        if (comp == 0) {
+          comp = Boolean.compare(this.endRowInclusive, other.endRowInclusive);
+        }
+      }
+    }

Review Comment:
   With the following
   
   ```java
     // create once and reused to avoid object allocation on each compare call
     private static final Comparator<Text> START_ROW_COMPARATOR = Comparator.nullsFirst(Text::compareTo);
     private static final Comparator<Text> END_ROW_COMPARATOR = Comparator.nullsLast(Text::compareTo);
   ```
   
   could shorten comparison function
   
   ```suggestion
         if (!this.infiniteStartRow) {
          comp = START_ROW_COMPARATOR.compare(this.startRow, other.startRow);
           if (comp == 0) {
             comp = Boolean.compare(other.startRowInclusive, this.startRowInclusive);
           }
         }
       }
   
       if (comp == 0) {
         // Compare infinite end rows
         comp = Boolean.compare(this.infiniteEndRow, other.infiniteEndRow);
   
         // Compare non-infinite end rows and end row inclusiveness
         if (comp == 0 && !this.infiniteEndRow) {
          comp = END_ROW_COMPARATOR.compare(this.endRow, other.endRow);
          
           if (comp == 0) {
             comp = Boolean.compare(this.endRowInclusive, other.endRowInclusive);
           }
         }
       }
   ```
   



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);

Review Comment:
   This method is not covered by unit test.



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.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; set to null to cover all rows
+   */
+  public static RowRange closed(CharSequence row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(Text startRow, boolean startInclusive, Text endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow, startInclusive, endRow, endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(CharSequence startRow, boolean startInclusive, CharSequence endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow == null ? null : new Text(startRow.toString()), startInclusive,
+        endRow == null ? null : new Text(endRow.toString()), endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  private RowRange(Text startRow, boolean startInclusive, Text endRow, boolean endInclusive) {
+    this.startRow = startRow;
+    this.startRowInclusive = startInclusive;
+    this.infiniteStartRow = startRow == null;
+    this.endRow = endRow;
+    this.endRowInclusive = endInclusive;
+    this.infiniteEndRow = endRow == null;
+
+    if (!infiniteStartRow && !infiniteEndRow && beforeStartRowImpl(endRow)) {
+      throw new IllegalArgumentException(
+          "Start row must be less than end row in row range (" + startRow + ", " + endRow + ")");
+    }
+  }
+
+  public Text getStartRow() {
+    return startRow;
+  }
+
+  public boolean isStartRowInclusive() {
+    return startRowInclusive;
+  }
+
+  public Text getEndRow() {
+    return endRow;
+  }
+
+  public boolean isEndRowInclusive() {
+    return endRowInclusive;
+  }
+
+  /**
+   * Converts this row range to a {@link Range} object.
+   */
+  public Range toRange() {
+    return new Range(startRow, startRowInclusive, endRow, endRowInclusive);
+  }
+
+  /**
+   * Determines if the given row is before the start row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is before the row range, otherwise false
+   */
+  public boolean beforeStartRow(Text row) {
+    return beforeStartRowImpl(row);
+  }
+
+  /**
+   * Determines if the given row is after the end row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is after the row range, otherwise false
+   */
+  public boolean afterEndRow(Text row) {
+    if (infiniteEndRow) {
+      return false;
+    }
+
+    if (endRowInclusive) {
+      return row.compareTo(endRow) > 0;
+    }
+    return row.compareTo(endRow) >= 0;
+  }
+
+  /**
+   * Implements logic of {@link #beforeStartRow(Text)}, but in a private method, so that it can be
+   * safely used by constructors if a subclass overrides that {@link #beforeStartRow(Text)}
+   */
+  private boolean beforeStartRowImpl(Text row) {
+    if (this.infiniteStartRow) {
+      return false;
+    }
+
+    if (startRowInclusive) {
+      return row.compareTo(startRow) < 0;
+    }
+    return row.compareTo(startRow) <= 0;
+  }
+
+  @Override
+  public String toString() {
+    final String startRangeSymbol = (startRowInclusive && startRow != null) ? "[" : "(";
+    final String startRow = this.startRow == null ? "-inf" : this.startRow.toString();
+    final String endRow = this.endRow == null ? "+inf" : this.endRow.toString();
+    final String endRangeSymbol = (endRowInclusive && this.endRow != null) ? "]" : ")";
+    return startRangeSymbol + startRow + "," + endRow + endRangeSymbol;
+  }
+
+  @Override
+  public int hashCode() {
+    int startHash = infiniteStartRow ? 0 : startRow.hashCode() + (startRowInclusive ? 1 : 0);
+    int stopHash = infiniteEndRow ? 0 : endRow.hashCode() + (endRowInclusive ? 1 : 0);
+
+    return startHash + stopHash;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (!(other instanceof RowRange)) {
+      return false;

Review Comment:
   I was curious about the code covered on the clip and merge overlapping method and I tried running the unit test with code coverage locally.  The class had really good coverage. Only a few places were not covered.  This line was one.  I commented on the other lines I saw that were not covered.



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.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; set to null to cover all rows
+   */
+  public static RowRange closed(CharSequence row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(Text startRow, boolean startInclusive, Text endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow, startInclusive, endRow, endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(CharSequence startRow, boolean startInclusive, CharSequence endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow == null ? null : new Text(startRow.toString()), startInclusive,
+        endRow == null ? null : new Text(endRow.toString()), endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  private RowRange(Text startRow, boolean startInclusive, Text endRow, boolean endInclusive) {
+    this.startRow = startRow;
+    this.startRowInclusive = startInclusive;
+    this.infiniteStartRow = startRow == null;
+    this.endRow = endRow;
+    this.endRowInclusive = endInclusive;
+    this.infiniteEndRow = endRow == null;
+
+    if (!infiniteStartRow && !infiniteEndRow && beforeStartRowImpl(endRow)) {
+      throw new IllegalArgumentException(
+          "Start row must be less than end row in row range (" + startRow + ", " + endRow + ")");
+    }
+  }
+
+  public Text getStartRow() {
+    return startRow;
+  }
+
+  public boolean isStartRowInclusive() {
+    return startRowInclusive;
+  }
+
+  public Text getEndRow() {
+    return endRow;
+  }
+
+  public boolean isEndRowInclusive() {
+    return endRowInclusive;
+  }
+
+  /**
+   * Converts this row range to a {@link Range} object.
+   */
+  public Range toRange() {
+    return new Range(startRow, startRowInclusive, endRow, endRowInclusive);
+  }

Review Comment:
   These method were not covered.



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.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; set to null to cover all rows
+   */
+  public static RowRange closed(CharSequence row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(Text startRow, boolean startInclusive, Text endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow, startInclusive, endRow, endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(CharSequence startRow, boolean startInclusive, CharSequence endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow == null ? null : new Text(startRow.toString()), startInclusive,
+        endRow == null ? null : new Text(endRow.toString()), endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  private RowRange(Text startRow, boolean startInclusive, Text endRow, boolean endInclusive) {
+    this.startRow = startRow;
+    this.startRowInclusive = startInclusive;
+    this.infiniteStartRow = startRow == null;
+    this.endRow = endRow;
+    this.endRowInclusive = endInclusive;
+    this.infiniteEndRow = endRow == null;
+
+    if (!infiniteStartRow && !infiniteEndRow && beforeStartRowImpl(endRow)) {
+      throw new IllegalArgumentException(
+          "Start row must be less than end row in row range (" + startRow + ", " + endRow + ")");
+    }
+  }
+
+  public Text getStartRow() {
+    return startRow;
+  }
+
+  public boolean isStartRowInclusive() {
+    return startRowInclusive;
+  }
+
+  public Text getEndRow() {
+    return endRow;
+  }
+
+  public boolean isEndRowInclusive() {
+    return endRowInclusive;
+  }
+
+  /**
+   * Converts this row range to a {@link Range} object.
+   */
+  public Range toRange() {
+    return new Range(startRow, startRowInclusive, endRow, endRowInclusive);
+  }
+
+  /**
+   * Determines if the given row is before the start row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is before the row range, otherwise false
+   */
+  public boolean beforeStartRow(Text row) {
+    return beforeStartRowImpl(row);
+  }
+
+  /**
+   * Determines if the given row is after the end row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is after the row range, otherwise false
+   */
+  public boolean afterEndRow(Text row) {
+    if (infiniteEndRow) {
+      return false;
+    }
+
+    if (endRowInclusive) {
+      return row.compareTo(endRow) > 0;
+    }
+    return row.compareTo(endRow) >= 0;
+  }
+
+  /**
+   * Implements logic of {@link #beforeStartRow(Text)}, but in a private method, so that it can be
+   * safely used by constructors if a subclass overrides that {@link #beforeStartRow(Text)}
+   */
+  private boolean beforeStartRowImpl(Text row) {
+    if (this.infiniteStartRow) {
+      return false;

Review Comment:
   This was not covered



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.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; set to null to cover all rows
+   */
+  public static RowRange closed(CharSequence row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(Text startRow, boolean startInclusive, Text endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow, startInclusive, endRow, endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(CharSequence startRow, boolean startInclusive, CharSequence endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow == null ? null : new Text(startRow.toString()), startInclusive,
+        endRow == null ? null : new Text(endRow.toString()), endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  private RowRange(Text startRow, boolean startInclusive, Text endRow, boolean endInclusive) {
+    this.startRow = startRow;
+    this.startRowInclusive = startInclusive;
+    this.infiniteStartRow = startRow == null;
+    this.endRow = endRow;
+    this.endRowInclusive = endInclusive;
+    this.infiniteEndRow = endRow == null;
+
+    if (!infiniteStartRow && !infiniteEndRow && beforeStartRowImpl(endRow)) {
+      throw new IllegalArgumentException(

Review Comment:
   This was not covered by unit test



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.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; set to null to cover all rows
+   */
+  public static RowRange closed(CharSequence row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(Text startRow, boolean startInclusive, Text endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow, startInclusive, endRow, endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(CharSequence startRow, boolean startInclusive, CharSequence endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow == null ? null : new Text(startRow.toString()), startInclusive,
+        endRow == null ? null : new Text(endRow.toString()), endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  private RowRange(Text startRow, boolean startInclusive, Text endRow, boolean endInclusive) {
+    this.startRow = startRow;
+    this.startRowInclusive = startInclusive;
+    this.infiniteStartRow = startRow == null;
+    this.endRow = endRow;
+    this.endRowInclusive = endInclusive;
+    this.infiniteEndRow = endRow == null;
+
+    if (!infiniteStartRow && !infiniteEndRow && beforeStartRowImpl(endRow)) {
+      throw new IllegalArgumentException(
+          "Start row must be less than end row in row range (" + startRow + ", " + endRow + ")");
+    }
+  }
+
+  public Text getStartRow() {
+    return startRow;
+  }
+
+  public boolean isStartRowInclusive() {
+    return startRowInclusive;
+  }
+
+  public Text getEndRow() {
+    return endRow;
+  }
+
+  public boolean isEndRowInclusive() {
+    return endRowInclusive;
+  }
+
+  /**
+   * Converts this row range to a {@link Range} object.
+   */
+  public Range toRange() {
+    return new Range(startRow, startRowInclusive, endRow, endRowInclusive);
+  }
+
+  /**
+   * Determines if the given row is before the start row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is before the row range, otherwise false
+   */
+  public boolean beforeStartRow(Text row) {
+    return beforeStartRowImpl(row);
+  }
+
+  /**
+   * Determines if the given row is after the end row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is after the row range, otherwise false
+   */
+  public boolean afterEndRow(Text row) {
+    if (infiniteEndRow) {
+      return false;
+    }
+
+    if (endRowInclusive) {
+      return row.compareTo(endRow) > 0;
+    }
+    return row.compareTo(endRow) >= 0;
+  }
+
+  /**
+   * Implements logic of {@link #beforeStartRow(Text)}, but in a private method, so that it can be
+   * safely used by constructors if a subclass overrides that {@link #beforeStartRow(Text)}
+   */
+  private boolean beforeStartRowImpl(Text row) {
+    if (this.infiniteStartRow) {
+      return false;
+    }
+
+    if (startRowInclusive) {
+      return row.compareTo(startRow) < 0;
+    }
+    return row.compareTo(startRow) <= 0;
+  }
+
+  @Override
+  public String toString() {
+    final String startRangeSymbol = (startRowInclusive && startRow != null) ? "[" : "(";
+    final String startRow = this.startRow == null ? "-inf" : this.startRow.toString();
+    final String endRow = this.endRow == null ? "+inf" : this.endRow.toString();
+    final String endRangeSymbol = (endRowInclusive && this.endRow != null) ? "]" : ")";
+    return startRangeSymbol + startRow + "," + endRow + endRangeSymbol;
+  }
+
+  @Override
+  public int hashCode() {
+    int startHash = infiniteStartRow ? 0 : startRow.hashCode() + (startRowInclusive ? 1 : 0);
+    int stopHash = infiniteEndRow ? 0 : endRow.hashCode() + (endRowInclusive ? 1 : 0);
+
+    return startHash + stopHash;
+  }
+
+  @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 equals, false otherwise
+   */
+  public boolean equals(RowRange other) {
+    return compareTo(other) == 0;
+  }
+
+  /**
+   * Compares this row range to another row range. Compares in order: start row, inclusiveness of
+   * start row, end row, inclusiveness of end row. Infinite rows sort first, and non-infinite rows
+   * are compared with {@link Text#compareTo(BinaryComparable)}. Inclusive sorts before
+   * non-inclusive.
+   *
+   * @param other range row to compare
+   * @return comparison result
+   */
+  @Override
+  public int compareTo(RowRange other) {
+    // Compare infinite start rows
+    int comp = Boolean.compare(this.infiniteStartRow, other.infiniteStartRow);
+
+    if (comp == 0) {
+      // Compare non-infinite start rows and start row inclusiveness
+      if (!this.infiniteStartRow) {
+        if (this.startRow == null && other.startRow == null) {
+          comp = 0;
+        } else if (this.startRow == null) {
+          comp = -1;
+        } else if (other.startRow == null) {
+          comp = 1;
+        } else {
+          comp = this.startRow.compareTo(other.startRow);
+        }
+        if (comp == 0) {
+          comp = Boolean.compare(other.startRowInclusive, this.startRowInclusive);
+        }
+      }
+    }
+
+    if (comp == 0) {
+      // Compare infinite end rows
+      comp = Boolean.compare(this.infiniteEndRow, other.infiniteEndRow);
+
+      // Compare non-infinite end rows and end row inclusiveness
+      if (comp == 0 && !this.infiniteEndRow) {
+        if (this.endRow == null && other.endRow == null) {
+          comp = 0;
+        } else if (this.endRow == null) {
+          comp = 1;
+        } else if (other.endRow == null) {
+          comp = -1;
+        } else {
+          comp = this.endRow.compareTo(other.endRow);
+        }
+        if (comp == 0) {
+          comp = Boolean.compare(this.endRowInclusive, other.endRowInclusive);
+        }
+      }
+    }
+
+    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, false otherwise
+   */
+  public boolean contains(Text row) {
+    if (infiniteStartRow) {
+      return !afterEndRow(row);
+    } else if (infiniteEndRow) {
+      return !beforeStartRow(row);
+    } else {
+      return !beforeStartRow(row) && !afterEndRow(row);
+    }
+  }
+
+  /**
+   * Merges overlapping and adjacent row ranges. 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 startRow values
+    sortedRowRanges.sort((r1, r2) -> {
+      if (r1.startRow == null && r2.startRow == null) {
+        return 0;
+      } else if (r1.startRow == null) {
+        return -1;
+      } else if (r2.startRow == null) {
+        return 1;
+      }
+      return r1.compareTo(r2);
+    });
+
+    ArrayList<RowRange> mergedRowRanges = new ArrayList<>(rowRanges.size());
+
+    // Initialize the current range for merging
+    RowRange currentRange = sortedRowRanges.get(0);
+    boolean currentStartRowInclusive = sortedRowRanges.get(0).startRowInclusive;
+
+    // Iterate through the sorted row ranges, merging overlapping and adjacent ranges
+    for (int i = 1; i < sortedRowRanges.size(); i++) {
+      if (currentRange.infiniteStartRow && currentRange.infiniteEndRow) {
+        // The current range covers all possible rows, no further merging needed
+        break;
+      }
+
+      RowRange nextRange = sortedRowRanges.get(i);
+
+      if (nextRange.infiniteStartRow && nextRange.infiniteEndRow) {
+        // The next range covers all possible rows, set the current range to cover all rows
+        currentRange = RowRange.all();
+        break;
+      }
+
+      boolean startRowsEqual = (currentRange.startRow == null && nextRange.startRow == null)
+          || (currentRange.startRow != null && currentRange.startRow.equals(nextRange.startRow));
+
+      int comparison;
+      if (startRowsEqual || currentRange.infiniteEndRow
+          || (nextRange.startRow != null && (currentRange.endRow == null
+              || currentRange.endRow.compareTo(nextRange.startRow) > 0
+              || (currentRange.endRow.equals(nextRange.startRow)
+                  && (!currentRange.endRowInclusive || nextRange.startRowInclusive))))) {
+        if (nextRange.infiniteEndRow) {
+          comparison = 1;
+        } else if (currentRange.endRow == null) {
+          comparison = -1;
+        } else {
+          comparison = nextRange.endRow.compareTo(currentRange.endRow);
+        }
+        if (comparison > 0 || (comparison == 0 && nextRange.endRowInclusive)) {
+          currentRange = RowRange.range(currentRange.startRow, currentStartRowInclusive,
+              nextRange.endRow, nextRange.endRowInclusive);
+        } // 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;
+        currentStartRowInclusive = nextRange.startRowInclusive;
+      }
+    }
+
+    // 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 passed in 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 passed in 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 start and end row values with the current instance's values
+    Text startRow = this.startRow;
+    boolean startRowInclusive = this.startRowInclusive;
+    Text endRow = this.endRow;
+    boolean endRowInclusive = this.endRowInclusive;
+
+    // If the input rowRange has a defined startRow, update startRow and startRowInclusive if needed
+    if (rowRange.startRow != null) {
+      // If the input rowRange's startRow is after this instance's endRow or equal but not
+      // inclusive, they do not overlap
+      if (afterEndRow(rowRange.startRow) || (rowRange.startRow.equals(this.endRow)
+          && !(rowRange.startRowInclusive && this.endRowInclusive))) {
+        if (returnNullIfDisjoint) {
+          return null;

Review Comment:
   This was not covered by unit test



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.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; set to null to cover all rows
+   */
+  public static RowRange closed(CharSequence row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);

Review Comment:
   This was not covered by unit test.



##########
core/src/test/java/org/apache/accumulo/core/data/RowRangeTest.java:
##########
@@ -0,0 +1,569 @@
+/*
+ * 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.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 StaticEntryPointTests {
+
+    @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 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 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 testEqualsWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.openClosed("r1", "row5");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen("r1", "row4");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentStartRowAndEndRowInclusiveness() {
+      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));
+    }
+
+    @Test
+    void testCompareWithDifferentStartRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r2", "r3");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentEndRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r1", "r4");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.closedOpen("r1", "r3");
+      assertTrue(range1.compareTo(range2) > 0);
+    }
+
+    @Test
+    void testCompareWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.openClosed("r1", "r3");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteStartRow() {
+      RowRange range1 = RowRange.atLeast("r1");
+      RowRange range2 = RowRange.all();
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteEndRow() {
+      RowRange range1 = RowRange.all();
+      RowRange range2 = RowRange.atLeast("r1");
+      assertTrue(range1.compareTo(range2) > 0);
+    }
+  }
+
+  @Nested
+  class ContainsTests {
+
+    @Test
+    void testContainsWithAllRange() {
+      RowRange range = RowRange.all();
+      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("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertFalse(range.contains(new Text("r3")));

Review Comment:
   Added some test data w/ same prefix and with binary zero that is next possible row.
   
   ```suggestion
         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")));
   ```



##########
core/src/test/java/org/apache/accumulo/core/data/RowRangeTest.java:
##########
@@ -0,0 +1,569 @@
+/*
+ * 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.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 StaticEntryPointTests {
+
+    @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 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 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 testEqualsWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.openClosed("r1", "row5");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen("r1", "row4");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentStartRowAndEndRowInclusiveness() {
+      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));
+    }
+
+    @Test
+    void testCompareWithDifferentStartRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r2", "r3");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentEndRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r1", "r4");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.closedOpen("r1", "r3");
+      assertTrue(range1.compareTo(range2) > 0);
+    }
+
+    @Test
+    void testCompareWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.openClosed("r1", "r3");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteStartRow() {
+      RowRange range1 = RowRange.atLeast("r1");
+      RowRange range2 = RowRange.all();
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteEndRow() {
+      RowRange range1 = RowRange.all();
+      RowRange range2 = RowRange.atLeast("r1");
+      assertTrue(range1.compareTo(range2) > 0);
+    }
+  }
+
+  @Nested
+  class ContainsTests {
+
+    @Test
+    void testContainsWithAllRange() {
+      RowRange range = RowRange.all();
+      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("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertFalse(range.contains(new Text("r3")));
+    }
+
+    @Test
+    void testContainsWithClosedRange() {
+      RowRange range = RowRange.closed("r1", "r3");
+      assertTrue(range.contains(new Text("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertTrue(range.contains(new Text("r3")));

Review Comment:
   ```suggestion
         assertFalse(range.contains(new Text("r1")));
         assertTrue(range.contains(new Text("r1")));
         assertTrue(range.contains(new Text("r2")));
         assertTrue(range.contains(new Text("r3")));
        assertFalse(range.contains(new Text("r30")));
   ```



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.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; set to null to cover all rows
+   */
+  public static RowRange closed(CharSequence row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(Text startRow, boolean startInclusive, Text endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow, startInclusive, endRow, endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(CharSequence startRow, boolean startInclusive, CharSequence endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow == null ? null : new Text(startRow.toString()), startInclusive,
+        endRow == null ? null : new Text(endRow.toString()), endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  private RowRange(Text startRow, boolean startInclusive, Text endRow, boolean endInclusive) {
+    this.startRow = startRow;
+    this.startRowInclusive = startInclusive;
+    this.infiniteStartRow = startRow == null;
+    this.endRow = endRow;
+    this.endRowInclusive = endInclusive;
+    this.infiniteEndRow = endRow == null;
+
+    if (!infiniteStartRow && !infiniteEndRow && beforeStartRowImpl(endRow)) {
+      throw new IllegalArgumentException(
+          "Start row must be less than end row in row range (" + startRow + ", " + endRow + ")");
+    }
+  }
+
+  public Text getStartRow() {
+    return startRow;
+  }
+
+  public boolean isStartRowInclusive() {
+    return startRowInclusive;
+  }
+
+  public Text getEndRow() {
+    return endRow;
+  }
+
+  public boolean isEndRowInclusive() {
+    return endRowInclusive;
+  }
+
+  /**
+   * Converts this row range to a {@link Range} object.
+   */
+  public Range toRange() {
+    return new Range(startRow, startRowInclusive, endRow, endRowInclusive);
+  }
+
+  /**
+   * Determines if the given row is before the start row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is before the row range, otherwise false
+   */
+  public boolean beforeStartRow(Text row) {
+    return beforeStartRowImpl(row);
+  }
+
+  /**
+   * Determines if the given row is after the end row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is after the row range, otherwise false
+   */
+  public boolean afterEndRow(Text row) {
+    if (infiniteEndRow) {
+      return false;
+    }
+
+    if (endRowInclusive) {
+      return row.compareTo(endRow) > 0;
+    }
+    return row.compareTo(endRow) >= 0;
+  }
+
+  /**
+   * Implements logic of {@link #beforeStartRow(Text)}, but in a private method, so that it can be
+   * safely used by constructors if a subclass overrides that {@link #beforeStartRow(Text)}
+   */
+  private boolean beforeStartRowImpl(Text row) {
+    if (this.infiniteStartRow) {
+      return false;
+    }
+
+    if (startRowInclusive) {
+      return row.compareTo(startRow) < 0;
+    }
+    return row.compareTo(startRow) <= 0;
+  }
+
+  @Override
+  public String toString() {
+    final String startRangeSymbol = (startRowInclusive && startRow != null) ? "[" : "(";
+    final String startRow = this.startRow == null ? "-inf" : this.startRow.toString();
+    final String endRow = this.endRow == null ? "+inf" : this.endRow.toString();
+    final String endRangeSymbol = (endRowInclusive && this.endRow != null) ? "]" : ")";
+    return startRangeSymbol + startRow + "," + endRow + endRangeSymbol;
+  }
+
+  @Override
+  public int hashCode() {
+    int startHash = infiniteStartRow ? 0 : startRow.hashCode() + (startRowInclusive ? 1 : 0);
+    int stopHash = infiniteEndRow ? 0 : endRow.hashCode() + (endRowInclusive ? 1 : 0);
+
+    return startHash + stopHash;
+  }
+
+  @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 equals, false otherwise
+   */
+  public boolean equals(RowRange other) {
+    return compareTo(other) == 0;
+  }
+
+  /**
+   * Compares this row range to another row range. Compares in order: start row, inclusiveness of
+   * start row, end row, inclusiveness of end row. Infinite rows sort first, and non-infinite rows
+   * are compared with {@link Text#compareTo(BinaryComparable)}. Inclusive sorts before
+   * non-inclusive.
+   *
+   * @param other range row to compare
+   * @return comparison result
+   */
+  @Override
+  public int compareTo(RowRange other) {
+    // Compare infinite start rows
+    int comp = Boolean.compare(this.infiniteStartRow, other.infiniteStartRow);
+
+    if (comp == 0) {
+      // Compare non-infinite start rows and start row inclusiveness
+      if (!this.infiniteStartRow) {
+        if (this.startRow == null && other.startRow == null) {
+          comp = 0;
+        } else if (this.startRow == null) {
+          comp = -1;
+        } else if (other.startRow == null) {
+          comp = 1;
+        } else {
+          comp = this.startRow.compareTo(other.startRow);
+        }
+        if (comp == 0) {
+          comp = Boolean.compare(other.startRowInclusive, this.startRowInclusive);
+        }
+      }
+    }
+
+    if (comp == 0) {
+      // Compare infinite end rows
+      comp = Boolean.compare(this.infiniteEndRow, other.infiniteEndRow);
+
+      // Compare non-infinite end rows and end row inclusiveness
+      if (comp == 0 && !this.infiniteEndRow) {
+        if (this.endRow == null && other.endRow == null) {
+          comp = 0;
+        } else if (this.endRow == null) {
+          comp = 1;
+        } else if (other.endRow == null) {
+          comp = -1;
+        } else {
+          comp = this.endRow.compareTo(other.endRow);
+        }
+        if (comp == 0) {
+          comp = Boolean.compare(this.endRowInclusive, other.endRowInclusive);
+        }
+      }
+    }
+
+    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, false otherwise
+   */
+  public boolean contains(Text row) {
+    if (infiniteStartRow) {
+      return !afterEndRow(row);
+    } else if (infiniteEndRow) {
+      return !beforeStartRow(row);
+    } else {
+      return !beforeStartRow(row) && !afterEndRow(row);
+    }
+  }
+
+  /**
+   * Merges overlapping and adjacent row ranges. 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 startRow values
+    sortedRowRanges.sort((r1, r2) -> {
+      if (r1.startRow == null && r2.startRow == null) {
+        return 0;
+      } else if (r1.startRow == null) {
+        return -1;
+      } else if (r2.startRow == null) {
+        return 1;
+      }
+      return r1.compareTo(r2);
+    });
+
+    ArrayList<RowRange> mergedRowRanges = new ArrayList<>(rowRanges.size());
+
+    // Initialize the current range for merging
+    RowRange currentRange = sortedRowRanges.get(0);
+    boolean currentStartRowInclusive = sortedRowRanges.get(0).startRowInclusive;
+
+    // Iterate through the sorted row ranges, merging overlapping and adjacent ranges
+    for (int i = 1; i < sortedRowRanges.size(); i++) {
+      if (currentRange.infiniteStartRow && currentRange.infiniteEndRow) {
+        // The current range covers all possible rows, no further merging needed
+        break;
+      }
+
+      RowRange nextRange = sortedRowRanges.get(i);
+
+      if (nextRange.infiniteStartRow && nextRange.infiniteEndRow) {
+        // The next range covers all possible rows, set the current range to cover all rows
+        currentRange = RowRange.all();

Review Comment:
   This is not covered by unit test.



##########
core/src/test/java/org/apache/accumulo/core/data/RowRangeTest.java:
##########
@@ -0,0 +1,569 @@
+/*
+ * 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.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 StaticEntryPointTests {
+
+    @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 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 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 testEqualsWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.openClosed("r1", "row5");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen("r1", "row4");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentStartRowAndEndRowInclusiveness() {
+      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));
+    }
+
+    @Test
+    void testCompareWithDifferentStartRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r2", "r3");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentEndRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r1", "r4");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.closedOpen("r1", "r3");
+      assertTrue(range1.compareTo(range2) > 0);
+    }
+
+    @Test
+    void testCompareWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.openClosed("r1", "r3");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteStartRow() {
+      RowRange range1 = RowRange.atLeast("r1");
+      RowRange range2 = RowRange.all();
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteEndRow() {
+      RowRange range1 = RowRange.all();
+      RowRange range2 = RowRange.atLeast("r1");
+      assertTrue(range1.compareTo(range2) > 0);
+    }
+  }
+
+  @Nested
+  class ContainsTests {
+
+    @Test
+    void testContainsWithAllRange() {
+      RowRange range = RowRange.all();
+      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("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertFalse(range.contains(new Text("r3")));
+    }
+
+    @Test
+    void testContainsWithClosedRange() {
+      RowRange range = RowRange.closed("r1", "r3");
+      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("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertTrue(range.contains(new Text("r3")));
+    }
+
+    @Test
+    void testContainsWithClosedOpenRange() {
+      RowRange range = RowRange.closedOpen("r1", "r3");
+      assertTrue(range.contains(new Text("r1")));

Review Comment:
   ```suggestion
        assertFalse(range.contains(new Text("r0")));
         assertTrue(range.contains(new Text("r1")));
   ```



##########
core/src/test/java/org/apache/accumulo/core/data/RowRangeTest.java:
##########
@@ -0,0 +1,569 @@
+/*
+ * 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.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 StaticEntryPointTests {
+
+    @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 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 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 testEqualsWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.openClosed("r1", "row5");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen("r1", "row4");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentStartRowAndEndRowInclusiveness() {
+      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));
+    }
+
+    @Test
+    void testCompareWithDifferentStartRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r2", "r3");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentEndRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r1", "r4");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.closedOpen("r1", "r3");
+      assertTrue(range1.compareTo(range2) > 0);
+    }
+
+    @Test
+    void testCompareWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.openClosed("r1", "r3");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteStartRow() {
+      RowRange range1 = RowRange.atLeast("r1");
+      RowRange range2 = RowRange.all();
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteEndRow() {
+      RowRange range1 = RowRange.all();
+      RowRange range2 = RowRange.atLeast("r1");
+      assertTrue(range1.compareTo(range2) > 0);
+    }
+  }
+
+  @Nested
+  class ContainsTests {
+
+    @Test
+    void testContainsWithAllRange() {
+      RowRange range = RowRange.all();
+      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("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertFalse(range.contains(new Text("r3")));
+    }
+
+    @Test
+    void testContainsWithClosedRange() {
+      RowRange range = RowRange.closed("r1", "r3");
+      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("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertTrue(range.contains(new Text("r3")));
+    }
+
+    @Test
+    void testContainsWithClosedOpenRange() {
+      RowRange range = RowRange.closedOpen("r1", "r3");
+      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("r1")));
+      assertFalse(range.contains(new Text("r2")));

Review Comment:
   ```suggestion
         assertTrue(range.contains(new Text("r1")));
         assertFalse(range.contains(new Text("r10")));
         assertFalse(range.contains(new Text("r2")));
   ```



##########
core/src/test/java/org/apache/accumulo/core/data/RowRangeTest.java:
##########
@@ -0,0 +1,569 @@
+/*
+ * 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.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 StaticEntryPointTests {
+
+    @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 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 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 testEqualsWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.openClosed("r1", "row5");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen("r1", "row4");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentStartRowAndEndRowInclusiveness() {
+      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));
+    }
+
+    @Test
+    void testCompareWithDifferentStartRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r2", "r3");
+      assertTrue(range1.compareTo(range2) < 0);

Review Comment:
   For all of the compare to test it would be nice to try both directions.
   
   ```suggestion
         assertTrue(range1.compareTo(range2) < 0);
         assertTrue(range2.compareTo(range1) > 0);
   ```



##########
core/src/test/java/org/apache/accumulo/core/data/RowRangeTest.java:
##########
@@ -0,0 +1,569 @@
+/*
+ * 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.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 StaticEntryPointTests {
+
+    @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 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 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 testEqualsWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.openClosed("r1", "row5");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.closedOpen("r1", "row5");
+      RowRange range2 = RowRange.closedOpen("r1", "row4");
+      assertFalse(range1.equals(range2));
+    }
+
+    @Test
+    void testEqualsWithDifferentStartRowAndEndRowInclusiveness() {
+      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));
+    }
+
+    @Test
+    void testCompareWithDifferentStartRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r2", "r3");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentEndRow() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.open("r1", "r4");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithDifferentStartRowInclusiveness() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.closedOpen("r1", "r3");
+      assertTrue(range1.compareTo(range2) > 0);
+    }
+
+    @Test
+    void testCompareWithDifferentEndRowInclusiveness() {
+      RowRange range1 = RowRange.open("r1", "r3");
+      RowRange range2 = RowRange.openClosed("r1", "r3");
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteStartRow() {
+      RowRange range1 = RowRange.atLeast("r1");
+      RowRange range2 = RowRange.all();
+      assertTrue(range1.compareTo(range2) < 0);
+    }
+
+    @Test
+    void testCompareWithInfiniteEndRow() {
+      RowRange range1 = RowRange.all();
+      RowRange range2 = RowRange.atLeast("r1");
+      assertTrue(range1.compareTo(range2) > 0);
+    }
+  }
+
+  @Nested
+  class ContainsTests {
+
+    @Test
+    void testContainsWithAllRange() {
+      RowRange range = RowRange.all();
+      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("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertFalse(range.contains(new Text("r3")));
+    }
+
+    @Test
+    void testContainsWithClosedRange() {
+      RowRange range = RowRange.closed("r1", "r3");
+      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("r1")));
+      assertTrue(range.contains(new Text("r2")));
+      assertTrue(range.contains(new Text("r3")));
+    }
+
+    @Test
+    void testContainsWithClosedOpenRange() {
+      RowRange range = RowRange.closedOpen("r1", "r3");
+      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("r1")));
+      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) {

Review Comment:
   This is neat, I have not see this before.



##########
core/src/main/java/org/apache/accumulo/core/data/RowRange.java:
##########
@@ -0,0 +1,650 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.hadoop.io.BinaryComparable;
+import org.apache.hadoop.io.Text;
+
+/**
+ * This class is used to specify a range of rows.
+ *
+ * @since 3.0.0
+ */
+public class RowRange implements Comparable<RowRange> {
+  final private Text startRow;
+  final private Text endRow;
+  final private boolean startRowInclusive;
+  final private boolean endRowInclusive;
+  final private boolean infiniteStartRow;
+  final private boolean infiniteEndRow;
+
+  /**
+   * 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 startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange open(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.greaterThan(row)?");
+    return range(startRow, false, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, true, endRow, true);
+  }
+
+  /**
+   * Creates a range that covers an entire row.
+   *
+   * @param row row to cover; set to null to cover all rows
+   */
+  public static RowRange closed(Text row) {
+    Objects.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; set to null to cover all rows
+   */
+  public static RowRange closed(CharSequence row) {
+    Objects.requireNonNull(row, "Did you mean to use RowRange.all()?");
+    return range(row, true, row, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow exclusive to endRow inclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange openClosed(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.atMost(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.atLeast(row)?");
+    return range(startRow, false, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(Text startRow, Text endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows from startRow inclusive to endRow exclusive.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param endRow ending row; set to null for positive infinity
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange closedOpen(CharSequence startRow, CharSequence endRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.lessThan(row)?");
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.moreThan(row)?");
+    return range(startRow, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly greater than startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange greaterThan(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, false, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(Text startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows greater than or equal to startRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   */
+  public static RowRange atLeast(CharSequence startRow) {
+    Objects.requireNonNull(startRow, "Did you mean to use RowRange.all()?");
+    return range(startRow, true, null, true);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows strictly less than endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange lessThan(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, false);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(Text endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows less than or equal to endRow.
+   *
+   * @param endRow ending row; set to null for positive infinity
+   */
+  public static RowRange atMost(CharSequence endRow) {
+    Objects.requireNonNull(endRow, "Did you mean to use RowRange.all()?");
+    return range(null, true, endRow, true);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(Text startRow, boolean startInclusive, Text endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow, startInclusive, endRow, endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  public static RowRange range(CharSequence startRow, boolean startInclusive, CharSequence endRow,
+      boolean endInclusive) {
+    return new RowRange(startRow == null ? null : new Text(startRow.toString()), startInclusive,
+        endRow == null ? null : new Text(endRow.toString()), endInclusive);
+  }
+
+  /**
+   * Creates a range of rows from startRow to endRow.
+   *
+   * @param startRow starting row; set to null for the smallest possible row (an empty one)
+   * @param startInclusive true to include start row, false to skip
+   * @param endRow ending row; set to null for positive infinity
+   * @param endInclusive true to include end row, false to skip
+   * @throws IllegalArgumentException if end row is before start row
+   */
+  private RowRange(Text startRow, boolean startInclusive, Text endRow, boolean endInclusive) {
+    this.startRow = startRow;
+    this.startRowInclusive = startInclusive;
+    this.infiniteStartRow = startRow == null;
+    this.endRow = endRow;
+    this.endRowInclusive = endInclusive;
+    this.infiniteEndRow = endRow == null;
+
+    if (!infiniteStartRow && !infiniteEndRow && beforeStartRowImpl(endRow)) {
+      throw new IllegalArgumentException(
+          "Start row must be less than end row in row range (" + startRow + ", " + endRow + ")");
+    }
+  }
+
+  public Text getStartRow() {
+    return startRow;
+  }
+
+  public boolean isStartRowInclusive() {
+    return startRowInclusive;
+  }
+
+  public Text getEndRow() {
+    return endRow;
+  }
+
+  public boolean isEndRowInclusive() {
+    return endRowInclusive;
+  }
+
+  /**
+   * Converts this row range to a {@link Range} object.
+   */
+  public Range toRange() {
+    return new Range(startRow, startRowInclusive, endRow, endRowInclusive);
+  }
+
+  /**
+   * Determines if the given row is before the start row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is before the row range, otherwise false
+   */
+  public boolean beforeStartRow(Text row) {
+    return beforeStartRowImpl(row);
+  }
+
+  /**
+   * Determines if the given row is after the end row of this row range.
+   *
+   * @param row row to check
+   * @return true if the given row is after the row range, otherwise false
+   */
+  public boolean afterEndRow(Text row) {
+    if (infiniteEndRow) {
+      return false;
+    }
+
+    if (endRowInclusive) {
+      return row.compareTo(endRow) > 0;
+    }
+    return row.compareTo(endRow) >= 0;
+  }
+
+  /**
+   * Implements logic of {@link #beforeStartRow(Text)}, but in a private method, so that it can be
+   * safely used by constructors if a subclass overrides that {@link #beforeStartRow(Text)}
+   */
+  private boolean beforeStartRowImpl(Text row) {
+    if (this.infiniteStartRow) {
+      return false;
+    }
+
+    if (startRowInclusive) {
+      return row.compareTo(startRow) < 0;
+    }
+    return row.compareTo(startRow) <= 0;
+  }
+
+  @Override
+  public String toString() {
+    final String startRangeSymbol = (startRowInclusive && startRow != null) ? "[" : "(";
+    final String startRow = this.startRow == null ? "-inf" : this.startRow.toString();
+    final String endRow = this.endRow == null ? "+inf" : this.endRow.toString();
+    final String endRangeSymbol = (endRowInclusive && this.endRow != null) ? "]" : ")";
+    return startRangeSymbol + startRow + "," + endRow + endRangeSymbol;
+  }
+
+  @Override
+  public int hashCode() {
+    int startHash = infiniteStartRow ? 0 : startRow.hashCode() + (startRowInclusive ? 1 : 0);
+    int stopHash = infiniteEndRow ? 0 : endRow.hashCode() + (endRowInclusive ? 1 : 0);
+
+    return startHash + stopHash;
+  }
+
+  @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 equals, false otherwise
+   */
+  public boolean equals(RowRange other) {
+    return compareTo(other) == 0;
+  }
+
+  /**
+   * Compares this row range to another row range. Compares in order: start row, inclusiveness of
+   * start row, end row, inclusiveness of end row. Infinite rows sort first, and non-infinite rows
+   * are compared with {@link Text#compareTo(BinaryComparable)}. Inclusive sorts before
+   * non-inclusive.
+   *
+   * @param other range row to compare
+   * @return comparison result
+   */
+  @Override
+  public int compareTo(RowRange other) {
+    // Compare infinite start rows
+    int comp = Boolean.compare(this.infiniteStartRow, other.infiniteStartRow);
+
+    if (comp == 0) {
+      // Compare non-infinite start rows and start row inclusiveness
+      if (!this.infiniteStartRow) {
+        if (this.startRow == null && other.startRow == null) {
+          comp = 0;
+        } else if (this.startRow == null) {
+          comp = -1;
+        } else if (other.startRow == null) {
+          comp = 1;
+        } else {
+          comp = this.startRow.compareTo(other.startRow);
+        }
+        if (comp == 0) {
+          comp = Boolean.compare(other.startRowInclusive, this.startRowInclusive);
+        }
+      }
+    }
+
+    if (comp == 0) {
+      // Compare infinite end rows
+      comp = Boolean.compare(this.infiniteEndRow, other.infiniteEndRow);
+
+      // Compare non-infinite end rows and end row inclusiveness
+      if (comp == 0 && !this.infiniteEndRow) {
+        if (this.endRow == null && other.endRow == null) {
+          comp = 0;
+        } else if (this.endRow == null) {
+          comp = 1;
+        } else if (other.endRow == null) {
+          comp = -1;
+        } else {
+          comp = this.endRow.compareTo(other.endRow);
+        }
+        if (comp == 0) {
+          comp = Boolean.compare(this.endRowInclusive, other.endRowInclusive);
+        }
+      }
+    }
+
+    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, false otherwise
+   */
+  public boolean contains(Text row) {
+    if (infiniteStartRow) {
+      return !afterEndRow(row);
+    } else if (infiniteEndRow) {
+      return !beforeStartRow(row);
+    } else {
+      return !beforeStartRow(row) && !afterEndRow(row);
+    }
+  }
+
+  /**
+   * Merges overlapping and adjacent row ranges. 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 startRow values
+    sortedRowRanges.sort((r1, r2) -> {
+      if (r1.startRow == null && r2.startRow == null) {
+        return 0;
+      } else if (r1.startRow == null) {
+        return -1;
+      } else if (r2.startRow == null) {
+        return 1;
+      }
+      return r1.compareTo(r2);
+    });
+
+    ArrayList<RowRange> mergedRowRanges = new ArrayList<>(rowRanges.size());
+
+    // Initialize the current range for merging
+    RowRange currentRange = sortedRowRanges.get(0);
+    boolean currentStartRowInclusive = sortedRowRanges.get(0).startRowInclusive;
+
+    // Iterate through the sorted row ranges, merging overlapping and adjacent ranges
+    for (int i = 1; i < sortedRowRanges.size(); i++) {
+      if (currentRange.infiniteStartRow && currentRange.infiniteEndRow) {
+        // The current range covers all possible rows, no further merging needed
+        break;
+      }
+
+      RowRange nextRange = sortedRowRanges.get(i);
+
+      if (nextRange.infiniteStartRow && nextRange.infiniteEndRow) {
+        // The next range covers all possible rows, set the current range to cover all rows
+        currentRange = RowRange.all();
+        break;
+      }
+
+      boolean startRowsEqual = (currentRange.startRow == null && nextRange.startRow == null)
+          || (currentRange.startRow != null && currentRange.startRow.equals(nextRange.startRow));
+
+      int comparison;
+      if (startRowsEqual || currentRange.infiniteEndRow
+          || (nextRange.startRow != null && (currentRange.endRow == null
+              || currentRange.endRow.compareTo(nextRange.startRow) > 0
+              || (currentRange.endRow.equals(nextRange.startRow)
+                  && (!currentRange.endRowInclusive || nextRange.startRowInclusive))))) {
+        if (nextRange.infiniteEndRow) {
+          comparison = 1;
+        } else if (currentRange.endRow == null) {
+          comparison = -1;
+        } else {
+          comparison = nextRange.endRow.compareTo(currentRange.endRow);
+        }
+        if (comparison > 0 || (comparison == 0 && nextRange.endRowInclusive)) {
+          currentRange = RowRange.range(currentRange.startRow, currentStartRowInclusive,
+              nextRange.endRow, nextRange.endRowInclusive);
+        } // 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;
+        currentStartRowInclusive = nextRange.startRowInclusive;
+      }
+    }
+
+    // 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 passed in 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 passed in 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 start and end row values with the current instance's values
+    Text startRow = this.startRow;
+    boolean startRowInclusive = this.startRowInclusive;
+    Text endRow = this.endRow;
+    boolean endRowInclusive = this.endRowInclusive;
+
+    // If the input rowRange has a defined startRow, update startRow and startRowInclusive if needed
+    if (rowRange.startRow != null) {
+      // If the input rowRange's startRow is after this instance's endRow or equal but not
+      // inclusive, they do not overlap
+      if (afterEndRow(rowRange.startRow) || (rowRange.startRow.equals(this.endRow)
+          && !(rowRange.startRowInclusive && this.endRowInclusive))) {
+        if (returnNullIfDisjoint) {
+          return null;
+        }
+        throw new IllegalArgumentException("RowRange " + rowRange + " does not overlap " + this);
+      } else if (!beforeStartRow(rowRange.startRow)) {
+        // If the input rowRange's startRow is within this instance's range, use it as the new
+        // startRow
+        startRow = rowRange.startRow;
+        startRowInclusive = rowRange.startRowInclusive;
+      }
+    }
+
+    // If the input rowRange has a defined endRow, update endRow and endRowInclusive if needed
+    if (rowRange.endRow != null) {
+      // If the input rowRange's endRow is before this instance's startRow or equal but not
+      // inclusive, they do not overlap
+      if (beforeStartRow(rowRange.endRow) || (rowRange.endRow.equals(this.startRow)
+          && !(rowRange.endRowInclusive && this.startRowInclusive))) {
+        if (returnNullIfDisjoint) {
+          return null;

Review Comment:
   This was not covered by unit test



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@accumulo.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org