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

vjasani pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/phoenix.git


The following commit(s) were added to refs/heads/master by this push:
     new 57b1f9f498 PHOENIX-7762 Incorrect scan boundary when slot span exceeds 
actual bound in composite key (#2373)
57b1f9f498 is described below

commit 57b1f9f498d25f1737d3dc7c2fc5b9dc8069eb6c
Author: Viraj Jasani <[email protected]>
AuthorDate: Tue Feb 17 21:34:01 2026 -0800

    PHOENIX-7762 Incorrect scan boundary when slot span exceeds actual bound in 
composite key (#2373)
---
 .../java/org/apache/phoenix/util/ScanUtil.java     |  37 +-
 .../phoenix/end2end/RowValueConstructorIT.java     | 398 +++++++++++++++++++++
 .../apache/phoenix/compile/QueryCompilerTest.java  | 189 ++++++++++
 3 files changed, 621 insertions(+), 3 deletions(-)

diff --git 
a/phoenix-core-client/src/main/java/org/apache/phoenix/util/ScanUtil.java 
b/phoenix-core-client/src/main/java/org/apache/phoenix/util/ScanUtil.java
index d668af16b4..5305408fbc 100644
--- a/phoenix-core-client/src/main/java/org/apache/phoenix/util/ScanUtil.java
+++ b/phoenix-core-client/src/main/java/org/apache/phoenix/util/ScanUtil.java
@@ -391,6 +391,34 @@ public class ScanUtil {
     return getKey(schema, slots, slotSpan, Bound.UPPER);
   }
 
+  private static Field getActualEndField(RowKeySchema schema, int 
startFieldIndex, int slotSpan,
+    KeyRange range, Bound bound) {
+    if (slotSpan > 0) {
+      byte[] boundBytes = range.getRange(bound);
+      // For multi-span slots, the bound bytes might cover fewer fields than 
the slotSpan. In such
+      // cases, we need to use the actual last field covered by the bytes for 
separator logic.
+      // Otherwise, a variable-length field may incorrectly omit its separator 
byte, producing an
+      // incorrect scan boundary.
+      if (boundBytes.length > 0 && boundBytes != KeyRange.UNBOUND) {
+        ImmutableBytesWritable spanPtr =
+          new ImmutableBytesWritable(boundBytes, 0, boundBytes.length);
+        if (!schema.position(spanPtr, startFieldIndex, startFieldIndex + 
slotSpan)) {
+          // The bytes don't cover the full span. Find the actual last field 
covered.
+          int actualEndFieldIdx = startFieldIndex;
+          for (int i = startFieldIndex + 1; i <= startFieldIndex + slotSpan; 
i++) {
+            spanPtr = new ImmutableBytesWritable(boundBytes, 0, 
boundBytes.length);
+            if (!schema.position(spanPtr, startFieldIndex, i)) {
+              break;
+            }
+            actualEndFieldIdx = i;
+          }
+          return schema.getField(actualEndFieldIdx);
+        }
+      }
+    }
+    return schema.getField(startFieldIndex + slotSpan);
+  }
+
   private static byte[] getKey(RowKeySchema schema, List<List<KeyRange>> 
slots, int[] slotSpan,
     Bound bound) {
     if (slots.isEmpty()) {
@@ -403,7 +431,9 @@ public class ScanUtil {
       position[i] = bound == Bound.LOWER ? 0 : slots.get(i).size() - 1;
       KeyRange range = slots.get(i).get(position[i]);
       slotEndingFieldPos = slotEndingFieldPos + slotSpan[i] + 1;
-      Field field = schema.getField(slotEndingFieldPos);
+      int startFieldPos = slotEndingFieldPos - slotSpan[i];
+      // Use the actual end field based on how many fields the bound bytes 
cover.
+      Field field = getActualEndField(schema, startFieldPos, slotSpan[i], 
range, bound);
       int keyLength = range.getRange(bound).length;
       if (!field.getDataType().isFixedWidth()) {
         if (field.getDataType() != PVarbinaryEncoded.INSTANCE) {
@@ -471,8 +501,9 @@ public class ScanUtil {
       // Build up the key by appending the bound of each key range
       // from the current position of each slot.
       KeyRange range = slots.get(i).get(position[i]);
-      // Use last slot in a multi-span column to determine if fixed width
-      field = schema.getField(fieldIndex + slotSpan[i]);
+      // Use last slot in a multi-span column to determine if fixed width.
+      // Use the actual end field based on how many fields the bound bytes 
cover.
+      field = getActualEndField(schema, fieldIndex, slotSpan[i], range, bound);
       boolean isFixedWidth = field.getDataType().isFixedWidth();
       /*
        * If the current slot is unbound then stop if: 1) setting the upper 
bound. There's no value
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/RowValueConstructorIT.java
 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/RowValueConstructorIT.java
index f2e4a2faa6..f4b03c7c5c 100644
--- 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/RowValueConstructorIT.java
+++ 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/RowValueConstructorIT.java
@@ -2098,6 +2098,404 @@ public class RowValueConstructorIT extends 
ParallelStatsDisabledIT {
     }
   }
 
+  @Test
+  public void testRVCWithUpperBoundOnIndex() throws Exception {
+    Connection conn = nextConnection(getUrl());
+    String tableName = generateUniqueName();
+    String indexName = generateUniqueName();
+
+    String ddl = "CREATE TABLE " + tableName + " (" + "pk VARCHAR NOT NULL, "
+      + "sk BIGINT NOT NULL, " + "category VARCHAR, " + "score DOUBLE, " + 
"data VARCHAR, "
+      + "CONSTRAINT table_pk PRIMARY KEY (pk, sk))";
+    conn.createStatement().execute(ddl);
+
+    conn = nextConnection(getUrl());
+    conn.createStatement()
+      .execute("CREATE INDEX " + indexName + " ON " + tableName + " (category, 
score)");
+
+    conn = nextConnection(getUrl());
+    PreparedStatement stmt = conn.prepareStatement(
+      "UPSERT INTO " + tableName + " (pk, sk, category, score, data) VALUES 
(?, ?, ?, ?, ?)");
+    for (int i = 0; i < 20000; i++) {
+      stmt.setString(1, "pk_" + (i % 100));
+      stmt.setLong(2, i);
+      stmt.setString(3, "category_" + (i % 10));
+      stmt.setDouble(4, i);
+      stmt.setString(5, "data_" + i);
+      stmt.execute();
+    }
+    conn.commit();
+
+    conn = nextConnection(getUrl());
+    // category_0 rows have scores: 0, 10, 20, ..., 19990
+    // With score <= 5000 AND (score, pk, sk) > (4990, 'pk_90', 4990):
+    // score=4990 row is (pk_90, 4990) which is NOT > the RVC bound (exact 
match)
+    // score=5000 row is (pk_0, 5000) which satisfies both conditions
+    // score=5010+ rows must be excluded by score <= 5000
+    String query =
+      "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score " + "FROM "
+        + tableName + " " + "WHERE category = 'category_0' " + "AND score <= 
5000 "
+        + "AND (score, pk, sk) > (4990, 'pk_90', 4990) "
+        + "ORDER BY category ASC, score ASC, pk ASC, sk ASC " + "LIMIT 100";
+    ResultSet rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_0", 5000, "category_0", 5000.0);
+    assertFalse(rs.next());
+
+    // Verify that score < 5000 returns no row
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_0' " + "AND 
score < 5000 "
+      + "AND (score, pk, sk) > (4990, 'pk_90', 4990) "
+      + "ORDER BY category ASC, score ASC, pk ASC, sk ASC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_0' " + "AND 
score <= 5000 "
+      + "AND (score, pk, sk) > (4990, 'pk_90', 4990) "
+      + "ORDER BY category ASC, score DESC, pk DESC, sk DESC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_0", 5000, "category_0", 5000.0);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_0' " + "AND 
score < 5000 "
+      + "AND (score, pk, sk) > (4990, 'pk_90', 4990) "
+      + "ORDER BY category ASC, score DESC, pk DESC, sk DESC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertFalse("Expected no rows with score < 5000 and DESC ordering", 
rs.next());
+
+    // score >= 4980 with RVC upper bound < (5010, 'pk_10', 5010).
+    // category_0 rows with score >= 4980:
+    // score=4980: (pk_80, 4980)
+    // score=4990: (pk_90, 4990)
+    // score=5000: (pk_0, 5000)
+    // score=5010: (pk_10, 5010) -> RVC equal, NOT strictly less -> excluded
+    // Expected 3 rows in ASC order.
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_0' " + "AND 
score >= 4980 "
+      + "AND (score, pk, sk) < (5010, 'pk_10', 5010) "
+      + "ORDER BY category ASC, score ASC, pk ASC, sk ASC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_80", 4980, "category_0", 4980.0);
+    assertRow(rs, "pk_90", 4990, "category_0", 4990.0);
+    assertRow(rs, "pk_0", 5000, "category_0", 5000.0);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_0' " + "AND 
score >= 4980 "
+      + "AND (score, pk, sk) < (5010, 'pk_10', 5010) "
+      + "ORDER BY category ASC, score DESC, pk DESC, sk DESC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_0", 5000, "category_0", 5000.0);
+    assertRow(rs, "pk_90", 4990, "category_0", 4990.0);
+    assertRow(rs, "pk_80", 4980, "category_0", 4980.0);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_0' "
+      + "AND (score, pk) <= (5000, 'pk_0') " + "AND (score, pk, sk) > (4970, 
'pk_70', 4970) "
+      + "ORDER BY category ASC, score ASC, pk ASC, sk ASC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_80", 4980, "category_0", 4980.0);
+    assertRow(rs, "pk_90", 4990, "category_0", 4990.0);
+    assertRow(rs, "pk_0", 5000, "category_0", 5000.0);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_0' "
+      + "AND (score, pk) <= (5000, 'pk_0') " + "AND (score, pk, sk) > (4970, 
'pk_70', 4970) "
+      + "ORDER BY category ASC, score DESC, pk DESC, sk DESC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_0", 5000, "category_0", 5000.0);
+    assertRow(rs, "pk_90", 4990, "category_0", 4990.0);
+    assertRow(rs, "pk_80", 4980, "category_0", 4980.0);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_0' "
+      + "AND (score, pk) < (5000, 'pk_0') " + "AND (score, pk, sk) > (4970, 
'pk_70', 4970) "
+      + "ORDER BY category ASC, score ASC, pk ASC, sk ASC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_80", 4980, "category_0", 4980.0);
+    assertRow(rs, "pk_90", 4990, "category_0", 4990.0);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_0' "
+      + "AND (score, pk) < (5000, 'pk_0') " + "AND (score, pk, sk) > (4970, 
'pk_70', 4970) "
+      + "ORDER BY category ASC, score DESC, pk DESC, sk DESC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_90", 4990, "category_0", 4990.0);
+    assertRow(rs, "pk_80", 4980, "category_0", 4980.0);
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void testRVCWithAlternatingFixedVarWidthPK() throws Exception {
+    Connection conn = nextConnection(getUrl());
+    String tableName = generateUniqueName();
+
+    String ddl = "CREATE TABLE " + tableName + " (" + "a VARCHAR NOT NULL, " + 
"b BIGINT NOT NULL, "
+      + "c VARCHAR NOT NULL, " + "d INTEGER NOT NULL, " + "e VARCHAR NOT NULL, 
"
+      + "f BIGINT NOT NULL, " + "val VARCHAR, " + "CONSTRAINT pk PRIMARY KEY 
(a, b, c, d, e, f))";
+    conn.createStatement().execute(ddl);
+
+    conn = nextConnection(getUrl());
+    PreparedStatement stmt = conn.prepareStatement(
+      "UPSERT INTO " + tableName + " (a, b, c, d, e, f, val) VALUES (?, ?, ?, 
?, ?, ?, ?)");
+    for (int i = 0; i < 50000; i++) {
+      stmt.setString(1, "a_" + (i % 5));
+      stmt.setLong(2, i);
+      stmt.setString(3, "c_" + (i % 7));
+      stmt.setInt(4, i % 100);
+      stmt.setString(5, "e_" + (i % 3));
+      stmt.setLong(6, i);
+      stmt.setString(7, "val_" + i);
+      stmt.execute();
+      if (i % 5000 == 0) {
+        conn.commit();
+      }
+    }
+    conn.commit();
+
+    conn = nextConnection(getUrl());
+    String query = "SELECT a, b, c, d, e, f FROM " + tableName
+      + " WHERE a = 'a_0' AND b >= 24990 AND b <= 25010" + " ORDER BY a, b, c, 
d, e, f";
+    ResultSet rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 24990, "c_0", 90, "e_0", 24990);
+    assertRow(rs, "a_0", 24995, "c_5", 95, "e_2", 24995);
+    assertRow(rs, "a_0", 25000, "c_3", 0, "e_1", 25000);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertRow(rs, "a_0", 25010, "c_6", 10, "e_2", 25010);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT a, b, c, d, e, f FROM " + tableName + " WHERE a = 'a_0' 
AND b <= 25010"
+      + " AND (b > 24995 OR (b = 24995 AND c > 'c_5'))" + " ORDER BY a, b, c, 
d, e, f LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 25000, "c_3", 0, "e_1", 25000);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertRow(rs, "a_0", 25010, "c_6", 10, "e_2", 25010);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT a, b, c, d, e, f FROM " + tableName
+      + " WHERE a = 'a_0' AND b <= 25010 AND (b, c) > (24995, 'c_5')"
+      + " ORDER BY a, b, c, d, e, f LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 25000, "c_3", 0, "e_1", 25000);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertRow(rs, "a_0", 25010, "c_6", 10, "e_2", 25010);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT a, b, c, d, e, f FROM " + tableName
+      + " WHERE a = 'a_0' AND b <= 25010 AND (b, c, d) > (25000, 'c_3', 0)"
+      + " ORDER BY a, b, c, d, e, f LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertRow(rs, "a_0", 25010, "c_6", 10, "e_2", 25010);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT a, b, c, d, e, f FROM " + tableName
+      + " WHERE a = 'a_0' AND b <= 25010 AND (b, c, d, e) > (25000, 'c_3', 0, 
'e_1')"
+      + " ORDER BY a, b, c, d, e, f LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertRow(rs, "a_0", 25010, "c_6", 10, "e_2", 25010);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query =
+      "SELECT a, b, c, d, e, f FROM " + tableName + " WHERE a = 'a_0' AND (b, 
c) <= (25005, 'c_1')"
+        + " AND (b, c, d) > (24995, 'c_5', 95)" + " ORDER BY a, b, c, d, e, f 
LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 25000, "c_3", 0, "e_1", 25000);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT a, b, c, d, e, f FROM " + tableName
+      + " WHERE a = 'a_0' AND (b, c, d) <= (25005, 'c_1', 5)"
+      + " AND (b, c, d, e, f) > (24995, 'c_5', 95, 'e_2', 24995)"
+      + " ORDER BY a, b, c, d, e, f LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 25000, "c_3", 0, "e_1", 25000);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT a, b, c, d, e, f FROM " + tableName
+      + " WHERE a = 'a_0' AND b >= 24995 AND (b, c, d) < (25010, 'c_6', 10)"
+      + " ORDER BY a, b, c, d, e, f LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 24995, "c_5", 95, "e_2", 24995);
+    assertRow(rs, "a_0", 25000, "c_3", 0, "e_1", 25000);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT a, b, c, d, e, f FROM " + tableName
+      + " WHERE a = 'a_0' AND b <= 25010 AND (b, c) > (24995, 'c_5')"
+      + " ORDER BY a ASC, b DESC, c DESC, d DESC, e DESC, f DESC LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 25010, "c_6", 10, "e_2", 25010);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertRow(rs, "a_0", 25000, "c_3", 0, "e_1", 25000);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT a, b, c, d, e, f FROM " + tableName
+      + " WHERE a = 'a_0' AND (b, c) <= (25005, 'c_1')" + " AND (b, c, d) > 
(24995, 'c_5', 95)"
+      + " ORDER BY a ASC, b DESC, c DESC, d DESC, e DESC, f DESC LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertRow(rs, "a_0", 25000, "c_3", 0, "e_1", 25000);
+    assertFalse(rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT a, b, c, d, e, f FROM " + tableName
+      + " WHERE a = 'a_0' AND b >= 24995 AND (b, c, d) < (25010, 'c_6', 10)"
+      + " ORDER BY a ASC, b DESC, c DESC, d DESC, e DESC, f DESC LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "a_0", 25005, "c_1", 5, "e_0", 25005);
+    assertRow(rs, "a_0", 25000, "c_3", 0, "e_1", 25000);
+    assertRow(rs, "a_0", 24995, "c_5", 95, "e_2", 24995);
+    assertFalse(rs.next());
+  }
+
+  @Test
+  public void testRVCWithBetweenOnIndex() throws Exception {
+    Connection conn = nextConnection(getUrl());
+    String tableName = generateUniqueName();
+    String indexName = generateUniqueName();
+
+    String ddl = "CREATE TABLE " + tableName + " (" + "pk VARCHAR NOT NULL, "
+      + "sk BIGINT NOT NULL, " + "category VARCHAR, " + "score DOUBLE, " + 
"data VARCHAR, "
+      + "CONSTRAINT table_pk PRIMARY KEY (pk, sk))";
+    conn.createStatement().execute(ddl);
+
+    conn = nextConnection(getUrl());
+    conn.createStatement()
+      .execute("CREATE INDEX " + indexName + " ON " + tableName + " (category, 
score)");
+
+    conn = nextConnection(getUrl());
+    PreparedStatement stmt = conn.prepareStatement(
+      "UPSERT INTO " + tableName + " (pk, sk, category, score, data) VALUES 
(?, ?, ?, ?, ?)");
+    for (int i = 0; i < 20000; i++) {
+      stmt.setString(1, "pk_" + (i % 100));
+      stmt.setLong(2, i);
+      stmt.setString(3, "category_" + (i % 10));
+      stmt.setDouble(4, i);
+      stmt.setString(5, "data_" + i);
+      stmt.execute();
+    }
+    conn.commit();
+
+    // category_7 rows have scores: 7, 17, 27, ..., 19997
+    // Scores in [2000, 8000]: 2007, 2017, ..., 7997
+    // With (score, pk, sk) > (7997, 'pk_97', 7997):
+    // score=7997 row is (pk_97, 7997) which is NOT > the RVC bound (exact 
match)
+    // Next category_7 score is 8007 which exceeds BETWEEN upper bound of 8000
+    conn = nextConnection(getUrl());
+    String query =
+      "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score " + "FROM "
+        + tableName + " " + "WHERE category = 'category_7' " + "AND score 
BETWEEN 2000 AND 8000 "
+        + "AND (score, pk, sk) > (7997, 'pk_97', 7997) "
+        + "ORDER BY category ASC, score ASC, pk ASC, sk ASC " + "LIMIT 100";
+    ResultSet rs = conn.createStatement().executeQuery(query);
+    assertFalse(rs.next());
+
+    // Verify that using >= instead of > includes the exact match row (7997, 
'pk_97', 7997).
+    conn = nextConnection(getUrl());
+    String queryGte =
+      "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score " + "FROM "
+        + tableName + " " + "WHERE category = 'category_7' " + "AND score 
BETWEEN 2000 AND 8000 "
+        + "AND (score, pk, sk) >= (7997, 'pk_97', 7997) "
+        + "ORDER BY category ASC, score ASC, pk ASC, sk ASC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(queryGte);
+    assertRow(rs, "pk_97", 7997, "category_7", 7997.0);
+    assertFalse("Expected only one row since next category_7 score (8007) 
exceeds "
+      + "BETWEEN upper bound of 8000.", rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_7' "
+      + "AND score BETWEEN 2000 AND 8000 " + "AND (score, pk, sk) > (7997, 
'pk_97', 7997) "
+      + "ORDER BY category ASC, score DESC, pk DESC, sk DESC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertFalse("Expected no rows with > RVC bound and DESC ordering", 
rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_7' "
+      + "AND score BETWEEN 2000 AND 8000 " + "AND (score, pk, sk) >= (7997, 
'pk_97', 7997) "
+      + "ORDER BY category ASC, score DESC, pk DESC, sk DESC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_97", 7997, "category_7", 7997.0);
+    assertFalse("Expected only one row with >= RVC bound and DESC ordering", 
rs.next());
+
+    // BETWEEN with RVC upper bound < (2037, 'pk_37', 2037).
+    // category_7 scores in [2000, 8000]: 2007, 2017, 2027, 2037, ...
+    // score=2007: (2007, 'pk_7', 2007) < (2037, 'pk_37', 2037)? Yes -> 
included
+    // score=2017: (2017, 'pk_17', 2017) < (2037, 'pk_37', 2037)? Yes -> 
included
+    // score=2027: (2027, 'pk_27', 2027) < (2037, 'pk_37', 2037)? Yes -> 
included
+    // score=2037: (2037, 'pk_37', 2037) < (2037, 'pk_37', 2037)? No (equal) 
-> excluded
+    // Expected 3 rows in ASC order.
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_7' "
+      + "AND score BETWEEN 2000 AND 8000 " + "AND (score, pk, sk) < (2037, 
'pk_37', 2037) "
+      + "ORDER BY category ASC, score ASC, pk ASC, sk ASC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_7", 2007, "category_7", 2007.0);
+    assertRow(rs, "pk_17", 2017, "category_7", 2017.0);
+    assertRow(rs, "pk_27", 2027, "category_7", 2027.0);
+    assertFalse("Expected exactly 3 rows with BETWEEN and RVC < (2037, 
'pk_37', 2037)", rs.next());
+
+    conn = nextConnection(getUrl());
+    query = "SELECT /*+ INDEX(" + tableName + " " + indexName + ") */ pk, sk, 
category, score "
+      + "FROM " + tableName + " " + "WHERE category = 'category_7' "
+      + "AND score BETWEEN 2000 AND 8000 " + "AND (score, pk, sk) < (2037, 
'pk_37', 2037) "
+      + "ORDER BY category ASC, score DESC, pk DESC, sk DESC " + "LIMIT 100";
+    rs = conn.createStatement().executeQuery(query);
+    assertRow(rs, "pk_27", 2027, "category_7", 2027.0);
+    assertRow(rs, "pk_17", 2017, "category_7", 2017.0);
+    assertRow(rs, "pk_7", 2007, "category_7", 2007.0);
+    assertFalse("Expected exactly 3 rows (DESC) with BETWEEN and RVC < (2037, 
'pk_37', 2037)",
+      rs.next());
+  }
+
+  private void assertRow(ResultSet rs, String pk, long sk, String category, 
double score)
+    throws SQLException {
+    assertTrue(rs.next());
+    assertEquals(pk, rs.getString("pk"));
+    assertEquals(sk, rs.getLong("sk"));
+    assertEquals(category, rs.getString("category"));
+    assertEquals(score, rs.getDouble("score"), 0.000);
+  }
+
+  private void assertRow(ResultSet rs, String a, long b, String c, int d, 
String e, long f)
+    throws SQLException {
+    assertTrue(rs.next());
+    assertEquals(a, rs.getString("a"));
+    assertEquals(b, rs.getLong("b"));
+    assertEquals(c, rs.getString("c"));
+    assertEquals(d, rs.getInt("d"));
+    assertEquals(e, rs.getString("e"));
+    assertEquals(f, rs.getLong("f"));
+  }
+
   private StringBuilder generateQueryToTest(int numItemsInClause, String 
fullViewName) {
     StringBuilder querySb =
       new StringBuilder("SELECT OBJECT_ID,OBJECT_DATA2,OBJECT_DATA FROM " + 
fullViewName);
diff --git 
a/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java 
b/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java
index 6ecfc7ff01..59bdac5ff6 100644
--- 
a/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java
+++ 
b/phoenix-core/src/test/java/org/apache/phoenix/compile/QueryCompilerTest.java
@@ -99,6 +99,7 @@ import org.apache.phoenix.schema.TableRef;
 import org.apache.phoenix.schema.types.PChar;
 import org.apache.phoenix.schema.types.PDecimal;
 import org.apache.phoenix.schema.types.PInteger;
+import org.apache.phoenix.schema.types.PLong;
 import org.apache.phoenix.schema.types.PVarchar;
 import org.apache.phoenix.util.ByteUtil;
 import org.apache.phoenix.util.EnvironmentEdgeManager;
@@ -7878,4 +7879,192 @@ public class QueryCompilerTest extends 
BaseConnectionlessQueryTest {
       
assertTrue(orderBy.getOrderByExpressions().get(3).toString().equals("VALID_CLICK_COUNT"));
     }
   }
+
+  @Test
+  public void testRVCScanBoundaries1() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.setAutoCommit(true);
+      conn.createStatement().execute(
+        "CREATE TABLE " + tableName + " (" + "category VARCHAR NOT NULL, score 
DECIMAL NOT NULL, "
+          + "pk VARCHAR NOT NULL, sk BIGINT NOT NULL, "
+          + "CONSTRAINT table_pk PRIMARY KEY (category, score, pk, sk))");
+
+      byte[] cat0 = PVarchar.INSTANCE.toBytes("category_0");
+      byte[] cat7 = PVarchar.INSTANCE.toBytes("category_7");
+      byte[] SEP = new byte[] { QueryConstants.SEPARATOR_BYTE };
+
+      byte[] dec4970 = PDecimal.INSTANCE.toBytes(new BigDecimal("4970"));
+      byte[] dec4980 = PDecimal.INSTANCE.toBytes(new BigDecimal("4980"));
+      byte[] dec4990 = PDecimal.INSTANCE.toBytes(new BigDecimal("4990"));
+      byte[] dec5000 = PDecimal.INSTANCE.toBytes(new BigDecimal("5000"));
+      byte[] dec5010 = PDecimal.INSTANCE.toBytes(new BigDecimal("5010"));
+      byte[] dec2000 = PDecimal.INSTANCE.toBytes(new BigDecimal("2000"));
+      byte[] dec2037 = PDecimal.INSTANCE.toBytes(new BigDecimal("2037"));
+      byte[] dec7997 = PDecimal.INSTANCE.toBytes(new BigDecimal("7997"));
+      byte[] dec8000 = PDecimal.INSTANCE.toBytes(new BigDecimal("8000"));
+
+      byte[] pk0 = PVarchar.INSTANCE.toBytes("pk_0");
+      byte[] pk10 = PVarchar.INSTANCE.toBytes("pk_10");
+      byte[] pk37 = PVarchar.INSTANCE.toBytes("pk_37");
+      byte[] pk70 = PVarchar.INSTANCE.toBytes("pk_70");
+      byte[] pk90 = PVarchar.INSTANCE.toBytes("pk_90");
+      byte[] pk97 = PVarchar.INSTANCE.toBytes("pk_97");
+      byte[] long4970 = PLong.INSTANCE.toBytes(4970L);
+      byte[] long4990 = PLong.INSTANCE.toBytes(4990L);
+      byte[] long5010 = PLong.INSTANCE.toBytes(5010L);
+      byte[] long7997 = PLong.INSTANCE.toBytes(7997L);
+      byte[] long2037 = PLong.INSTANCE.toBytes(2037L);
+
+      String sql = "SELECT * FROM " + tableName + " WHERE category = 
'category_0' AND score <= 5000"
+        + " AND (score, pk, sk) > (4990, 'pk_90', 4990)";
+      Scan scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(
+        ByteUtil.nextKey(ByteUtil.concat(cat0, SEP, dec4990, SEP, pk90, SEP, 
long4990)),
+        scan.getStartRow());
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(cat0, SEP, dec5000, 
SEP)),
+        scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName + " WHERE category = 'category_0' AND 
score < 5000"
+        + " AND (score, pk, sk) > (4990, 'pk_90', 4990)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(
+        ByteUtil.nextKey(ByteUtil.concat(cat0, SEP, dec4990, SEP, pk90, SEP, 
long4990)),
+        scan.getStartRow());
+      assertArrayEquals(ByteUtil.concat(cat0, SEP, dec5000), 
scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName + " WHERE category = 'category_0' AND 
score >= 4980"
+        + " AND (score, pk, sk) < (5010, 'pk_10', 5010)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(ByteUtil.concat(cat0, SEP, dec4980, SEP), 
scan.getStartRow());
+      assertArrayEquals(ByteUtil.concat(cat0, SEP, dec5010, SEP, pk10, SEP, 
long5010),
+        scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName + " WHERE category = 'category_0'"
+        + " AND (score, pk) <= (5000, 'pk_0')" + " AND (score, pk, sk) > 
(4970, 'pk_70', 4970)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(
+        ByteUtil.nextKey(ByteUtil.concat(cat0, SEP, dec4970, SEP, pk70, SEP, 
long4970)),
+        scan.getStartRow());
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(cat0, SEP, dec5000, 
SEP, pk0, SEP)),
+        scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName + " WHERE category = 'category_0'"
+        + " AND (score, pk) < (5000, 'pk_0')" + " AND (score, pk, sk) > (4970, 
'pk_70', 4970)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(
+        ByteUtil.nextKey(ByteUtil.concat(cat0, SEP, dec4970, SEP, pk70, SEP, 
long4970)),
+        scan.getStartRow());
+      assertArrayEquals(ByteUtil.concat(cat0, SEP, dec5000, SEP, pk0), 
scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName
+        + " WHERE category = 'category_7' AND score BETWEEN 2000 AND 8000"
+        + " AND (score, pk, sk) > (7997, 'pk_97', 7997)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(
+        ByteUtil.nextKey(ByteUtil.concat(cat7, SEP, dec7997, SEP, pk97, SEP, 
long7997)),
+        scan.getStartRow());
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(cat7, SEP, dec8000, 
SEP)),
+        scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName
+        + " WHERE category = 'category_7' AND score BETWEEN 2000 AND 8000"
+        + " AND (score, pk, sk) >= (7997, 'pk_97', 7997)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(ByteUtil.concat(cat7, SEP, dec7997, SEP, pk97, SEP, 
long7997),
+        scan.getStartRow());
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(cat7, SEP, dec8000, 
SEP)),
+        scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName
+        + " WHERE category = 'category_7' AND score BETWEEN 2000 AND 8000"
+        + " AND (score, pk, sk) < (2037, 'pk_37', 2037)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(ByteUtil.concat(cat7, SEP, dec2000, SEP), 
scan.getStartRow());
+      assertArrayEquals(ByteUtil.concat(cat7, SEP, dec2037, SEP, pk37, SEP, 
long2037),
+        scan.getStopRow());
+    }
+  }
+
+  @Test
+  public void testRVCScanBoundaries2() throws Exception {
+    String tableName = generateUniqueName();
+    try (Connection conn = DriverManager.getConnection(getUrl())) {
+      conn.setAutoCommit(true);
+      conn.createStatement()
+        .execute("CREATE TABLE " + tableName + " ("
+          + "a VARCHAR NOT NULL, b BIGINT NOT NULL, c VARCHAR NOT NULL, "
+          + "d INTEGER NOT NULL, e VARCHAR NOT NULL, f BIGINT NOT NULL, "
+          + "val VARCHAR, CONSTRAINT pk PRIMARY KEY (a, b, c, d, e, f))");
+
+      byte[] a0 = PVarchar.INSTANCE.toBytes("a_0");
+      byte[] SEP = new byte[] { QueryConstants.SEPARATOR_BYTE };
+      byte[] c5 = PVarchar.INSTANCE.toBytes("c_5");
+      byte[] c3 = PVarchar.INSTANCE.toBytes("c_3");
+      byte[] c1 = PVarchar.INSTANCE.toBytes("c_1");
+      byte[] c6 = PVarchar.INSTANCE.toBytes("c_6");
+      byte[] e1 = PVarchar.INSTANCE.toBytes("e_1");
+      byte[] e2 = PVarchar.INSTANCE.toBytes("e_2");
+
+      String sql = "SELECT * FROM " + tableName + " WHERE a = 'a_0' AND b >= 
24990 AND b <= 25010";
+      Scan scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(24990L)),
+        scan.getStartRow());
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(25010L))),
+        scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName
+        + " WHERE a = 'a_0' AND b <= 25010 AND (b, c) > (24995, 'c_5')";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(
+        ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(24995L), c5, SEP)),
+        scan.getStartRow());
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(25010L))),
+        scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName
+        + " WHERE a = 'a_0' AND b <= 25010 AND (b, c, d) > (25000, 'c_3', 0)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(25000L),
+        c3, SEP, PInteger.INSTANCE.toBytes(0))), scan.getStartRow());
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(25010L))),
+        scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName
+        + " WHERE a = 'a_0' AND b <= 25010 AND (b, c, d, e) > (25000, 'c_3', 
0, 'e_1')";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(25000L),
+        c3, SEP, PInteger.INSTANCE.toBytes(0), e1, SEP)), scan.getStartRow());
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(25010L))),
+        scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName + " WHERE a = 'a_0' AND (b, c) <= 
(25005, 'c_1')"
+        + " AND (b, c, d) > (24995, 'c_5', 95)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(24995L),
+        c5, SEP, PInteger.INSTANCE.toBytes(95))), scan.getStartRow());
+      assertArrayEquals(
+        ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(25005L), c1, SEP)),
+        scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName + " WHERE a = 'a_0' AND (b, c, d) <= 
(25005, 'c_1', 5)"
+        + " AND (b, c, d, e, f) > (24995, 'c_5', 95, 'e_2', 24995)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(
+        ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(24995L), c5, SEP,
+          PInteger.INSTANCE.toBytes(95), e2, SEP, 
PLong.INSTANCE.toBytes(24995L))),
+        scan.getStartRow());
+      assertArrayEquals(ByteUtil.nextKey(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(25005L),
+        c1, SEP, PInteger.INSTANCE.toBytes(5))), scan.getStopRow());
+
+      sql = "SELECT * FROM " + tableName
+        + " WHERE a = 'a_0' AND b >= 24995 AND (b, c, d) < (25010, 'c_6', 10)";
+      scan = TestUtil.getOptimizeQueryPlanNoIterator(conn, 
sql).getContext().getScan();
+      assertArrayEquals(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(24995L)),
+        scan.getStartRow());
+      assertArrayEquals(ByteUtil.concat(a0, SEP, 
PLong.INSTANCE.toBytes(25010L), c6, SEP,
+        PInteger.INSTANCE.toBytes(10)), scan.getStopRow());
+    }
+  }
+
 }


Reply via email to