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

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


The following commit(s) were added to refs/heads/main by this push:
     new 8861d05  Project PKs only when Select in Query/Scan is COUNT
8861d05 is described below

commit 8861d05d3e1a1be46e2734fb777c7b038fe80ac1
Author: Palash Chauhan <[email protected]>
AuthorDate: Tue Jun 23 09:57:17 2026 -0700

    Project PKs only when Select in Query/Scan is COUNT
---
 DDB_API_REFERENCE.md                               |   6 +-
 .../apache/phoenix/ddb/service/QueryService.java   |  43 ++++-
 .../apache/phoenix/ddb/service/ScanService.java    |  42 +++--
 .../apache/phoenix/ddb/service/utils/DQLUtils.java | 179 +++++++++++++++++----
 .../phoenix/ddb/service/utils/ScanConfig.java      |   8 +-
 .../java/org/apache/phoenix/ddb/Misc1Util.java     |  61 ++++---
 .../test/java/org/apache/phoenix/ddb/MiscIT.java   |   6 +
 .../test/java/org/apache/phoenix/ddb/QueryIT.java  | 107 ++++++++++++
 .../java/org/apache/phoenix/ddb/QueryIndex1IT.java |  59 +++++++
 .../java/org/apache/phoenix/ddb/TestUtils.java     |  89 ++++++++++
 .../phoenix/ddb/bson/BsonNumberConversionUtil.java |   2 +-
 11 files changed, 527 insertions(+), 75 deletions(-)

diff --git a/DDB_API_REFERENCE.md b/DDB_API_REFERENCE.md
index b32357b..cd22828 100644
--- a/DDB_API_REFERENCE.md
+++ b/DDB_API_REFERENCE.md
@@ -370,7 +370,9 @@ All list/query/scan operations support pagination:
 | BatchGetItem response size | 16 MB | BatchGetItem |
 | GetRecords response size | 1 MB | GetRecords |
 | Query result limit (max per page) | 100 items OR 1 MB, whichever comes first 
| Query |
+| Query result limit (max per page) when `Select=COUNT` | 300 rows; 1 MB byte 
cap does NOT apply | Query |
 | Scan result limit (max per page) | 100 items OR 1 MB, whichever comes first 
| Scan |
+| Scan result limit (max per page) when `Select=COUNT` | 300 rows; 1 MB byte 
cap does NOT apply | Scan |
 | GetRecords limit (max per page) | 50 records OR 1 MB, whichever comes first 
| GetRecords |
 | ListTables default limit | 100 tables | ListTables |
 | ListStreams default limit | 100 streams | ListStreams |
@@ -1225,7 +1227,7 @@ Retrieves items from a table or index based on primary 
key conditions. Items are
 | `FilterExpression` | String | No | Post-read filter |
 | `ProjectionExpression` | String | No | Attributes to return |
 | `Select` | String | No | What to return (see below) |
-| `Limit` | Integer | No | Max items to return (capped at 100 items OR 1 MB, 
whichever comes first) |
+| `Limit` | Integer | No | Max items to return (capped at 100 items OR 1 MB, 
whichever comes first; when `Select=COUNT` the per-page cap is 300 rows and the 
1 MB byte cap does NOT apply) |
 | `ScanIndexForward` | Boolean | No | `true` (default) = ASC, `false` = DESC |
 | `ExclusiveStartKey` | Map | No | Pagination cursor from previous response |
 | `KeyConditions` | Map | No | Legacy key conditions (*mutually exclusive with 
`KeyConditionExpression`*) |
@@ -1302,7 +1304,7 @@ Returns all items from a table or index (full table 
scan). Supports filtering an
 | `ExpressionAttributeValues` | Map | No | Value placeholders |
 | `ProjectionExpression` | String | No | Attributes to return |
 | `Select` | String | No | `ALL_ATTRIBUTES`, `SPECIFIC_ATTRIBUTES`, or `COUNT` 
|
-| `Limit` | Integer | No | Max items per page (capped at 100 items OR 1 MB, 
whichever comes first) |
+| `Limit` | Integer | No | Max items per page (capped at 100 items OR 1 MB, 
whichever comes first; when `Select=COUNT` the per-page cap is 300 rows and the 
1 MB byte cap does NOT apply) |
 | `ExclusiveStartKey` | Map | No | Pagination cursor |
 | `Segment` | Integer | No | Segment number for parallel scan |
 | `TotalSegments` | Integer | No | Total segments for parallel scan |
diff --git 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/QueryService.java
 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/QueryService.java
