This is an automated email from the ASF dual-hosted git repository.
vjasani 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 09a5b6a Use ANTLR grammar to evaluate KeyConditionExpression in query
09a5b6a is described below
commit 09a5b6adfb6106232f45292ee718a1f94d53c596
Author: Rahul Kumar <[email protected]>
AuthorDate: Wed Jan 28 23:50:09 2026 +0530
Use ANTLR grammar to evaluate KeyConditionExpression in query
---
.../apache/phoenix/ddb/service/QueryService.java | 9 +-
.../phoenix/ddb/KeyConditionsHolderTest.java | 240 ++++++++++++++++++--
.../test/java/org/apache/phoenix/ddb/QueryIT.java | 38 ++++
.../java/org/apache/phoenix/ddb/QueryIndex1IT.java | 100 +++++++++
.../phoenix/ddb/utils/KeyConditionsHolder.java | 248 +++++++++++++++++----
5 files changed, 582 insertions(+), 53 deletions(-)
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 141a289..13bc8e2 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
@@ -120,8 +120,13 @@ public class QueryService {
PhoenixUtils.getFullTableName(tableName,
true)));
// parse Key Conditions
- KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames,
- useIndex ? indexPKCols : tablePKCols, useIndex);
+ KeyConditionsHolder keyConditions;
+ try {
+ keyConditions = new KeyConditionsHolder(keyCondExpr, exprAttrNames,
+ useIndex ? indexPKCols : tablePKCols, useIndex);
+ } catch (RuntimeException e) {
+ throw new ValidationException(e.getMessage());
+ }
PColumn sortKeyPKCol = keyConditions.getSortKeyPKCol();
// append all conditions for WHERE clause
diff --git
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/KeyConditionsHolderTest.java
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/KeyConditionsHolderTest.java
index a714274..51d479a 100644
---
a/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/KeyConditionsHolderTest.java
+++
b/phoenix-ddb-rest/src/test/java/org/apache/phoenix/ddb/KeyConditionsHolderTest.java
@@ -18,21 +18,53 @@
package org.apache.phoenix.ddb;
import org.apache.phoenix.ddb.utils.KeyConditionsHolder;
+import org.apache.phoenix.schema.PColumn;
+import org.apache.phoenix.schema.PName;
+import org.apache.phoenix.schema.PNameFactory;
import org.junit.Test;
import org.junit.Assert;
+import org.mockito.Mockito;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
public class KeyConditionsHolderTest {
private Map<String, String> exprAttrNames = new HashMap<>();
+ private List<PColumn> createPkCols(String partitionKey) {
+ List<PColumn> pkCols = new ArrayList<>();
+ PColumn mockColumn = Mockito.mock(PColumn.class);
+ PName mockName = PNameFactory.newName(partitionKey);
+ Mockito.when(mockColumn.getName()).thenReturn(mockName);
+ pkCols.add(mockColumn);
+ return pkCols;
+ }
+
+ private List<PColumn> createPkColsWithSortKey(String partitionKey, String
sortKey) {
+ List<PColumn> pkCols = new ArrayList<>();
+ PColumn mockPartitionColumn = Mockito.mock(PColumn.class);
+ PName mockPartitionName = PNameFactory.newName(partitionKey);
+
Mockito.when(mockPartitionColumn.getName()).thenReturn(mockPartitionName);
+ pkCols.add(mockPartitionColumn);
+
+ PColumn mockSortColumn = Mockito.mock(PColumn.class);
+ PName mockSortName = PNameFactory.newName(sortKey);
+ Mockito.when(mockSortColumn.getName()).thenReturn(mockSortName);
+ pkCols.add(mockSortColumn);
+
+ return pkCols;
+ }
+
+
@Test(timeout = 120000)
public void test1() {
String keyCondExpr = "#0 = :0";
exprAttrNames.put("#0", "hashKey");
- KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames);
+ List<PColumn> pkCols = createPkCols("hashKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertFalse(keyConditions.hasSortKey());
}
@@ -42,7 +74,8 @@ public class KeyConditionsHolderTest {
String keyCondExpr = "#0 = :0 AND #1 = :1";
exprAttrNames.put("#0", "hashKey");
exprAttrNames.put("#1", "sortKey");
- KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames);
+ List<PColumn> pkCols = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
@@ -55,7 +88,8 @@ public class KeyConditionsHolderTest {
String keyCondExpr = "#0 = :0 AND #1 > :1";
exprAttrNames.put("#0", "hashKey");
exprAttrNames.put("#1", "sortKey");
- KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames);
+ List<PColumn> pkCols = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
@@ -68,7 +102,8 @@ public class KeyConditionsHolderTest {
String keyCondExpr = "#0 = :0 AND #1 < :1";
exprAttrNames.put("#0", "hashKey");
exprAttrNames.put("#1", "sortKey");
- KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames);
+ List<PColumn> pkCols = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
@@ -81,7 +116,8 @@ public class KeyConditionsHolderTest {
String keyCondExpr = "#0 = :0 AND #1 >= :1";
exprAttrNames.put("#0", "hashKey");
exprAttrNames.put("#1", "sortKey");
- KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames);
+ List<PColumn> pkCols = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
@@ -94,7 +130,8 @@ public class KeyConditionsHolderTest {
String keyCondExpr = "#0 = :0 AND #1 <= :1";
exprAttrNames.put("#0", "hashKey");
exprAttrNames.put("#1", "sortKey");
- KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames);
+ List<PColumn> pkCols = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
@@ -107,7 +144,8 @@ public class KeyConditionsHolderTest {
String keyCondExpr1 = "#0 = :0 AND #1 BETWEEN :1 AND :2";
exprAttrNames.put("#0", "hashKey");
exprAttrNames.put("#1", "sortKey");
- KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr1, exprAttrNames);
+ List<PColumn> pkCols1 = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr1, exprAttrNames, pkCols1, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
@@ -119,7 +157,8 @@ public class KeyConditionsHolderTest {
String keyCondExpr2 = "#hashKey = :hashKey AND #sortKey BETWEEN
:sortKeyFrom AND :sortKeyTo";
exprAttrNames.put("#hashKey", "Id");
exprAttrNames.put("#sortKey", "Title");
- keyConditions = new KeyConditionsHolder(keyCondExpr2, exprAttrNames);
+ List<PColumn> pkCols = createPkCols("Id");
+ keyConditions = new KeyConditionsHolder(keyCondExpr2, exprAttrNames,
pkCols, false);
Assert.assertEquals("Id", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("Title", keyConditions.getSortKeyName());
@@ -134,7 +173,8 @@ public class KeyConditionsHolderTest {
String keyCondExpr = "#0 = :0 AND begins_with(#1, :1)";
exprAttrNames.put("#0", "hashKey");
exprAttrNames.put("#1", "sortKey");
- KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames);
+ List<PColumn> pkCols = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
@@ -146,7 +186,8 @@ public class KeyConditionsHolderTest {
public void test9() {
exprAttrNames = null;
String keyCondExpr1 = "hashKey=:0 AND sortKey>:1";
- KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr1, exprAttrNames);
+ List<PColumn> pkCols1 = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr1, exprAttrNames, pkCols1, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
@@ -154,7 +195,8 @@ public class KeyConditionsHolderTest {
Assert.assertEquals(":1", keyConditions.getSortKeyValue1());
String keyCondExpr2 = "hashKey= :0 AND sortKey >:1";
- keyConditions = new KeyConditionsHolder(keyCondExpr2, exprAttrNames);
+ List<PColumn> pkCols2 = createPkColsWithSortKey("hashKey", "sortKey");
+ keyConditions = new KeyConditionsHolder(keyCondExpr2, exprAttrNames,
pkCols2, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
@@ -162,8 +204,8 @@ public class KeyConditionsHolderTest {
Assert.assertEquals(":1", keyConditions.getSortKeyValue1());
String keyCondExpr3 = "hashKey =:0 AND sortKey> :1";
-
- keyConditions = new KeyConditionsHolder(keyCondExpr3, exprAttrNames);
+ List<PColumn> pkCols3 = createPkColsWithSortKey("hashKey", "sortKey");
+ keyConditions = new KeyConditionsHolder(keyCondExpr3, exprAttrNames,
pkCols3, false);
Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
Assert.assertTrue(keyConditions.hasSortKey());
Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
@@ -172,4 +214,176 @@ public class KeyConditionsHolderTest {
exprAttrNames = new HashMap<>();
}
+
+ @Test(timeout = 120000)
+ public void testSortKeyBeforePartitionKey() {
+ // Test that order of conditions doesn't matter - sort key can come
before partition key
+ String keyCondExpr = "#sk > :v1 AND #pk = :v0";
+ exprAttrNames.put("#pk", "hashKey");
+ exprAttrNames.put("#sk", "sortKey");
+ List<PColumn> pkCols = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
+
+ // Verify partition key is still recognized
+ Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
+ Assert.assertEquals(":v0", keyConditions.getPartitionValue());
+
+ // Verify sort key is still recognized
+ Assert.assertTrue(keyConditions.hasSortKey());
+ Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
+ Assert.assertEquals(">", keyConditions.getSortKeyOperator());
+ Assert.assertEquals(":v1", keyConditions.getSortKeyValue1());
+ }
+
+ @Test(timeout = 120000)
+ public void testSortKeyWithEqualBeforePartitionKey() {
+ // Test that sort key using = operator and coming first is not
mistaken for partition key
+ String keyCondExpr = "#sk = :v1 AND #pk = :v0";
+ exprAttrNames.put("#pk", "hashKey");
+ exprAttrNames.put("#sk", "sortKey");
+ List<PColumn> pkCols = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
+
+ // Verify partition key is still recognized
+ Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
+ Assert.assertEquals(":v0", keyConditions.getPartitionValue());
+
+ // Verify sort key is still recognized
+ Assert.assertTrue(keyConditions.hasSortKey());
+ Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
+ Assert.assertEquals("=", keyConditions.getSortKeyOperator());
+ Assert.assertEquals(":v1", keyConditions.getSortKeyValue1());
+ }
+
+ @Test(timeout = 120000)
+ public void testSortKeyBetweenBeforePartitionKey() {
+ // Test that BETWEEN condition for sort key can come before partition
key
+ String keyCondExpr = "#sk BETWEEN :v3 AND :v4 AND #pk = :v1";
+ exprAttrNames.put("#pk", "hashKey");
+ exprAttrNames.put("#sk", "sortKey");
+ List<PColumn> pkCols = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
+
+ // Verify partition key is still recognized
+ Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
+ Assert.assertEquals(":v1", keyConditions.getPartitionValue());
+
+ // Verify sort key BETWEEN condition is correctly parsed
+ Assert.assertTrue(keyConditions.hasSortKey());
+ Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
+ Assert.assertTrue(keyConditions.hasBetween());
+ Assert.assertEquals("BETWEEN", keyConditions.getSortKeyOperator());
+ Assert.assertEquals(":v3", keyConditions.getSortKeyValue1());
+ Assert.assertEquals(":v4", keyConditions.getSortKeyValue2());
+ }
+
+ @Test(timeout = 120000)
+ public void testBeginsWithBeforePartitionKey() {
+ // Test that begins_with condition for sort key can come before
partition key
+ String keyCondExpr = "begins_with(#sk, :v2) AND #pk = :v1";
+ exprAttrNames.put("#pk", "hashKey");
+ exprAttrNames.put("#sk", "sortKey");
+ List<PColumn> pkCols = createPkColsWithSortKey("hashKey", "sortKey");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
+
+ // Verify partition key is still recognized
+ Assert.assertEquals("hashKey", keyConditions.getPartitionKeyName());
+ Assert.assertEquals(":v1", keyConditions.getPartitionValue());
+
+ // Verify sort key begins_with condition is correctly parsed
+ Assert.assertTrue(keyConditions.hasSortKey());
+ Assert.assertEquals("sortKey", keyConditions.getSortKeyName());
+ Assert.assertTrue(keyConditions.hasBeginsWith());
+ Assert.assertEquals(":v2", keyConditions.getBeginsWithSortKeyVal());
+ }
+
+
+ @Test(timeout = 120000, expected = IllegalArgumentException.class)
+ public void testNullPkCols() {
+ // Test that null pkCols throws IllegalArgumentException
+ String keyCondExpr = "#0 = :0";
+ exprAttrNames.put("#0", "hashKey");
+ new KeyConditionsHolder(keyCondExpr, exprAttrNames, null, false);
+ }
+
+ @Test(timeout = 120000, expected = IllegalArgumentException.class)
+ public void testEmptyPkCols() {
+ // Test that empty pkCols throws IllegalArgumentException
+ String keyCondExpr = "#0 = :0";
+ exprAttrNames.put("#0", "hashKey");
+ List<PColumn> pkCols = new ArrayList<>();
+ new KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
+ }
+
+ @Test(timeout = 120000, expected = IllegalArgumentException.class)
+ public void testMoreThanTwoPkCols() {
+ // Test that more than 2 pkCols throws IllegalArgumentException
+ String keyCondExpr = "#0 = :0";
+ exprAttrNames.put("#0", "hashKey");
+ List<PColumn> pkCols = new ArrayList<>();
+
+ PColumn mockColumn1 = Mockito.mock(PColumn.class);
+ PName mockName1 = PNameFactory.newName("col1");
+ Mockito.when(mockColumn1.getName()).thenReturn(mockName1);
+ pkCols.add(mockColumn1);
+
+ PColumn mockColumn2 = Mockito.mock(PColumn.class);
+ PName mockName2 = PNameFactory.newName("col2");
+ Mockito.when(mockColumn2.getName()).thenReturn(mockName2);
+ pkCols.add(mockColumn2);
+
+ PColumn mockColumn3 = Mockito.mock(PColumn.class);
+ PName mockName3 = PNameFactory.newName("col3");
+ Mockito.when(mockColumn3.getName()).thenReturn(mockName3);
+ pkCols.add(mockColumn3);
+
+ new KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
+ }
+
+ @Test(timeout = 120000)
+ public void testSinglePkColWithOnlyPartitionKey() {
+ // Test that single pkCol works correctly with partition key only
+ String keyCondExpr = "#pk = :v0";
+ exprAttrNames.put("#pk", "userId");
+ List<PColumn> pkCols = createPkCols("userId");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
+
+ Assert.assertEquals("userId", keyConditions.getPartitionKeyName());
+ Assert.assertEquals(":v0", keyConditions.getPartitionValue());
+ Assert.assertFalse(keyConditions.hasSortKey());
+ Assert.assertNull(keyConditions.getSortKeyName());
+ }
+
+ @Test(timeout = 120000)
+ public void testTwoPkColsWithBothKeys() {
+ // Test that two pkCols works correctly with both partition and sort
keys
+ String keyCondExpr = "#pk = :v0 AND #sk > :v1";
+ exprAttrNames.put("#pk", "userId");
+ exprAttrNames.put("#sk", "timestamp");
+ List<PColumn> pkCols = createPkColsWithSortKey("userId", "timestamp");
+ KeyConditionsHolder keyConditions = new
KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
+
+ Assert.assertEquals("userId", keyConditions.getPartitionKeyName());
+ Assert.assertEquals(":v0", keyConditions.getPartitionValue());
+ Assert.assertTrue(keyConditions.hasSortKey());
+ Assert.assertEquals("timestamp", keyConditions.getSortKeyName());
+ Assert.assertEquals(">", keyConditions.getSortKeyOperator());
+ Assert.assertEquals(":v1", keyConditions.getSortKeyValue1());
+ }
+
+ @Test(timeout = 120000)
+ public void testFailedKeyConditionExpressionWithContains() {
+ String keyCondExpr = "#pk = :v0 AND contains(#sk, :v1)";
+ exprAttrNames.put("#pk", "userId");
+ exprAttrNames.put("#sk", "tags");
+ List<PColumn> pkCols = createPkColsWithSortKey("userId", "tags");
+ try {
+ new KeyConditionsHolder(keyCondExpr, exprAttrNames, pkCols, false);
+ Assert.fail("Invalid KeyConditionExpression expected");
+ } catch (RuntimeException e) {
+ Assert.assertEquals("Invalid KeyConditionExpression:
DocumentFieldContainsParseNode",
+ e.getMessage());
+
+ }
+ }
}
\ No newline at end of file
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 8ed4582..953d97a 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
@@ -916,6 +916,44 @@ public class QueryIT {
return item;
}
+ @Test(timeout = 120000)
+ public void testInvalidNotEqualOperatorInKeyCondition() throws Exception {
+ // Create table
+ final String tableName = testName.getMethodName();
+ CreateTableRequest createTableRequest =
+ DDLTestUtils.getCreateTableRequest(tableName, "attr_0",
+ ScalarAttributeType.S, "attr_1",
ScalarAttributeType.N);
+ phoenixDBClientV2.createTable(createTableRequest);
+ dynamoDbClient.createTable(createTableRequest);
+
+ // Query request with <> operator in key condition
+ 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("B").build());
+ qr.expressionAttributeValues(exprAttrVal);
+
+ // Verify that both DynamoDB and Phoenix return 400
+ try {
+ dynamoDbClient.query(qr.build());
+ Assert.fail("Expected DynamoDbException for DynamoDB");
+ } catch (DynamoDbException e) {
+ Assert.assertEquals(400, e.statusCode());
+ Assert.assertTrue(e.getMessage().contains("Invalid operator used
in KeyConditionExpression: <>"));
+ }
+
+ try {
+ phoenixDBClientV2.query(qr.build());
+ Assert.fail("Expected DynamoDbException for Phoenix");
+ } catch (DynamoDbException e) {
+ Assert.assertEquals(400, e.statusCode());
+ Assert.assertTrue(e.getMessage().contains("Invalid operator used
in KeyConditionExpression:"));
+ }
+ }
+
public static Map<String, AttributeValue> getItem4() {
Map<String, AttributeValue> item = new HashMap<>();
item.put("attr_0", AttributeValue.builder().s("B").build());
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 65810a3..6538999 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
@@ -566,4 +566,104 @@ public class QueryIndex1IT {
item.put("Id3", AttributeValue.builder().s("bar").build());
return item;
}
+
+ @Test(timeout = 120000)
+ public void testGlobalIndexSortKeyBeforePartitionKey() throws SQLException
{
+ // create table with keys [attr_0]
+ final String tableName = testName.getMethodName();
+ final String indexName = "G_IDX_" + tableName;
+ CreateTableRequest createTableRequest =
+ DDLTestUtils.getCreateTableRequest(tableName, "attr_0",
+ ScalarAttributeType.S, null, null);
+ // create index on IdS, Id2
+ createTableRequest = DDLTestUtils.addIndexToRequest(true,
createTableRequest, indexName, "IdS",
+ ScalarAttributeType.S, "Id2", ScalarAttributeType.N);
+ 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();
+ phoenixDBClientV2.putItem(putItemRequest1);
+ phoenixDBClientV2.putItem(putItemRequest2);
+ dynamoDbClient.putItem(putItemRequest1);
+ dynamoDbClient.putItem(putItemRequest2);
+
+ //query request using index with sort key condition before partition
key
+ QueryRequest.Builder qr = QueryRequest.builder().tableName(tableName);
+ qr.indexName(indexName);
+ qr.keyConditionExpression("#1 < :v1 AND #0 = :v0");
+ Map<String, String> exprAttrNames = new HashMap<>();
+ exprAttrNames.put("#0", "IdS");
+ exprAttrNames.put("#1", "Id2");
+ qr.expressionAttributeNames(exprAttrNames);
+ Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+ exprAttrVal.put(":v0", AttributeValue.builder().s("101.01").build());
+ exprAttrVal.put(":v1", AttributeValue.builder().n("2.1").build());
+ qr.expressionAttributeValues(exprAttrVal);
+
+ // query result
+ QueryResponse phoenixResult = phoenixDBClientV2.query(qr.build());
+ QueryResponse dynamoResult = dynamoDbClient.query(qr.build());
+ Assert.assertEquals(dynamoResult.count(), phoenixResult.count());
+ Assert.assertEquals(dynamoResult.items(), phoenixResult.items());
+ Assert.assertEquals(dynamoResult.scannedCount(),
phoenixResult.scannedCount());
+
+ // check last evaluated key
+ Assert.assertTrue(dynamoResult.lastEvaluatedKey().isEmpty());
+ Assert.assertTrue(phoenixResult.lastEvaluatedKey().isEmpty());
+
+ // explain plan
+ TestUtils.validateIndexUsed(qr.build(), url);
+ }
+
+ @Test(timeout = 120000)
+ public void testGlobalIndexBeginsWithBeforePartitionKey() throws
SQLException {
+ // create table with keys [attr_0]
+ final String tableName = testName.getMethodName();
+ final String indexName = "G_IDX_" + tableName;
+ CreateTableRequest createTableRequest =
+ DDLTestUtils.getCreateTableRequest(tableName, "attr_0",
+ ScalarAttributeType.S, null, null);
+ // create index on IdS, Id3
+ createTableRequest = DDLTestUtils.addIndexToRequest(true,
createTableRequest, indexName, "IdS",
+ ScalarAttributeType.S, "Id3", 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();
+ phoenixDBClientV2.putItem(putItemRequest1);
+ phoenixDBClientV2.putItem(putItemRequest2);
+ dynamoDbClient.putItem(putItemRequest1);
+ dynamoDbClient.putItem(putItemRequest2);
+
+ //query request using index with begins_with before partition key
+ QueryRequest.Builder qr = QueryRequest.builder().tableName(tableName);
+ qr.indexName(indexName);
+ qr.keyConditionExpression("begins_with(#1, :v1) AND #0 = :v0");
+ Map<String, String> exprAttrNames = new HashMap<>();
+ exprAttrNames.put("#0", "IdS");
+ exprAttrNames.put("#1", "Id3");
+ qr.expressionAttributeNames(exprAttrNames);
+ Map<String, AttributeValue> exprAttrVal = new HashMap<>();
+ exprAttrVal.put(":v0", AttributeValue.builder().s("101.01").build());
+ exprAttrVal.put(":v1", AttributeValue.builder().s("fo").build());
+ qr.expressionAttributeValues(exprAttrVal);
+
+ // query result
+ QueryResponse phoenixResult = phoenixDBClientV2.query(qr.build());
+ QueryResponse dynamoResult = dynamoDbClient.query(qr.build());
+ Assert.assertEquals(dynamoResult.count(), phoenixResult.count());
+ Assert.assertEquals(dynamoResult.items(), phoenixResult.items());
+ Assert.assertEquals(dynamoResult.scannedCount(),
phoenixResult.scannedCount());
+
+ // check last evaluated key
+ Assert.assertTrue(dynamoResult.lastEvaluatedKey().isEmpty());
+ Assert.assertTrue(phoenixResult.lastEvaluatedKey().isEmpty());
+
+ // explain plan
+ TestUtils.validateIndexUsed(qr.build(), url);
+ }
}
diff --git
a/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/utils/KeyConditionsHolder.java
b/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/utils/KeyConditionsHolder.java
index d983090..bc8b23b 100644
---
a/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/utils/KeyConditionsHolder.java
+++
b/phoenix-ddb-utils/src/main/java/org/apache/phoenix/ddb/utils/KeyConditionsHolder.java
@@ -1,17 +1,25 @@
package org.apache.phoenix.ddb.utils;
+import org.apache.phoenix.parse.AndParseNode;
+import org.apache.phoenix.parse.BetweenParseNode;
+import org.apache.phoenix.parse.BsonExpressionParser;
+import org.apache.phoenix.parse.ComparisonParseNode;
+import org.apache.phoenix.parse.LiteralParseNode;
+import org.apache.phoenix.parse.ParseNode;
+import org.apache.phoenix.parse.DocumentFieldBeginsWithParseNode;
import org.apache.phoenix.schema.PColumn;
import org.apache.phoenix.schema.types.PVarbinaryEncoded;
import org.apache.phoenix.schema.types.PVarchar;
+import java.sql.SQLException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import java.util.List;
/**
- * Helper class to parse KeyConditionExpression provided in a DynamoDB request:
+ * Helper class to parse KeyConditionExpression provided in request:
* partitionKeyName = :partitionkeyval AND SortKeyCondition
*
* SortKeyCondition can be either sortKeyName op :sortkeyval
@@ -23,10 +31,8 @@ import java.util.regex.Pattern;
*/
public class KeyConditionsHolder {
- private static final String KEY_REGEX =
-
"([#\\w]+)\\s*=\\s*(:\\w+)(?:\\s+AND\\s+(?:begins_with\\s*\\(\\s*([#\\w]+)\\s*,\\s*(:\\w+)\\s*\\)|([#\\w]+)\\s*(=|>|<|<=|>=|BETWEEN)\\s*(:\\w+)(?:\\s+AND\\s*(:\\w+))?))?";
+ private static final Logger LOGGER =
LoggerFactory.getLogger(KeyConditionsHolder.class);
- private static final Pattern KEY_PATTERN = Pattern.compile(KEY_REGEX);
private String keyCondExpr;
private Map<String, String> exprAttrNames;
private List<PColumn> pkCols;
@@ -47,12 +53,16 @@ public class KeyConditionsHolder {
private PColumn partitionKeyPKCol;
private PColumn sortKeyPKCol;
- public KeyConditionsHolder(String keyCondExpr, Map<String, String>
exprAttrNames) {
- this(keyCondExpr, exprAttrNames, null, false);
- }
-
public KeyConditionsHolder(String keyCondExpr, Map<String, String>
exprAttrNames,
List<PColumn> pkCols, boolean useIndex) {
+ if (pkCols == null) {
+ throw new IllegalArgumentException("pkCols cannot be null");
+ }
+ if (pkCols.isEmpty() || pkCols.size() > 2) {
+ throw new IllegalArgumentException(
+ "pkCols must contain 1 or 2 columns, but got " +
pkCols.size());
+ }
+
this.keyCondExpr = keyCondExpr;
this.exprAttrNames = (exprAttrNames != null) ? exprAttrNames : new
HashMap<>();
this.pkCols = pkCols;
@@ -146,35 +156,197 @@ public class KeyConditionsHolder {
}
/**
- * Parse the provided KeyConditionExpression into individual components.
+ * Parse the provided KeyConditionExpression into individual components
+ * using BsonExpressionParser and Phoenix ParseNode tree.
+ *
+ * Handles combinations like:
+ * 1. pk = :v1
+ * 2. pk = :v1 AND begins_with(sk, :v2)
+ * 3. pk = :v1 AND sk BETWEEN :v3 AND :v4
+ * 4. pk = :v1 AND sk = :v2 (and other comparison operators)
+ * 5. sk = :v2 (and other comparison operators) AND pk = :v1
+ * 6. begins_with(sk, :v2) AND pk = :v1
*/
private void parse() {
- Matcher matcher = KEY_PATTERN.matcher(keyCondExpr);
- if (!matcher.find()) {
- throw new RuntimeException("Unable to parse Key Condition
Expression: " + keyCondExpr);
- }
- String partitionKey = matcher.group(1);
- this.partitionValue = matcher.group(2);
- this.beginsWithSortKey = matcher.group(3);
- this.beginsWithSortKeyVal = matcher.group(4);
- String sortKey = matcher.group(5);
- this.sortKeyOperator = matcher.group(6);
- this.sortKeyValue1 = matcher.group(7);
- this.sortKeyValue2 = matcher.group(8);
-
- //partition key name
- this.partitionKeyName = exprAttrNames.getOrDefault(partitionKey,
partitionKey);
-
- // sort key name
- this.sortKeyName = (sortKey != null)
- ? exprAttrNames.getOrDefault(sortKey, sortKey)
- : (beginsWithSortKey != null)
- ? exprAttrNames.getOrDefault(beginsWithSortKey,
beginsWithSortKey)
- : null;
-
- if(pkCols != null) {
+ BsonExpressionParser bsonExpressionParser = new
BsonExpressionParser(keyCondExpr);
+ ParseNode parseNode;
+ try {
+ parseNode = bsonExpressionParser.parseExpression();
+ LOGGER.trace("Parsing KeyConditionExpression: {} -> ParseNode
type: {}",
+ keyCondExpr, parseNode.getClass().getSimpleName());
+
+ // Set up PK columns
this.partitionKeyPKCol = pkCols.get(0);
- this.sortKeyPKCol = (pkCols.size()==2) ? pkCols.get(1) : null;
+ this.sortKeyPKCol = (pkCols.size() == 2) ? pkCols.get(1) : null;
+
+ if (parseNode instanceof AndParseNode) {
+ List<ParseNode> children = parseNode.getChildren();
+ String partitionKeyColName =
CommonServiceUtils.getKeyNameFromBsonValueFunc(
+ partitionKeyPKCol.getName().getString());
+
+ // Process each condition
+ for (ParseNode child : children) {
+ if (child instanceof ComparisonParseNode) {
+ processComparisonExpression((ComparisonParseNode)
child, partitionKeyColName);
+ } else {
+ // Non-comparison nodes (BETWEEN, begins_with) are
always sort key conditions
+ processParseNode(child);
+ }
+ }
+ }
+ else {
+ // Single condition - must be partition key with =
+ processParseNode(parseNode);
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(String.format("BsonExpressionParser
failed for expression: %s", keyCondExpr), e);
}
}
-}
+
+ /**
+ * Process a ParseNode and extract key condition information.
+ * Based on Phoenix SQLComparisonExpressionUtils approach.
+ */
+ private void processParseNode(ParseNode parseNode) {
+ if (parseNode instanceof ComparisonParseNode) {
+ // Handle single comparison: pk = :v1
+ String partitionKeyColName =
CommonServiceUtils.getKeyNameFromBsonValueFunc(
+ partitionKeyPKCol.getName().getString());
+ processComparisonExpression((ComparisonParseNode) parseNode,
partitionKeyColName);
+ } else if (parseNode instanceof BetweenParseNode) {
+ // Handle BETWEEN: sk BETWEEN :v1 AND :v2
+ processBetweenExpression((BetweenParseNode) parseNode);
+ } else if (parseNode instanceof DocumentFieldBeginsWithParseNode) {
+ // Handle document field begins_with
+ processDocumentBeginsWithExpression(parseNode);
+ }
+ else {
+ throw new RuntimeException(String.format("Invalid
KeyConditionExpression: "
+ + "%s", parseNode.getClass().getSimpleName()));
+ }
+ }
+
+ /**
+ * Process comparison expressions (=, <, >, <=, >=)
+ */
+ private void processComparisonExpression(ComparisonParseNode compNode,
String partitionKeyColName) {
+ List<ParseNode> children = compNode.getChildren();
+ ParseNode leftNode = children.get(0);
+ ParseNode rightNode = children.get(1);
+ String operatorName = compNode.getFilterOp().toString();
+ String sqlOperator = mapFilterOpToSqlOperator(operatorName);
+
+ String columnName = extractColumnName(leftNode);
+ String resolvedName = resolveAttributeName(columnName);
+ String bindVariable = extractBindVariable(rightNode);
+
+ LOGGER.trace("Comparison: {}='{}' {} (SQL: {}) {}='{}'",
+ leftNode.getClass().getSimpleName(), columnName,
+ operatorName, sqlOperator,
+ rightNode.getClass().getSimpleName(), bindVariable);
+
+ // Identify partition key condition:
+ // 1. Must use = operator
+ // 2. Column matches partition key from schema
+ if (sqlOperator.equals("=") &&
resolvedName.equals(partitionKeyColName)) {
+ this.partitionKeyName = resolvedName;
+ this.partitionValue = bindVariable;
+ LOGGER.trace("Found partition key: {} = {}",
this.partitionKeyName, this.partitionValue);
+ } else {
+ // Any other condition is sort key
+ this.sortKeyName = resolvedName;
+ this.sortKeyOperator = sqlOperator;
+ this.sortKeyValue1 = bindVariable;
+ LOGGER.trace("Found sort key: {} {} {}", this.sortKeyName,
this.sortKeyOperator, this.sortKeyValue1);
+ }
+ }
+
+ /**
+ * Process BETWEEN expressions
+ */
+ private void processBetweenExpression(BetweenParseNode betweenNode) {
+ ParseNode columnNode = betweenNode.getChildren().get(0);
+ ParseNode lowerBoundNode = betweenNode.getChildren().get(1);
+ ParseNode upperBoundNode = betweenNode.getChildren().get(2);
+
+ String columnName = extractColumnName(columnNode);
+ String lowerValue = extractBindVariable(lowerBoundNode);
+ String upperValue = extractBindVariable(upperBoundNode);
+
+ if (columnName != null && lowerValue != null && upperValue != null) {
+ this.sortKeyName = resolveAttributeName(columnName);
+ this.sortKeyOperator = "BETWEEN";
+ this.sortKeyValue1 = lowerValue;
+ this.sortKeyValue2 = upperValue;
+ LOGGER.trace("Sort key: {} BETWEEN {} AND {}",
+ this.sortKeyName, this.sortKeyValue1,
this.sortKeyValue2);
+ }
+ }
+
+ /**
+ * Process document field begins_with expressions (different ParseNode
type)
+ */
+ private void processDocumentBeginsWithExpression(ParseNode node) {
+ LOGGER.trace("Processing document begins_with: {}",
node.getClass().getSimpleName());
+
+ List<ParseNode> children = node.getChildren();
+
+ String columnName = extractColumnName(children.get(0));
+ String bindVariable = extractBindVariable(children.get(1));
+
+ String resolvedColumnName = resolveAttributeName(columnName);
+ this.beginsWithSortKey = resolvedColumnName;
+ this.beginsWithSortKeyVal = bindVariable;
+ this.sortKeyName = resolvedColumnName;
+ LOGGER.trace("Set document begins_with: {} begins_with {}",
+ this.beginsWithSortKey, this.beginsWithSortKeyVal);
+ }
+
+ /**
+ * Extract column name from a ParseNode.
+ * key conditions always use string literals for column names.
+ */
+ private String extractColumnName(ParseNode node) {
+ return (String) ((LiteralParseNode) node).getValue();
+ }
+
+ /**
+ * Extract bind variable from a ParseNode.
+ * key conditions always use string literals starting with ':' for bind
variables.
+ */
+ private String extractBindVariable(ParseNode node) {
+ return (String) ((LiteralParseNode) node).getValue();
+ }
+
+ /**
+ * Map Phoenix FilterOp enum names to SQL operators
+ */
+ private String mapFilterOpToSqlOperator(String filterOpName) {
+ if (filterOpName == null) {
+ return "=";
+ }
+
+ switch (filterOpName.toUpperCase()) {
+ case "EQUAL":
+ return "=";
+ case "GREATER":
+ return ">";
+ case "GREATER_OR_EQUAL":
+ return ">=";
+ case "LESS":
+ return "<";
+ case "LESS_OR_EQUAL":
+ return "<=";
+ default:
+ throw new RuntimeException(String.format("Invalid operator
used in "
+ + "KeyConditionExpression: %s",
filterOpName.toUpperCase()));
+ }
+ }
+
+ /**
+ * Resolve attribute name using expression attribute names map
+ */
+ private String resolveAttributeName(String name) {
+ return exprAttrNames.getOrDefault(name, name);
+ }
+}
\ No newline at end of file