This is an automated email from the ASF dual-hosted git repository.
vjasani pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/phoenix.git
The following commit(s) were added to refs/heads/master by this push:
new 2274dc4bc3 PHOENIX-7585 New BSON Condition Function begins_with()
(#2214)
2274dc4bc3 is described below
commit 2274dc4bc3e7b645102e489bfcd7d5baf346a250
Author: Viraj Jasani <[email protected]>
AuthorDate: Mon Jul 7 21:40:13 2025 -0700
PHOENIX-7585 New BSON Condition Function begins_with() (#2214)
---
.../src/main/antlr3/PhoenixBsonExpression.g | 16 ++-
.../BsonConditionInvalidArgumentException.java | 33 +++++++
.../util/bson/SQLComparisonExpressionUtils.java | 59 +++++++++++
.../parse/DocumentFieldBeginsWithParseNode.java | 63 ++++++++++++
.../org/apache/phoenix/parse/ParseNodeFactory.java | 5 +
.../java/org/apache/phoenix/end2end/Bson1IT.java | 22 +++++
.../util/bson/ComparisonExpressionUtilsTest.java | 108 ++++++++++++++++++++-
7 files changed, 302 insertions(+), 4 deletions(-)
diff --git a/phoenix-core-client/src/main/antlr3/PhoenixBsonExpression.g
b/phoenix-core-client/src/main/antlr3/PhoenixBsonExpression.g
index 537b4c4259..86313300ef 100644
--- a/phoenix-core-client/src/main/antlr3/PhoenixBsonExpression.g
+++ b/phoenix-core-client/src/main/antlr3/PhoenixBsonExpression.g
@@ -32,6 +32,7 @@ tokens
ATTR_NOT = 'attribute_not_exists';
FIELD = 'field_exists';
FIELD_NOT = 'field_not_exists';
+ BEGINS_WITH = 'begins_with';
}
@parser::header {
@@ -211,8 +212,6 @@ and_expression returns [ParseNode ret]
not_expression returns [ParseNode ret]
: (NOT? boolean_expression ) => n=NOT? e=boolean_expression { $ret = n
== null ? e : factory.not(e); }
| n=NOT? LPAREN e=expression RPAREN { $ret = n == null ? e :
factory.not(e); }
- | (ATTR | FIELD) ( LPAREN t=literal RPAREN {$ret =
factory.documentFieldExists(t, true); } )
- | (ATTR_NOT | FIELD_NOT) ( LPAREN t=literal RPAREN {$ret =
factory.documentFieldExists(t, false); } )
;
comparison_op returns [CompareOperator ret]
@@ -232,6 +231,10 @@ boolean_expression returns [ParseNode ret]
| (IN (LPAREN v=one_or_more_expressions RPAREN
{List<ParseNode> il = new ArrayList<ParseNode>(v.size() + 1); il.add(l);
il.addAll(v); $ret = factory.inList(il,n!=null);}))
))
| { $ret = l; } )
+ | (ATTR | FIELD) ( LPAREN t=literal RPAREN {$ret =
factory.documentFieldExists(t, true); } )
+ | (ATTR_NOT | FIELD_NOT) ( LPAREN t=literal RPAREN {$ret =
factory.documentFieldExists(t, false); } )
+ | BEGINS_WITH ( LPAREN l=value_expression COMMA r=value_expression
RPAREN
+ {$ret = factory.documentFieldBeginsWith(l, r); } )
;
value_expression returns [ParseNode ret]
@@ -512,3 +515,12 @@ CHAR_ESC
)
| '\'\'' { setText("\'"); }
;
+
+WS
+ : ( ' ' | '\t' | '\u2002' ) { $channel=HIDDEN; }
+ ;
+
+EOL
+ : ('\r' | '\n')
+ { skip(); }
+ ;
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/BsonConditionInvalidArgumentException.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/BsonConditionInvalidArgumentException.java
new file mode 100644
index 0000000000..3475d46416
--- /dev/null
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/BsonConditionInvalidArgumentException.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.phoenix.expression.util.bson;
+
+/**
+ * Exception thrown when invalid arguments are provided to BSON condition
expressions.
+ */
+public class BsonConditionInvalidArgumentException extends
IllegalArgumentException {
+
+ public BsonConditionInvalidArgumentException(String message) {
+ super(message);
+ }
+
+ public BsonConditionInvalidArgumentException(String message, Throwable
cause) {
+ super(message, cause);
+ }
+}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/SQLComparisonExpressionUtils.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/SQLComparisonExpressionUtils.java
index c4faa88275..8a9ac9bd72 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/SQLComparisonExpressionUtils.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/SQLComparisonExpressionUtils.java
@@ -21,6 +21,7 @@ package org.apache.phoenix.expression.util.bson;
import org.apache.phoenix.parse.AndParseNode;
import org.apache.phoenix.parse.BetweenParseNode;
import org.apache.phoenix.parse.DocumentFieldExistsParseNode;
+import org.apache.phoenix.parse.DocumentFieldBeginsWithParseNode;
import org.apache.phoenix.parse.BsonExpressionParser;
import org.apache.phoenix.parse.EqualParseNode;
import org.apache.phoenix.parse.GreaterThanOrEqualParseNode;
@@ -44,6 +45,7 @@ import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.stream.IntStream;
/**
* SQL style condition expression evaluation support.
@@ -164,6 +166,17 @@ public final class SQLComparisonExpressionUtils {
String fieldName = (String) fieldKey.getValue();
fieldName = replaceExpressionFieldNames(fieldName, keyAliasDocument,
sortedKeyNames);
return documentFieldExistsParseNode.isExists() == exists(fieldName,
rawBsonDocument);
+ } else if (parseNode instanceof DocumentFieldBeginsWithParseNode) {
+ final DocumentFieldBeginsWithParseNode documentFieldBeginsWithParseNode =
+ (DocumentFieldBeginsWithParseNode) parseNode;
+ final LiteralParseNode fieldKey =
+ (LiteralParseNode)
documentFieldBeginsWithParseNode.getFieldKey();
+ final LiteralParseNode value =
+ (LiteralParseNode) documentFieldBeginsWithParseNode.getValue();
+ String fieldName = (String) fieldKey.getValue();
+ fieldName = replaceExpressionFieldNames(fieldName, keyAliasDocument,
sortedKeyNames);
+ final String prefixValue = (String) value.getValue();
+ return beginsWith(fieldName, prefixValue, rawBsonDocument,
comparisonValuesDocument);
} else if (parseNode instanceof EqualParseNode) {
final EqualParseNode equalParseNode = (EqualParseNode) parseNode;
final LiteralParseNode lhs = (LiteralParseNode) equalParseNode.getLHS();
@@ -514,4 +527,50 @@ public final class SQLComparisonExpressionUtils {
comparisonValuesDocument);
}
+ /**
+ * Returns true if the value of the field begins with the prefix value
represented by
+ * {@code prefixValue}. The comparison supports String and Binary data types
only.
+ * For other data types, throws BsonConditionInvalidArgumentException.
+ *
+ * @param fieldKey The field key for which value is checked for prefix.
+ * @param prefixValue The prefix value to check against the field value.
+ * @param rawBsonDocument Bson Document representing the cell value on which
the comparison is
+ * to be performed.
+ * @param comparisonValuesDocument Bson Document with values placeholder.
+ * @return True if the value of the field begins with prefixValue.
+ * @throws BsonConditionInvalidArgumentException if unsupported data types
are used.
+ */
+ private static boolean beginsWith(final String fieldKey, final String
prefixValue,
+ final RawBsonDocument rawBsonDocument,
+ final BsonDocument
comparisonValuesDocument)
+ throws BsonConditionInvalidArgumentException {
+ BsonValue topLevelValue = rawBsonDocument.get(fieldKey);
+ BsonValue fieldValue = topLevelValue != null ?
+ topLevelValue :
+ CommonComparisonExpressionUtils.getFieldFromDocument(fieldKey,
rawBsonDocument);
+ if (fieldValue == null) {
+ return false;
+ }
+ BsonValue prefixBsonValue = comparisonValuesDocument.get(prefixValue);
+ if (prefixBsonValue == null) {
+ return false;
+ }
+ if (fieldValue.isString() && prefixBsonValue.isString()) {
+ String fieldStr = ((BsonString) fieldValue).getValue();
+ String prefixStr = ((BsonString) prefixBsonValue).getValue();
+ return fieldStr.startsWith(prefixStr);
+ } else if (fieldValue.isBinary() && prefixBsonValue.isBinary()) {
+ byte[] fieldBytes = fieldValue.asBinary().getData();
+ byte[] prefixBytes = prefixBsonValue.asBinary().getData();
+ if (prefixBytes.length > fieldBytes.length) {
+ return false;
+ }
+ return IntStream.range(0, prefixBytes.length)
+ .noneMatch(i -> fieldBytes[i] != prefixBytes[i]);
+ } else {
+ throw new BsonConditionInvalidArgumentException(
+ "begins_with function only supports String and Binary data
types.");
+ }
+ }
+
}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/DocumentFieldBeginsWithParseNode.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/DocumentFieldBeginsWithParseNode.java
new file mode 100644
index 0000000000..91cc703407
--- /dev/null
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/DocumentFieldBeginsWithParseNode.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.phoenix.parse;
+
+import org.apache.phoenix.compile.ColumnResolver;
+
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Parse Node to help determine whether the document field starts with a given
value.
+ * The first operand is the field key and the second operand is the value to
check.
+ */
+public class DocumentFieldBeginsWithParseNode extends CompoundParseNode {
+
+ DocumentFieldBeginsWithParseNode(ParseNode fieldKey, ParseNode value) {
+ super(Arrays.asList(fieldKey, value));
+ }
+
+ @Override
+ public <T> T accept(ParseNodeVisitor<T> visitor) throws SQLException {
+ List<T> l = java.util.Collections.emptyList();
+ if (visitor.visitEnter(this)) {
+ l = acceptChildren(visitor);
+ }
+ return visitor.visitLeave(this, l);
+ }
+
+ @Override
+ public void toSQL(ColumnResolver resolver, StringBuilder buf) {
+ List<ParseNode> children = getChildren();
+ buf.append("begins_with(");
+ children.get(0).toSQL(resolver, buf);
+ buf.append(", ");
+ children.get(1).toSQL(resolver, buf);
+ buf.append(")");
+ }
+
+ public ParseNode getFieldKey() {
+ return getChildren().get(0);
+ }
+
+ public ParseNode getValue() {
+ return getChildren().get(1);
+ }
+}
diff --git
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
index 658f052c28..f70f4ad16f 100644
---
a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
+++
b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/ParseNodeFactory.java
@@ -298,6 +298,11 @@ public class ParseNodeFactory {
return new DocumentFieldExistsParseNode(fieldName, exists);
}
+ public DocumentFieldBeginsWithParseNode documentFieldBeginsWith(ParseNode
fieldKey,
+ ParseNode
value) {
+ return new DocumentFieldBeginsWithParseNode(fieldKey, value);
+ }
+
public ColumnDef columnDef(ColumnName columnDefName, String sqlTypeName,
boolean isArray, Integer arrSize, Boolean
isNull,
Integer maxLength, Integer scale, boolean isPK,
diff --git a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson1IT.java
b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson1IT.java
index f49faad77f..5ed87f4837 100644
--- a/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson1IT.java
+++ b/phoenix-core/src/it/java/org/apache/phoenix/end2end/Bson1IT.java
@@ -186,6 +186,27 @@ public class Bson1IT extends ParallelStatsDisabledIT {
assertEquals(bsonDocument2, document2);
assertFalse(rs.next());
+
+ conditionExpression =
+ "NestedList1[0] <= :NestedList1_485 AND NestedList1[2][0] >=
:NestedList1_xyz0123 "
+ + "AND NestedList1[2][1].Id < :Id1 AND IdS < :Ids1 AND
Id2 > :Id2 "
+ + "AND begins_with(Title, :TitlePrefix)";
+
+ conditionDoc = new BsonDocument();
+ conditionDoc.put("$EXPR", new BsonString(conditionExpression));
+ conditionDoc.put("$VAL", compareValuesDocument);
+
+ query = "SELECT * FROM " + tableName + " WHERE
BSON_CONDITION_EXPRESSION(COL, '"
+ + conditionDoc.toJson() + "')";
+ rs = conn.createStatement().executeQuery(query);
+
+ assertTrue(rs.next());
+ assertEquals("pk0002", rs.getString(1));
+ assertEquals(4596.354, rs.getDouble(2), 0.0);
+ document2 = (BsonDocument) rs.getObject(3);
+ assertEquals(bsonDocument2, document2);
+
+ assertFalse(rs.next());
}
}
@@ -194,6 +215,7 @@ public class Bson1IT extends ParallelStatsDisabledIT {
" \":NestedList1_485\" : -485.33,\n" +
" \":ISBN\" : \"111-1111111111\",\n" +
" \":Title\" : \"Book 101 Title\",\n" +
+ " \":TitlePrefix\" : \"Book \",\n" +
" \":Id\" : 101.01,\n" +
" \":Id2\" : 12,\n" +
" \":Id1\" : 120,\n" +
diff --git
a/phoenix-core/src/test/java/org/apache/phoenix/util/bson/ComparisonExpressionUtilsTest.java
b/phoenix-core/src/test/java/org/apache/phoenix/util/bson/ComparisonExpressionUtilsTest.java
index b06d815000..af68fcbf97 100644
---
a/phoenix-core/src/test/java/org/apache/phoenix/util/bson/ComparisonExpressionUtilsTest.java
+++
b/phoenix-core/src/test/java/org/apache/phoenix/util/bson/ComparisonExpressionUtilsTest.java
@@ -34,11 +34,13 @@ import org.bson.RawBsonDocument;
import org.junit.Test;
import org.apache.hadoop.hbase.util.Bytes;
+import
org.apache.phoenix.expression.util.bson.BsonConditionInvalidArgumentException;
import
org.apache.phoenix.expression.util.bson.DocumentComparisonExpressionUtils;
import org.apache.phoenix.expression.util.bson.SQLComparisonExpressionUtils;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
/**
* Tests for BSON Condition Expression Utility.
@@ -1568,6 +1570,106 @@ public class ComparisonExpressionUtilsTest {
}
+ @Test
+ public void testBeginsWithFunction() {
+ RawBsonDocument rawBsonDocument = getDocumentValue();
+ RawBsonDocument compareValues = getCompareValDocument();
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(Title, #Title)", rawBsonDocument, compareValues));
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(ISBN, :ISBN)", rawBsonDocument, compareValues));
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(Title, :TitlePrefix)", rawBsonDocument,
compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(Title, #NestedList1_xyz0123)", rawBsonDocument,
compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(ISBN, #NestedList1_1)", rawBsonDocument,
compareValues));
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(NestedMap1.Title, #Title)", rawBsonDocument,
compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(NestedMap1.Title, #NestedList1_xyz0123)",
rawBsonDocument, compareValues));
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(NestedMap1.NList1[2], #NestedMap1_NList1_3)",
rawBsonDocument,
+ compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(NestedMap1.NList1[2], $NestedMap1_NList1_30)",
rawBsonDocument,
+ compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(NonExistentField, #Title)", rawBsonDocument,
compareValues));
+
+ try {
+ SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(Id, #Title)", rawBsonDocument, compareValues);
+ fail("Expected BsonConditionInvalidArgumentException");
+ } catch (BsonConditionInvalidArgumentException e) {
+ // expected
+ }
+
+ try {
+ SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(Title, #NestedMap1_NList1_3)", rawBsonDocument,
compareValues);
+ fail("Expected BsonConditionInvalidArgumentException");
+ } catch (BsonConditionInvalidArgumentException e) {
+ // expected
+ }
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(Title, #Title) AND field_exists(Id) =
begins_with(Title, #Title)",
+ rawBsonDocument, compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(Title, #NestedList1_xyz0123) AND field_exists(Id)",
rawBsonDocument,
+ compareValues));
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(Title, #NestedList1_xyz0123) OR field_exists(Id)",
rawBsonDocument,
+ compareValues));
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "begins_with(Title, #Title) OR field_not_exists(Id)",
rawBsonDocument, compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "NOT begins_with(Title, #Title)", rawBsonDocument, compareValues));
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "NOT begins_with(Title, #NestedList1_xyz0123)", rawBsonDocument,
compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "NOT begins_with(Title, #Title)", rawBsonDocument, compareValues));
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "NOT begins_with(Title, #NestedList1_xyz0123)", rawBsonDocument,
compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "NOT begins_with(Title, :TitlePrefix)", rawBsonDocument,
compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "NOT begins_with(NestedMap1.NList1[2], #NestedMap1_NList1_3)",
rawBsonDocument,
+ compareValues));
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "NOT begins_with(NestedMap1.NList1[2], $NestedMap1_NList1_30)",
rawBsonDocument,
+ compareValues));
+
+ assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "NOT begins_with(Title, #NestedList1_xyz0123) AND
field_exists(Id)", rawBsonDocument,
+ compareValues));
+
+ assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression(
+ "NOT begins_with(Title, #Title) OR NOT begins_with(Title,
:TitlePrefix)",
+ rawBsonDocument, compareValues));
+ }
+
private static RawBsonDocument getCompareValDocument() {
String json = "{\n" +
" \"$Id20\" : 101.011,\n" +
@@ -1598,7 +1700,8 @@ public class ComparisonExpressionUtilsTest {
" \"#NMap1_NList1\" : \"NListVal01\",\n" +
" \"$NestedList1_4850\" : -485.35,\n" +
" \"$Id\" : 101.01,\n" +
- " \"#Title\" : \"Book 101 Title\"\n" +
+ " \"#Title\" : \"Book 101 Title\",\n" +
+ " \":TitlePrefix\" : \"Book\"\n" +
"}";
//{
// "$Id20" : 101.011,
@@ -1628,7 +1731,8 @@ public class ComparisonExpressionUtilsTest {
// "#NMap1_NList1" : "NListVal01",
// "$NestedList1_4850" : -485.35,
// "$Id" : 101.01,
- // "#Title" : "Book 101 Title"
+ // "#Title" : "Book 101 Title",
+ // ":TitlePrefix" : "Book"
//}
return RawBsonDocument.parse(json);
}