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) {