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

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


The following commit(s) were added to refs/heads/5.3 by this push:
     new 96323c53a3 PHOENIX-8010 NullPointerException in getOffsetScanner for 
point-lookup queries with OFFSET (#2626)
96323c53a3 is described below

commit 96323c53a32d85433b2dee9954709bc2a9fcb8a4
Author: Xavier Fernandis <[email protected]>
AuthorDate: Tue Oct 13 21:38:19 2026 +0530

    PHOENIX-8010 NullPointerException in getOffsetScanner for point-lookup 
queries with OFFSET (#2626)
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../iterate/NonAggregateRegionScannerFactory.java  | 10 ++++--
 .../apache/phoenix/end2end/QueryWithOffsetIT.java  | 40 ++++++++++++++++++++++
 2 files changed, 48 insertions(+), 2 deletions(-)

diff --git 
a/phoenix-core-server/src/main/java/org/apache/phoenix/iterate/NonAggregateRegionScannerFactory.java
 
b/phoenix-core-server/src/main/java/org/apache/phoenix/iterate/NonAggregateRegionScannerFactory.java
index 65affd6e79..946e95821f 100644
--- 
a/phoenix-core-server/src/main/java/org/apache/phoenix/iterate/NonAggregateRegionScannerFactory.java
+++ 
b/phoenix-core-server/src/main/java/org/apache/phoenix/iterate/NonAggregateRegionScannerFactory.java
@@ -389,9 +389,15 @@ public class NonAggregateRegionScannerFactory extends 
RegionScannerFactory {
     // offset. However, if the region moves after valid row was returned, we 
do need to
     // set row count to offset because we return valid row only after offset 
num of rows
     // are skipped.
+    // prevScanStartRowKey is null on the very first scan RPC for scans that 
do not go
+    // through ScanRanges.intersectScan(), e.g. the point-lookup fast path in
+    // BaseResultIterators. Such scans are single-RPC and never a mid-scan 
continuation,
+    // so no row-count recovery is needed. Guarding against null also avoids a 
NPE from
+    // ByteUtil.concat(null, ...) below (see PHOENIX-8010).
     if (
-      Bytes.compareTo(prevScanStartRowKey, initStartRowKey) != 0 && Bytes
-        .compareTo(ByteUtil.concat(prevScanStartRowKey, ByteUtil.ZERO_BYTE), 
initStartRowKey) != 0
+      prevScanStartRowKey != null && Bytes.compareTo(prevScanStartRowKey, 
initStartRowKey) != 0
+        && Bytes.compareTo(ByteUtil.concat(prevScanStartRowKey, 
ByteUtil.ZERO_BYTE),
+          initStartRowKey) != 0
     ) {
       iterator.setRowCountToOffset();
     }
diff --git 
a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithOffsetIT.java 
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithOffsetIT.java
index 65ec34ff08..aee144740e 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithOffsetIT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/QueryWithOffsetIT.java
@@ -213,6 +213,46 @@ public class QueryWithOffsetIT extends 
ParallelStatsDisabledIT {
     conn.close();
   }
 
+  @Test
+  public void testPointLookupWithOffset() throws SQLException {
+    Properties props = PropertiesUtil.deepCopy(TEST_PROPERTIES);
+    try (Connection conn = DriverManager.getConnection(getUrl(), props)) {
+      createTestTable(getUrl(), ddl);
+      initTableValues(conn);
+      updateStatistics(conn);
+      // Point lookup (full primary key pinned) combined with OFFSET. Prior to
+      // PHOENIX-8010 this threw a server-side NullPointerException from
+      // ByteUtil.concat(null, ...) in 
NonAggregateRegionScannerFactory.getOffsetScanner,
+      // because the point-lookup fast path never sets the 
SCAN_ACTUAL_START_ROW scan
+      // attribute, leaving it null on the first (and only) scan RPC.
+      // The single matching row must be returned for OFFSET 0.
+      String pointLookupOffsetZero = "SELECT t_id, k1, k2 FROM " + tableName
+        + " WHERE t_id = 'c' AND k1 = 2 AND k2 = 3 LIMIT 5 OFFSET 0";
+      try (ResultSet rs = 
conn.createStatement().executeQuery(pointLookupOffsetZero)) {
+        assertTrue(rs.next());
+        assertEquals("c", rs.getString(1));
+        assertEquals(2, rs.getInt(2));
+        assertEquals(3, rs.getInt(3));
+        assertFalse(rs.next());
+      }
+      // A non-zero OFFSET skips the only matching row, yielding an empty 
result set
+      // (and must still not crash).
+      String pointLookupOffsetOne = "SELECT t_id FROM " + tableName
+        + " WHERE t_id = 'c' AND k1 = 2 AND k2 = 3 LIMIT 5 OFFSET 1";
+      try (ResultSet rs = 
conn.createStatement().executeQuery(pointLookupOffsetOne)) {
+        assertFalse(rs.next());
+      }
+      // The reverse-scan variant exercises the same crash site.
+      String pointLookupReversed = "SELECT t_id FROM " + tableName
+        + " WHERE t_id = 'c' AND k1 = 2 AND k2 = 3 ORDER BY t_id DESC LIMIT 5 
OFFSET 0";
+      try (ResultSet rs = 
conn.createStatement().executeQuery(pointLookupReversed)) {
+        assertTrue(rs.next());
+        assertEquals("c", rs.getString(1));
+        assertFalse(rs.next());
+      }
+    }
+  }
+
   @Test
   public void testMetaDataWithOffset() throws SQLException {
     Connection conn;

Reply via email to