index c11a695..996e0ed 100644
--- 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/QueryService.java
+++ 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/QueryService.java
@@ -50,7 +50,13 @@ public class QueryService {
     public static final String SELECT_QUERY_WITH_INDEX_HINT =
             "SELECT /*+ INDEX(\"%s.%s\" \"%s\") */ COL FROM %s WHERE ";
 
+    public static final String COUNT_QUERY = "SELECT %s FROM %s WHERE ";
+    public static final String COUNT_QUERY_WITH_INDEX_HINT =
+            "SELECT /*+ INDEX(\"%s.%s\" \"%s\") */ %s FROM %s WHERE ";
+
     private static final int MAX_QUERY_LIMIT = 100;
+    /** Higher page cap when {@code Select=COUNT}: PK-only rows, byte cap not 
applied. */
+    private static final int MAX_COUNT_QUERY_LIMIT = 300;
 
     public static Map<String, Object> query(Map<String, Object> request, 
String connectionUrl) {
         ValidationUtil.validateQueryRequest(request);
@@ -107,15 +113,12 @@ public class QueryService {
         Map<String, Object> exprAttrValues =
                 (Map<String, Object>) 
request.get(ApiMetadata.EXPRESSION_ATTRIBUTE_VALUES);
         String keyCondExpr = (String) 
request.get(ApiMetadata.KEY_CONDITION_EXPRESSION);
+        boolean countOnly = 
ApiMetadata.SELECT_COUNT.equals(request.get(ApiMetadata.SELECT));
 
         // build SQL query
-        StringBuilder queryBuilder = StringUtils.isEmpty(indexName) ?
-                new StringBuilder(String.format(SELECT_QUERY,
-                        PhoenixUtils.getFullTableName(tableName, true))) :
-                new StringBuilder(
-                        String.format(SELECT_QUERY_WITH_INDEX_HINT, 
PhoenixUtils.SCHEMA_NAME,
-                                tableName, 
PhoenixUtils.getInternalIndexName(tableName, indexName),
-                                PhoenixUtils.getFullTableName(tableName, 
true)));
+        StringBuilder queryBuilder =
+                new StringBuilder(buildSelectClause(countOnly, useIndex, 
tableName, indexName,
+                        tablePKCols, indexPKCols));
 
         // parse Key Conditions
         KeyConditionsHolder keyConditions;
@@ -136,7 +139,8 @@ public class QueryService {
         DQLUtils.addFilterCondition(true, queryBuilder, (String) 
request.get(ApiMetadata.FILTER_EXPRESSION),
                 exprAttrNames, exprAttrValues);
         addOrderByClause(queryBuilder, useIndex, tablePKCols, indexPKCols, 
scanIndexForward);
-        DQLUtils.addLimit(queryBuilder, (Integer) 
request.get(ApiMetadata.LIMIT), MAX_QUERY_LIMIT);
+        DQLUtils.addLimit(queryBuilder, (Integer) 
request.get(ApiMetadata.LIMIT),
+                countOnly ? MAX_COUNT_QUERY_LIMIT : MAX_QUERY_LIMIT);
         LOGGER.debug("SELECT Query: " + queryBuilder);
 
         // Set values on the PreparedStatement
@@ -145,6 +149,29 @@ public class QueryService {
         return Pair.newPair(stmt, !useIndex && tablePKCols.size() == 1);
     }
 
+    /**
+     * On the count-only path the projection is PK columns only ; cheap on an 
UNCOVERED
+     * INDEX since no back-join to the data table is needed.
+     */
+    private static String buildSelectClause(boolean countOnly, boolean 
useIndex, String tableName,
+            String indexName, List<PColumn> tablePKCols, List<PColumn> 
indexPKCols) {
+        String fullTableName = PhoenixUtils.getFullTableName(tableName, true);
+        if (countOnly) {
+            String projection = DQLUtils.buildCountProjection(useIndex, 
tablePKCols, indexPKCols);
+            if (useIndex) {
+                return String.format(COUNT_QUERY_WITH_INDEX_HINT, 
PhoenixUtils.SCHEMA_NAME,
+                        tableName, 
PhoenixUtils.getInternalIndexName(tableName, indexName),
+                        projection, fullTableName);
+            }
+            return String.format(COUNT_QUERY, projection, fullTableName);
+        }
+        if (useIndex) {
+            return String.format(SELECT_QUERY_WITH_INDEX_HINT, 
PhoenixUtils.SCHEMA_NAME, tableName,
+                    PhoenixUtils.getInternalIndexName(tableName, indexName), 
fullTableName);
+        }
+        return String.format(SELECT_QUERY, fullTableName);
+    }
+
     private static void addOrderByClause(StringBuilder queryBuilder, boolean 
useIndex,
             List<PColumn> tablePKCols, List<PColumn> indexPKCols, boolean 
scanIndexForward) {
         List<PColumn> relevantPKCols = useIndex ? indexPKCols : tablePKCols;
diff --git 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/ScanService.java
 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/ScanService.java
index 2bbdb3f..036a25a 100644
--- 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/ScanService.java
+++ 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/ScanService.java
@@ -53,7 +53,13 @@ public class ScanService {
     private static final String SELECT_QUERY_WITH_INDEX_HINT =
             "SELECT /*+ INDEX(\"%s.%s\" \"%s\") */ COL FROM %s ";
 
+    private static final String COUNT_QUERY = "SELECT %s FROM %s ";
+    private static final String COUNT_QUERY_WITH_INDEX_HINT =
+            "SELECT /*+ INDEX(\"%s.%s\" \"%s\") */ %s FROM %s ";
+
     private static final int MAX_SCAN_LIMIT = 100;
+    /** See {@link QueryService} ; mirrors {@code MAX_COUNT_QUERY_LIMIT}. */
+    private static final int MAX_COUNT_SCAN_LIMIT = 300;
 
     public static Map<String, Object> scan(Map<String, Object> request, String 
connectionUrl) {
         ValidationUtil.validateScanRequest(request);
@@ -114,12 +120,12 @@ public class ScanService {
         return ScanType.WITH_EXCLUSIVE_START_KEY;
     }
 
-    /**
-     * Get effective limit, applying default and maximum constraints
-     */
     private static int getEffectiveLimit(Map<String, Object> request) {
         Integer requestLimit = (Integer) request.get(ApiMetadata.LIMIT);
-        return (requestLimit == null) ? MAX_SCAN_LIMIT : 
Math.min(requestLimit, MAX_SCAN_LIMIT);
+        int max = 
ApiMetadata.SELECT_COUNT.equals(request.get(ApiMetadata.SELECT))
+                ? MAX_COUNT_SCAN_LIMIT
+                : MAX_SCAN_LIMIT;
+        return (requestLimit == null) ? max : Math.min(requestLimit, max);
     }
 
     /**
@@ -153,18 +159,32 @@ public class ScanService {
     }
 
     /**
-     * Build the base SELECT clause with optional index hint
+     * Build the base SELECT clause with optional index hint. On the 
count-only path
+     * (including segment scans, which share this builder) the projection is 
PK columns
+     * only.
      */
     private static StringBuilder buildBaseSelectClause(ScanConfig config) {
         String fullTableName = 
PhoenixUtils.getFullTableName(config.getTableName(), true);
-        if (StringUtils.isEmpty(config.getIndexName())) {
+        boolean useIndex = !StringUtils.isEmpty(config.getIndexName());
+        if (config.isCountOnly()) {
+            String projection = DQLUtils.buildCountProjection(useIndex, 
config.getTablePKCols(),
+                    config.getIndexPKCols());
+            if (!useIndex) {
+                return new StringBuilder(String.format(COUNT_QUERY, 
projection, fullTableName));
+            }
+            String fullIndexName = 
PhoenixUtils.getInternalIndexName(config.getTableName(),
+                    config.getIndexName());
+            return new StringBuilder(String.format(COUNT_QUERY_WITH_INDEX_HINT,
+                    PhoenixUtils.SCHEMA_NAME, config.getTableName(), 
fullIndexName, projection,
+                    fullTableName));
+        }
+        if (!useIndex) {
             return new StringBuilder(String.format(SELECT_QUERY, 
fullTableName));
-        } else {
-            String fullIndexName = 
PhoenixUtils.getInternalIndexName(config.getTableName(), config.getIndexName());
-            return new StringBuilder(
-                    String.format(SELECT_QUERY_WITH_INDEX_HINT, 
PhoenixUtils.SCHEMA_NAME,
-                            config.getTableName(), fullIndexName, 
fullTableName));
         }
+        String fullIndexName = 
PhoenixUtils.getInternalIndexName(config.getTableName(),
+                config.getIndexName());
+        return new StringBuilder(String.format(SELECT_QUERY_WITH_INDEX_HINT,
+                PhoenixUtils.SCHEMA_NAME, config.getTableName(), 
fullIndexName, fullTableName));
     }
 
     /**
diff --git 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/DQLUtils.java
 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/DQLUtils.java
index de64a72..990395b 100644
--- 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/DQLUtils.java
+++ 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/DQLUtils.java
@@ -2,12 +2,14 @@ package org.apache.phoenix.ddb.service.utils;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.phoenix.ddb.bson.BsonDocumentToMap;
+import org.apache.phoenix.ddb.bson.BsonNumberConversionUtil;
 import org.apache.phoenix.ddb.service.exceptions.ValidationException;
 import org.apache.phoenix.ddb.utils.ApiMetadata;
 import org.apache.phoenix.ddb.utils.CommonServiceUtils;
 import org.apache.phoenix.ddb.utils.PhoenixUtils;
 import org.apache.phoenix.jdbc.PhoenixResultSet;
 import org.apache.phoenix.schema.PColumn;
+import org.apache.phoenix.schema.types.PDataType;
 
 import org.bson.BsonDocument;
 import org.bson.RawBsonDocument;
@@ -21,7 +23,6 @@ import java.util.Base64;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Properties;
 
 /**
  * Utility methods used for both Query and Scan API requests.
@@ -35,43 +36,23 @@ public class DQLUtils {
     private static final String RVC_4 = "(%s, %s, %s, %s) %s (?, ?, ?, ?)";
 
     /**
-     * Execute the given PreparedStatement, collect all returned items with 
projected attributes
-     * and return QueryResult or ScanResponse.
+     * Execute the given PreparedStatement and return a QueryResult / 
ScanResponse.
+     *
+     * <p>When {@code countOnly} is true the SQL must project PK columns only
+     * (see {@link #buildCountProjection}); the per-row {@code MAX_BYTES_SIZE} 
cap does
+     * not apply on that path ; row count is bounded by the SQL {@code LIMIT}.
      */
     public static Map<String, Object> 
executeStatementReturnResult(PreparedStatement stmt,
             List<String> projectionAttributes, boolean useIndex,
             List<PColumn> tablePKCols, List<PColumn> indexPKCols, String 
tableName,
             boolean isSingleRowExpected, boolean countOnly) throws 
SQLException {
-        int count = 0;
-        int bytesSize = 0;
-        List<Map<String, Object>> items = new ArrayList<>();
-        RawBsonDocument lastBsonDoc = null;
         try (ResultSet rs = stmt.executeQuery()) {
-            while (rs.next()) {
-                lastBsonDoc = (RawBsonDocument) rs.getObject(1);
-                Map<String, Object> item =
-                        BsonDocumentToMap.getProjectedItem(lastBsonDoc, 
projectionAttributes);
-                items.add(item);
-                count++;
-                bytesSize +=
-                        (int) 
rs.unwrap(PhoenixResultSet.class).getCurrentRow().getSerializedSize();
-                if (bytesSize >= ApiMetadata.MAX_BYTES_SIZE) {
-                    break;
-                }
-            }
-            Map<String, Object> lastKey = isSingleRowExpected ? null
-                    : DQLUtils.getKeyFromDoc(lastBsonDoc, useIndex, 
tablePKCols, indexPKCols);
-            int countRowsScanned = (int) PhoenixUtils.getRowsScanned(rs);
-            Map<String, Object> response = new HashMap<>();
-            if (!countOnly) {
-                response.put(ApiMetadata.ITEMS, items);
+            if (countOnly) {
+                return executeCountOnlyResult(rs, useIndex, tablePKCols, 
indexPKCols, tableName,
+                        isSingleRowExpected);
             }
-            response.put(ApiMetadata.COUNT, count);
-            response.put(ApiMetadata.LAST_EVALUATED_KEY, lastKey);
-            response.put(ApiMetadata.SCANNED_COUNT, countRowsScanned);
-            response.put(ApiMetadata.CONSUMED_CAPACITY,
-                    CommonServiceUtils.getConsumedCapacity(tableName));
-            return response;
+            return executeItemsResult(rs, projectionAttributes, useIndex, 
tablePKCols, indexPKCols,
+                    tableName, isSingleRowExpected);
         } catch (SQLException e) {
             if (e.getMessage() != null && e.getMessage()
                     .contains("BsonConditionInvalidArgumentException")) {
@@ -81,6 +62,142 @@ public class DQLUtils {
         }
     }
 
+    private static Map<String, Object> executeItemsResult(ResultSet rs,
+            List<String> projectionAttributes, boolean useIndex,
+            List<PColumn> tablePKCols, List<PColumn> indexPKCols, String 
tableName,
+            boolean isSingleRowExpected) throws SQLException {
+        int count = 0;
+        int bytesSize = 0;
+        List<Map<String, Object>> items = new ArrayList<>();
+        RawBsonDocument lastBsonDoc = null;
+        while (rs.next()) {
+            lastBsonDoc = (RawBsonDocument) rs.getObject(1);
+            Map<String, Object> item =
+                    BsonDocumentToMap.getProjectedItem(lastBsonDoc, 
projectionAttributes);
+            items.add(item);
+            count++;
+            bytesSize +=
+                    (int) 
rs.unwrap(PhoenixResultSet.class).getCurrentRow().getSerializedSize();
+            if (bytesSize >= ApiMetadata.MAX_BYTES_SIZE) {
+                break;
+            }
+        }
+        Map<String, Object> lastKey = isSingleRowExpected ? null
+                : DQLUtils.getKeyFromDoc(lastBsonDoc, useIndex, tablePKCols, 
indexPKCols);
+        Map<String, Object> response = buildResponseEnvelope(count,
+                (int) PhoenixUtils.getRowsScanned(rs), lastKey, tableName);
+        response.put(ApiMetadata.ITEMS, items);
+        return response;
+    }
+
+    private static Map<String, Object> executeCountOnlyResult(ResultSet rs, 
boolean useIndex,
+            List<PColumn> tablePKCols, List<PColumn> indexPKCols, String 
tableName,
+            boolean isSingleRowExpected) throws SQLException {
+        int count = 0;
+        Map<String, Object> lastKey = isSingleRowExpected ? null : new 
HashMap<>();
+        boolean haveLastRow = false;
+        while (rs.next()) {
+            count++;
+            if (lastKey != null) {
+                readKeyFromResultSet(rs, lastKey, useIndex, tablePKCols, 
indexPKCols);
+                haveLastRow = true;
+            }
+        }
+        return buildResponseEnvelope(count, (int) 
PhoenixUtils.getRowsScanned(rs),
+                haveLastRow ? lastKey : null, tableName);
+    }
+
+    private static Map<String, Object> buildResponseEnvelope(int count, int 
scannedCount,
+            Map<String, Object> lastKey, String tableName) {
+        Map<String, Object> response = new HashMap<>();
+        response.put(ApiMetadata.COUNT, count);
+        response.put(ApiMetadata.LAST_EVALUATED_KEY, lastKey);
+        response.put(ApiMetadata.SCANNED_COUNT, scannedCount);
+        response.put(ApiMetadata.CONSUMED_CAPACITY,
+                CommonServiceUtils.getConsumedCapacity(tableName));
+        return response;
+    }
+
+    /**
+     * Comma-separated list of PK columns (index PKs first when {@code 
useIndex}, then table
+     * PKs). Used as the SELECT projection on the count-only path: on an 
UNCOVERED INDEX this
+     * projection is fully covered by the index row key, so the server never 
back-joins to
+     * the data table.
+     */
+    public static String buildCountProjection(boolean useIndex, List<PColumn> 
tablePKCols,
+            List<PColumn> indexPKCols) {
+        StringBuilder sb = new StringBuilder();
+        boolean first = true;
+        if (useIndex && indexPKCols != null) {
+            for (PColumn pkCol : indexPKCols) {
+                if (!first) sb.append(", ");
+                sb.append(CommonServiceUtils.getColumnExprFromPCol(pkCol, 
true));
+                first = false;
+            }
+        }
+        for (PColumn pkCol : tablePKCols) {
+            if (!first) sb.append(", ");
+            
sb.append(CommonServiceUtils.getEscapedArgument(pkCol.getName().toString()));
+            first = false;
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Overwrite {@code dest} with the current row's PK values, index PKs 
first when
+     * {@code useIndex}, then table PKs.
+     */
+    private static void readKeyFromResultSet(ResultSet rs, Map<String, Object> 
dest,
+            boolean useIndex, List<PColumn> tablePKCols, List<PColumn> 
indexPKCols)
+            throws SQLException {
+        int col = 1;
+        if (useIndex && indexPKCols != null) {
+            for (PColumn pkCol : indexPKCols) {
+                String name =
+                        
CommonServiceUtils.getKeyNameFromBsonValueFunc(pkCol.getName().toString());
+                dest.put(name, attributeValueFromResultSetColumn(rs, col++, 
pkCol.getDataType()));
+            }
+        }
+        for (PColumn pkCol : tablePKCols) {
+            dest.put(pkCol.getName().toString(),
+                    attributeValueFromResultSetColumn(rs, col++, 
pkCol.getDataType()));
+        }
+    }
+
+    /** Convert a JDBC ResultSet column (typed by {@code PDataType}) into a 
DynamoDB-shaped
+     *  attribute map (one of {@code {S: <string>}}, {@code {N: <string>}}, 
{@code {B: <base64 string>}}). */
+    private static Map<String, Object> 
attributeValueFromResultSetColumn(ResultSet rs, int col,
+                                                                         
PDataType<?> dataType) throws SQLException {
+        Map<String, Object> attrVal = new HashMap<>();
+        switch (CommonServiceUtils.getScalarAttributeFromPDataType(dataType)) {
+            case S:
+                attrVal.put("S", rs.getString(col));
+                break;
+            case N:
+                // N PKs are always stored in a DOUBLE column (see 
CreateTableService) so we
+                // can't recover the original BSON sub-type. Pick the Number 
sub-type that gives
+                // items-path-compatible formatting: long form when integral, 
double form
+                // otherwise.
+                double d = rs.getDouble(col);
+                Number n;
+                if (!Double.isInfinite(d) && !Double.isNaN(d) && d == 
Math.floor(d)
+                        && d >= Long.MIN_VALUE && d <= Long.MAX_VALUE) {
+                    n = (long) d;
+                } else {
+                    n = d;
+                }
+                attrVal.put("N", BsonNumberConversionUtil.numberToString(n));
+                break;
+            case B:
+                attrVal.put("B", 
Base64.getEncoder().encodeToString(rs.getBytes(col)));
+                break;
+            default:
+                throw new IllegalStateException(
+                        "Unsupported PK data type for count-only 
LastEvaluatedKey: " + dataType);
+        }
+        return attrVal;
+    }
+
     /**
      * Return the attribute value map with only the primary keys from the 
given bson document.
      * Return both data and index table keys when querying index table.
diff --git 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/ScanConfig.java
 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/ScanConfig.java
index d9c8656..63a5343 100644
--- 
a/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/ScanConfig.java
+++ 
b/phoenix-ddb-rest/src/main/java/org/apache/phoenix/ddb/service/utils/ScanConfig.java
@@ -106,11 +106,13 @@ public class ScanConfig {
 
     @Override
     public String toString() {
+        StringBuilder sb = new StringBuilder(this.type.toString());
         if (isSegmentScan) {
-            return this.type.toString() + "," + 
this.scanSegmentInfo.toShortString();
-        } else {
-            return this.type.toString();
+            sb.append(",").append(this.scanSegmentInfo.toShortString());
         }
+        sb.append(",countOnly=").append(this.countOnly);
+        sb.append(",limit=").append(this.limit);
+        return sb.toString();
     }
 }
 
diff --git 
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/Misc1Util.java 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/Misc1Util.java
index 4a022b8..f24c6e0 100644
--- a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/Misc1Util.java
+++ b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/Misc1Util.java
@@ -837,6 +837,14 @@ public class Misc1Util {
     public static void test3(DynamoDbClient dynamoDbClient, DynamoDbClient 
phoenixDBClientV2,
             DynamoDbStreamsClient dynamoDbStreamsClient,
             DynamoDbStreamsClient phoenixDBStreamsClientV2) throws 
InterruptedException {
+        test3(dynamoDbClient, phoenixDBClientV2, dynamoDbStreamsClient, 
phoenixDBStreamsClientV2,
+                false);
+    }
+
+    public static void test3(DynamoDbClient dynamoDbClient, DynamoDbClient 
phoenixDBClientV2,
+            DynamoDbStreamsClient dynamoDbStreamsClient,
+            DynamoDbStreamsClient phoenixDBStreamsClientV2, boolean useCount)
+            throws InterruptedException {
 
         String table1Name = "test.highVolume_table1";
         String table2Name = "test.highVolume_table2";
@@ -881,21 +889,24 @@ public class Misc1Util {
                     .keyConditionExpression("pk = 
:pkval").expressionAttributeValues(
                             Collections.singletonMap(":pkval",
                                     
AttributeValue.builder().s(pkValueTable1).build()));
-            TestUtils.compareQueryOutputs(queryBuilder1, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(queryBuilder1, phoenixDBClientV2, dynamoDbClient, 
useCount);
 
             LOGGER.info("Querying and comparing partition {} for table2", 
pkValueTable2);
             QueryRequest.Builder queryBuilder2 = 
QueryRequest.builder().tableName(table2Name)
                     .keyConditionExpression("pk = 
:pkval").expressionAttributeValues(
                             Collections.singletonMap(":pkval",
                                     
AttributeValue.builder().s(pkValueTable2).build()));
-            TestUtils.compareQueryOutputs(queryBuilder2, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(queryBuilder2, phoenixDBClientV2, dynamoDbClient, 
useCount);
         }
 
-        queryGSIsWithoutConditions(dynamoDbClient, phoenixDBClientV2, 
table1Name, "_t1");
-        queryGSIsWithSortKeyConditions(dynamoDbClient, phoenixDBClientV2, 
table2Name, "_t2");
+        queryGSIsWithoutConditions(dynamoDbClient, phoenixDBClientV2, 
table1Name, "_t1", useCount);
+        queryGSIsWithSortKeyConditions(dynamoDbClient, phoenixDBClientV2, 
table2Name, "_t2",
+                useCount);
 
-        queryLSIsWithSortKeyConditions(dynamoDbClient, phoenixDBClientV2, 
table1Name, "_t1");
-        queryLSIsWithSortKeyConditions(dynamoDbClient, phoenixDBClientV2, 
table2Name, "_t2");
+        queryLSIsWithSortKeyConditions(dynamoDbClient, phoenixDBClientV2, 
table1Name, "_t1",
+                useCount);
+        queryLSIsWithSortKeyConditions(dynamoDbClient, phoenixDBClientV2, 
table2Name, "_t2",
+                useCount);
 
         TestUtils.compareAllStreamRecords(table1Name, dynamoDbStreamsClient,
                 phoenixDBStreamsClientV2);
@@ -908,6 +919,15 @@ public class Misc1Util {
         
phoenixDBClientV2.deleteTable(DeleteTableRequest.builder().tableName(table2Name).build());
     }
 
+    private static void runCompareQuery(QueryRequest.Builder qr, 
DynamoDbClient phoenix,
+            DynamoDbClient dynamo, boolean useCount) {
+        if (useCount) {
+            TestUtils.compareQueryCountOutputs(qr, phoenix, dynamo);
+        } else {
+            TestUtils.compareQueryOutputs(qr, phoenix, dynamo);
+        }
+    }
+
     private static CreateTableRequest createHighVolumeTableRequest(String 
tableName) {
         return 
CreateTableRequest.builder().tableName(tableName).attributeDefinitions(
                         AttributeDefinition.builder().attributeName("pk")
@@ -1173,7 +1193,8 @@ public class Misc1Util {
     }
 
     private static void queryGSIsWithoutConditions(DynamoDbClient 
dynamoDbClient,
-            DynamoDbClient phoenixDBClientV2, String tableName, String 
tableSuffix) {
+            DynamoDbClient phoenixDBClientV2, String tableName, String 
tableSuffix,
+            boolean useCount) {
 
         // Query GSI 1 (gsi1_pk values: gsi1_0 to gsi1_9)
         for (int i = 0; i < 10; i++) {
@@ -1183,7 +1204,7 @@ public class Misc1Util {
                             .keyConditionExpression("gsi1_pk = 
:gsi1val").expressionAttributeValues(
                                     Collections.singletonMap(":gsi1val",
                                             
AttributeValue.builder().s(gsi1Value).build()));
-            TestUtils.compareQueryOutputs(queryBuilder, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(queryBuilder, phoenixDBClientV2, dynamoDbClient, 
useCount);
         }
 
         // Query GSI 2 (gsi2_pk values: 0 to 19 for table1, 100 to 119 for 
table2)
@@ -1195,7 +1216,7 @@ public class Misc1Util {
                                     Collections.singletonMap(":gsi2val",
                                             
AttributeValue.builder().n(String.valueOf(i + gsi2Offset))
                                                     .build()));
-            TestUtils.compareQueryOutputs(queryBuilder, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(queryBuilder, phoenixDBClientV2, dynamoDbClient, 
useCount);
         }
 
         // Query GSI 3 (gsi3_pk values: gsi3_cat_0 to gsi3_cat_4)
@@ -1206,12 +1227,13 @@ public class Misc1Util {
                             .keyConditionExpression("gsi3_pk = 
:gsi3val").expressionAttributeValues(
                                     Collections.singletonMap(":gsi3val",
                                             
AttributeValue.builder().s(gsi3Value).build()));
-            TestUtils.compareQueryOutputs(queryBuilder, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(queryBuilder, phoenixDBClientV2, dynamoDbClient, 
useCount);
         }
     }
 
     private static void queryGSIsWithSortKeyConditions(DynamoDbClient 
dynamoDbClient,
-            DynamoDbClient phoenixDBClientV2, String tableName, String 
tableSuffix) {
+            DynamoDbClient phoenixDBClientV2, String tableName, String 
tableSuffix,
+            boolean useCount) {
 
         // Sort key offset: 0 for table1, 1000000 for table2
         int sortKeyOffset = tableSuffix.equals("_t1") ? 0 : 1000000;
@@ -1230,7 +1252,7 @@ public class Misc1Util {
                     
QueryRequest.builder().tableName(tableName).indexName("gsi_index_1")
                             .keyConditionExpression("gsi1_pk = :gsi1val AND 
gsi1_sk > :skval")
                             .expressionAttributeValues(expressionValues);
-            TestUtils.compareQueryOutputs(queryBuilder, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(queryBuilder, phoenixDBClientV2, dynamoDbClient, 
useCount);
         }
 
         // GSI 2 offset: 0 for table1, 100 for table2
@@ -1253,7 +1275,7 @@ public class Misc1Util {
                             .keyConditionExpression(
                                     "gsi2_pk = :gsi2val AND gsi2_sk BETWEEN 
:lower AND :upper")
                             .expressionAttributeValues(expressionValues);
-            TestUtils.compareQueryOutputs(queryBuilder, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(queryBuilder, phoenixDBClientV2, dynamoDbClient, 
useCount);
         }
 
         for (int i = 0; i < 5; i++) {
@@ -1270,12 +1292,13 @@ public class Misc1Util {
                     
QueryRequest.builder().tableName(tableName).indexName("gsi_index_3")
                             .keyConditionExpression("gsi3_pk = :gsi3val AND 
gsi3_sk < :skval")
                             .expressionAttributeValues(expressionValues);
-            TestUtils.compareQueryOutputs(queryBuilder, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(queryBuilder, phoenixDBClientV2, dynamoDbClient, 
useCount);
         }
     }
 
     private static void queryLSIsWithSortKeyConditions(DynamoDbClient 
dynamoDbClient,
-            DynamoDbClient phoenixDBClientV2, String tableName, String 
tableSuffix) {
+            DynamoDbClient phoenixDBClientV2, String tableName, String 
tableSuffix,
+            boolean useCount) {
 
         int sortKeyOffset = tableSuffix.equals("_t1") ? 0 : 1000000;
 
@@ -1294,7 +1317,7 @@ public class Misc1Util {
                     
QueryRequest.builder().tableName(tableName).indexName("lsi_index_1")
                             .keyConditionExpression("pk = :pkval AND lsi1_sk > 
:skval")
                             .expressionAttributeValues(lsi1Expr);
-            TestUtils.compareQueryOutputs(lsi1QueryBuilder, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(lsi1QueryBuilder, phoenixDBClientV2, 
dynamoDbClient, useCount);
 
             // LSI 2: Query with string sort key BETWEEN condition
             // lsi2_sk format: "item_XXXXX_t1" or "item_XXXXX_t2"
@@ -1310,7 +1333,7 @@ public class Misc1Util {
                             .keyConditionExpression(
                                     "pk = :pkval AND lsi2_sk BETWEEN :lower 
AND :upper")
                             .expressionAttributeValues(lsi2Expr);
-            TestUtils.compareQueryOutputs(lsi2QueryBuilder, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(lsi2QueryBuilder, phoenixDBClientV2, 
dynamoDbClient, useCount);
 
             // LSI 3: Query with timestamp-based sort key < condition
             // lsi3_sk = currentTime + (sortKeyId * 1000) + timestampOffset
@@ -1328,14 +1351,14 @@ public class Misc1Util {
                     
QueryRequest.builder().tableName(tableName).indexName("lsi_index_3")
                             .keyConditionExpression("pk = :pkval AND lsi3_sk 
<= :skval")
                             .expressionAttributeValues(lsi3Expr);
-            TestUtils.compareQueryOutputs(lsi3QueryBuilder, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(lsi3QueryBuilder, phoenixDBClientV2, 
dynamoDbClient, useCount);
 
             QueryRequest.Builder lsi4QueryBuilder =
                     
QueryRequest.builder().tableName(tableName).indexName("lsi_index_3")
                             .scanIndexForward(false)
                             .keyConditionExpression("pk = :pkval AND lsi3_sk < 
:skval")
                             .expressionAttributeValues(lsi3Expr);
-            TestUtils.compareQueryOutputs(lsi4QueryBuilder, phoenixDBClientV2, 
dynamoDbClient);
+            runCompareQuery(lsi4QueryBuilder, phoenixDBClientV2, 
dynamoDbClient, useCount);
         }
     }
 
diff --git a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/MiscIT.java 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/MiscIT.java
index 343e49c..8b65381 100644
--- a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/MiscIT.java
+++ b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/MiscIT.java
@@ -132,6 +132,12 @@ public class MiscIT {
                 phoenixDBStreamsClientV2);
     }
 
+    @Test(timeout = 600000)
+    public void testMixWorkflows4WithCount() throws Exception {
+        Misc1Util.test3(dynamoDbClient, phoenixDBClientV2, 
dynamoDbStreamsClient,
+                phoenixDBStreamsClientV2, true);
+    }
+
     @Test(timeout = 120000)
     public void testMixWorkflows3() throws Exception {
         final String tableName = "tests";
diff --git a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/QueryIT.java 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/QueryIT.java
index d18491e..f2ed134 100644
--- a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/QueryIT.java
+++ b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/QueryIT.java
@@ -621,6 +621,113 @@ public class QueryIT {
         Assert.assertEquals(3, totalCount);
     }
 
+    @Test(timeout = 120000)
+    public void testQuerySingleHashKeyCount() throws Exception {
+        final String tableName = testName.getMethodName();
+        CreateTableRequest createTableRequest =
+                DDLTestUtils.getCreateTableRequest(tableName, "attr_0",
+                        ScalarAttributeType.S, null, null);
+        phoenixDBClientV2.createTable(createTableRequest);
+        dynamoDbClient.createTable(createTableRequest);
+
+        PutItemRequest putItemRequest1 = 
PutItemRequest.builder().tableName(tableName)
+                .item(getItem1()).build();
+        phoenixDBClientV2.putItem(putItemRequest1);
+        dynamoDbClient.putItem(putItemRequest1);
+
+        QueryRequest.Builder qr = QueryRequest.builder().tableName(tableName);
+        qr.keyConditionExpression("#0 = :v0");
+        Map<String, String> exprAttrNames = new HashMap<>();
+        exprAttrNames.put("#0", "attr_0");
+        qr.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":v0", AttributeValue.builder().s("A").build());
+        qr.expressionAttributeValues(exprAttrVal);
+        qr.select("COUNT");
+
+        QueryResponse phoenixResult = phoenixDBClientV2.query(qr.build());
+        QueryResponse dynamoResult = dynamoDbClient.query(qr.build());
+        Assert.assertEquals(dynamoResult.count(), phoenixResult.count());
+        Assert.assertEquals(1, phoenixResult.count().intValue());
+        Assert.assertTrue(phoenixResult.items().isEmpty());
+        Assert.assertTrue(dynamoResult.items().isEmpty());
+        Assert.assertEquals(dynamoResult.scannedCount(), 
phoenixResult.scannedCount());
+        // Single-row-expected path must not emit a cursor.
+        Assert.assertTrue(phoenixResult.lastEvaluatedKey().isEmpty());
+        Assert.assertEquals(dynamoResult.lastEvaluatedKey(), 
phoenixResult.lastEvaluatedKey());
+    }
+
+    @Test(timeout = 240000)
+    public void testQueryNTypedPKBoundaryParity() throws Exception {
+        // Stress-test N-typed PK cursor format across byte/short/int/long 
boundaries
+        // (both signs), up to 2^53 (last exact integer representable as 
double), plus
+        // a sprinkle of non-integral decimals. Every page's lastEvaluatedKey 
must
+        // match the DynamoDB SDK's exactly,  the cursor-parity assertion 
catches
+        // any regression in the integer-detection branch of 
attributeValueFromColumn
+        // (N case), e.g. emitting "1.0" where DynamoDB emits "1".
+        final String tableName = testName.getMethodName();
+        CreateTableRequest createTableRequest =
+                DDLTestUtils.getCreateTableRequest(tableName, "pk", 
ScalarAttributeType.S,
+                        "sk", ScalarAttributeType.N);
+        phoenixDBClientV2.createTable(createTableRequest);
+        dynamoDbClient.createTable(createTableRequest);
+
+        String[] testValues = {
+                "0",
+                "1", "-1", "2", "-2",
+                "127", "-127", "128", "-128", "129", "-129",     // byte 
boundary
+                "255", "-255", "256", "-256", "257", "-257",
+                "1024", "-1024", "1025", "-1025",
+                "32767", "-32767", "32768", "-32768",            // short 
boundary
+                "65535", "-65535",
+                "1000000", "-1000000",
+                "2147483647", "-2147483647",                     // int 
boundary
+                "2147483648", "-2147483648",
+                "9007199254740991", "-9007199254740991",         // just under 
2^53
+                "9007199254740992", "-9007199254740992",         // 2^53 (last 
exact int)
+                // non-integral (BsonDouble-shaped on items path)
+                "0.5", "-0.5",
+                "1.5", "-1.5",
+                "126.5", "-126.5",
+                "256.34", "-256.34",
+                "258.9", "-258.9",
+                "1024.5", "-1024.5",
+                "1.234567890123", "-1.234567890123",
+        };
+
+        for (String v : testValues) {
+            Map<String, AttributeValue> item = new HashMap<>();
+            item.put("pk", AttributeValue.builder().s("A").build());
+            item.put("sk", AttributeValue.builder().n(v).build());
+            PutItemRequest putRequest =
+                    
PutItemRequest.builder().tableName(tableName).item(item).build();
+            phoenixDBClientV2.putItem(putRequest);
+            dynamoDbClient.putItem(putRequest);
+        }
+
+        Map<String, String> exprAttrNames = new HashMap<>();
+        exprAttrNames.put("#0", "pk");
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":v0", AttributeValue.builder().s("A").build());
+
+        // Count path: exercises the new count-only inversion in DQLUtils.
+        QueryRequest.Builder countQr = 
QueryRequest.builder().tableName(tableName)
+                .keyConditionExpression("#0 = :v0")
+                .expressionAttributeNames(exprAttrNames)
+                .expressionAttributeValues(exprAttrVal)
+                .limit(1); // one page = one row + one cursor
+        TestUtils.compareQueryCountOutputsPerPage(countQr, phoenixDBClientV2, 
dynamoDbClient);
+
+        // Items path: exercises the BSON-doc inversion (getKeyFromDoc) across 
the
+        // same value set.
+        QueryRequest.Builder itemsQr = 
QueryRequest.builder().tableName(tableName)
+                .keyConditionExpression("#0 = :v0")
+                .expressionAttributeNames(exprAttrNames)
+                .expressionAttributeValues(exprAttrVal)
+                .limit(1);
+        TestUtils.compareQueryOutputsPerPage(itemsQr, phoenixDBClientV2, 
dynamoDbClient);
+    }
+
     @Test(timeout = 120000)
     public void querySelectAllAttributesTest() throws Exception {
         //create table
diff --git 
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/QueryIndex1IT.java 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/QueryIndex1IT.java
index fb86f92..e44f3a4 100644
--- a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/QueryIndex1IT.java
+++ b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/QueryIndex1IT.java
@@ -439,6 +439,65 @@ public class QueryIndex1IT {
         TestUtils.validateIndexUsed(qr.build(), url);
     }
 
+    @Test(timeout = 120000)
+    public void testQueryIndexSelectCountWithNonPKFilter() throws SQLException 
{
+        // create table with keys [attr_0]
+        final String tableName = testName.getMethodName();
+        final String indexName = "IDX_" + tableName;
+        CreateTableRequest createTableRequest =
+                DDLTestUtils.getCreateTableRequest(tableName, "attr_0",
+                        ScalarAttributeType.S, null, null);
+        // create index on Id3, IdS
+        createTableRequest = DDLTestUtils.addIndexToRequest(true, 
createTableRequest, indexName, "Id3",
+                ScalarAttributeType.S, "IdS", ScalarAttributeType.S);
+        phoenixDBClientV2.createTable(createTableRequest);
+        dynamoDbClient.createTable(createTableRequest);
+
+        //put items
+        PutItemRequest putItemRequest1 = 
PutItemRequest.builder().tableName(tableName).item(getItem1()).build();
+        PutItemRequest putItemRequest2 = 
PutItemRequest.builder().tableName(tableName).item(getItem2()).build();
+        PutItemRequest putItemRequest3 = 
PutItemRequest.builder().tableName(tableName).item(getItem3()).build();
+        PutItemRequest putItemRequest4 = 
PutItemRequest.builder().tableName(tableName).item(getItem4()).build();
+        phoenixDBClientV2.putItem(putItemRequest1);
+        phoenixDBClientV2.putItem(putItemRequest2);
+        phoenixDBClientV2.putItem(putItemRequest3);
+        phoenixDBClientV2.putItem(putItemRequest4);
+        dynamoDbClient.putItem(putItemRequest1);
+        dynamoDbClient.putItem(putItemRequest2);
+        dynamoDbClient.putItem(putItemRequest3);
+        dynamoDbClient.putItem(putItemRequest4);
+
+        // Query the GSI for Id3="foo" (matches items 1 + 2) with a 
FilterExpression on
+        // Id2 (non-PK and not on the index). Id2 forces a server-side 
BSON_CONDITION_EXPRESSION
+        // evaluation against COL even though the gateway projects only PK 
columns on the
+        // count path. Filter keeps only item 1 (Id2=1.1); item 2 (Id2=2.2) is 
filtered out.
+        QueryRequest.Builder qr = QueryRequest.builder().tableName(tableName);
+        qr.indexName(indexName);
+        qr.keyConditionExpression("#0 = :v0");
+        qr.filterExpression("#1 < :v1");
+        Map<String, String> exprAttrNames = new HashMap<>();
+        exprAttrNames.put("#0", "Id3");
+        exprAttrNames.put("#1", "Id2");
+        qr.expressionAttributeNames(exprAttrNames);
+        Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+        exprAttrVal.put(":v0", AttributeValue.builder().s("foo").build());
+        exprAttrVal.put(":v1", AttributeValue.builder().n("2.0").build());
+        qr.expressionAttributeValues(exprAttrVal);
+        qr.select("COUNT");
+        TestUtils.waitForEventualConsistentIndex();
+
+        QueryResponse phoenixResult = phoenixDBClientV2.query(qr.build());
+        QueryResponse dynamoResult = dynamoDbClient.query(qr.build());
+        Assert.assertEquals(dynamoResult.count(), phoenixResult.count());
+        Assert.assertEquals(1, phoenixResult.count().intValue());
+        Assert.assertTrue(phoenixResult.items().isEmpty());
+        Assert.assertTrue(dynamoResult.items().isEmpty());
+        Assert.assertEquals(dynamoResult.scannedCount(), 
phoenixResult.scannedCount());
+
+        // explain plan
+        TestUtils.validateIndexUsed(qr.build(), url);
+    }
+
     @Test(timeout = 120000)
     public void testQueryIndexSelectCountWithPagination() throws SQLException {
         // create table with keys [attr_0]
diff --git 
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/TestUtils.java 
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/TestUtils.java
index dcd0bb7..1bdc281 100644
--- a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/TestUtils.java
+++ b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/TestUtils.java
@@ -465,6 +465,95 @@ public class TestUtils {
         Assert.assertTrue(ItemComparator.areItemsEqual(ddbResult, 
phoenixResult));
     }
 
+    /**
+     * Paginated parity comparison for {@code Select=COUNT} Query: sets {@code 
Select=COUNT}
+     * on the builder, drains both Phoenix and DynamoDB, and asserts the 
summed {@code count}
+     * matches.
+     */
+    public static void compareQueryCountOutputs(QueryRequest.Builder qr,
+            DynamoDbClient phoenixDBClientV2, DynamoDbClient dynamoDbClient) {
+        qr.select("COUNT");
+        int phoenixCount = 0;
+        QueryResponse phoenixResponse;
+        do {
+            phoenixResponse = phoenixDBClientV2.query(qr.build());
+            phoenixCount += phoenixResponse.count();
+            qr.exclusiveStartKey(phoenixResponse.lastEvaluatedKey());
+        } while (phoenixResponse.hasLastEvaluatedKey());
+
+        qr.exclusiveStartKey(null);
+        int ddbCount = 0;
+        QueryResponse ddbResponse;
+        do {
+            ddbResponse = dynamoDbClient.query(qr.build());
+            ddbCount += ddbResponse.count();
+            qr.exclusiveStartKey(ddbResponse.lastEvaluatedKey());
+        } while (ddbResponse.hasLastEvaluatedKey());
+
+        Assert.assertEquals("count mismatch", ddbCount, phoenixCount);
+    }
+
+    /**
+     * Paginated parity comparison asserting {@code items}, {@code count}, and
+     * {@code lastEvaluatedKey} match between Phoenix and DynamoDB at every 
page,
+     * plus a final cumulative-count check. Stricter than
+     * {@link #compareQueryOutputs} which only compares cumulative end-state,
+     * use this for stress / cursor-format regression tests.
+     */
+    public static void compareQueryOutputsPerPage(QueryRequest.Builder qr,
+            DynamoDbClient phoenixDBClientV2, DynamoDbClient dynamoDbClient) {
+        int page = 0;
+        int phoenixTotal = 0;
+        int ddbTotal = 0;
+        QueryResponse phoenixResponse;
+        QueryResponse ddbResponse;
+        do {
+            phoenixResponse = phoenixDBClientV2.query(qr.build());
+            ddbResponse = dynamoDbClient.query(qr.build());
+            Assert.assertEquals("items mismatch at page " + page,
+                    ddbResponse.items(), phoenixResponse.items());
+            Assert.assertEquals("count mismatch at page " + page,
+                    ddbResponse.count(), phoenixResponse.count());
+            Assert.assertEquals("cursor mismatch at page " + page,
+                    ddbResponse.lastEvaluatedKey(), 
phoenixResponse.lastEvaluatedKey());
+            phoenixTotal += phoenixResponse.count();
+            ddbTotal += ddbResponse.count();
+            qr.exclusiveStartKey(phoenixResponse.lastEvaluatedKey());
+            page++;
+        } while (phoenixResponse.lastEvaluatedKey() != null
+                && !phoenixResponse.lastEvaluatedKey().isEmpty());
+        Assert.assertEquals("total count mismatch", ddbTotal, phoenixTotal);
+    }
+
+    /**
+     * Paginated {@code Select=COUNT} variant of {@link 
#compareQueryOutputsPerPage}.
+     * Sets {@code Select=COUNT} on the builder; asserts per-page {@code 
count} +
+     * {@code lastEvaluatedKey} parity and final cumulative-count parity.
+     */
+    public static void compareQueryCountOutputsPerPage(QueryRequest.Builder qr,
+            DynamoDbClient phoenixDBClientV2, DynamoDbClient dynamoDbClient) {
+        qr.select("COUNT");
+        int page = 0;
+        int phoenixTotal = 0;
+        int ddbTotal = 0;
+        QueryResponse phoenixResponse;
+        QueryResponse ddbResponse;
+        do {
+            phoenixResponse = phoenixDBClientV2.query(qr.build());
+            ddbResponse = dynamoDbClient.query(qr.build());
+            Assert.assertEquals("count mismatch at page " + page,
+                    ddbResponse.count(), phoenixResponse.count());
+            Assert.assertEquals("cursor mismatch at page " + page,
+                    ddbResponse.lastEvaluatedKey(), 
phoenixResponse.lastEvaluatedKey());
+            phoenixTotal += phoenixResponse.count();
+            ddbTotal += ddbResponse.count();
+            qr.exclusiveStartKey(phoenixResponse.lastEvaluatedKey());
+            page++;
+        } while (phoenixResponse.lastEvaluatedKey() != null
+                && !phoenixResponse.lastEvaluatedKey().isEmpty());
+        Assert.assertEquals("total count mismatch", ddbTotal, phoenixTotal);
+    }
+
     public static void compareScanOutputs(ScanRequest.Builder sr,
             DynamoDbClient phoenixDBClientV2, DynamoDbClient dynamoDbClient,
             String partitionKeyName, String sortKeyName, ScalarAttributeType 
partitionKeyType,
diff --git 
a/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/bson/BsonNumberConversionUtil.java
 
b/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/bson/BsonNumberConversionUtil.java
index b859832..204a5a5 100644
--- 
a/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/bson/BsonNumberConversionUtil.java
+++ 
b/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/bson/BsonNumberConversionUtil.java
@@ -56,7 +56,7 @@ public final class BsonNumberConversionUtil {
      * @param number The Number object.
      * @return String represented number value.
      */
-    static String numberToString(Number number) {
+    public static String numberToString(Number number) {
         if (number instanceof Integer || number instanceof Short || number 
instanceof Byte) {
             return Integer.toString(number.intValue());
         } else if (number instanceof Long) {

Reply via email to