This is an automated email from the ASF dual-hosted git repository. palashc 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 e8ff86e623 PHOENIX-7663 : BSON Condition Function field_type() (#2232) e8ff86e623 is described below commit e8ff86e6235f8bd65892d9126d797d497f3cf978 Author: Palash Chauhan <palashc...@gmail.com> AuthorDate: Sat Jul 19 12:00:37 2025 -0700 PHOENIX-7663 : BSON Condition Function field_type() (#2232) Co-authored-by: Palash Chauhan <p.chau...@pchauha-ltmgv47.internal.salesforce.com> --- .../src/main/antlr3/PhoenixBsonExpression.g | 4 + .../util/bson/CommonComparisonExpressionUtils.java | 24 ++ .../util/bson/SQLComparisonExpressionUtils.java | 62 +++++ .../phoenix/parse/DocumentFieldTypeParseNode.java | 60 +++++ .../org/apache/phoenix/parse/ParseNodeFactory.java | 4 + .../java/org/apache/phoenix/end2end/Bson1IT.java | 25 +- .../util/bson/ComparisonExpressionUtilsTest.java | 279 +++++++++++++++++++++ 7 files changed, 457 insertions(+), 1 deletion(-) diff --git a/phoenix-core-client/src/main/antlr3/PhoenixBsonExpression.g b/phoenix-core-client/src/main/antlr3/PhoenixBsonExpression.g index 9edccc51d2..53ba10103b 100644 --- a/phoenix-core-client/src/main/antlr3/PhoenixBsonExpression.g +++ b/phoenix-core-client/src/main/antlr3/PhoenixBsonExpression.g @@ -34,6 +34,8 @@ tokens FIELD_NOT = 'field_not_exists'; BEGINS_WITH = 'begins_with'; CONTAINS = 'contains'; + FIELD_TYPE = 'field_type'; + ATTR_TYPE = 'attribute_type'; } @parser::header { @@ -238,6 +240,8 @@ boolean_expression returns [ParseNode ret] {$ret = factory.documentFieldBeginsWith(l, r); } ) | CONTAINS ( LPAREN l=value_expression COMMA r=value_expression RPAREN {$ret = factory.documentFieldContains(l, r); } ) + | (FIELD_TYPE | ATTR_TYPE) ( LPAREN l=value_expression COMMA r=value_expression RPAREN + {$ret = factory.documentFieldType(l, r); } ) ; value_expression returns [ParseNode ret] diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/CommonComparisonExpressionUtils.java b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/CommonComparisonExpressionUtils.java index aec21fc8f5..89aec18eef 100644 --- a/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/CommonComparisonExpressionUtils.java +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/expression/util/bson/CommonComparisonExpressionUtils.java @@ -55,6 +55,30 @@ public class CommonComparisonExpressionUtils { return false; } + public static boolean isBsonNumberSet(final BsonValue bsonValue) { + if (!isBsonSet(bsonValue)) { + return false; + } + BsonArray bsonArray = ((BsonDocument) bsonValue).get("$set").asArray(); + return !bsonArray.isEmpty() && bsonArray.get(0).isNumber(); + } + + public static boolean isBsonBinarySet(final BsonValue bsonValue) { + if (!isBsonSet(bsonValue)) { + return false; + } + BsonArray bsonArray = ((BsonDocument) bsonValue).get("$set").asArray(); + return !bsonArray.isEmpty() && bsonArray.get(0).isBinary(); + } + + public static boolean isBsonStringSet(final BsonValue bsonValue) { + if (!isBsonSet(bsonValue)) { + return false; + } + BsonArray bsonArray = ((BsonDocument) bsonValue).get("$set").asArray(); + return !bsonArray.isEmpty() && bsonArray.get(0).isString(); + } + /** * Comparison operators supported for the Document value comparisons. */ 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 eca475d12f..b31da51d4c 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 @@ -28,6 +28,7 @@ import org.apache.phoenix.parse.BsonExpressionParser; import org.apache.phoenix.parse.DocumentFieldBeginsWithParseNode; import org.apache.phoenix.parse.DocumentFieldContainsParseNode; import org.apache.phoenix.parse.DocumentFieldExistsParseNode; +import org.apache.phoenix.parse.DocumentFieldTypeParseNode; import org.apache.phoenix.parse.EqualParseNode; import org.apache.phoenix.parse.GreaterThanOrEqualParseNode; import org.apache.phoenix.parse.GreaterThanParseNode; @@ -178,6 +179,15 @@ public final class SQLComparisonExpressionUtils { fieldName = replaceExpressionFieldNames(fieldName, keyAliasDocument, sortedKeyNames); final String containsValue = (String) value.getValue(); return contains(fieldName, containsValue, rawBsonDocument, comparisonValuesDocument); + } else if (parseNode instanceof DocumentFieldTypeParseNode) { + final DocumentFieldTypeParseNode documentFieldTypeParseNode = + (DocumentFieldTypeParseNode) parseNode; + final LiteralParseNode fieldKey = (LiteralParseNode) documentFieldTypeParseNode.getFieldKey(); + final LiteralParseNode value = (LiteralParseNode) documentFieldTypeParseNode.getValue(); + String fieldName = (String) fieldKey.getValue(); + fieldName = replaceExpressionFieldNames(fieldName, keyAliasDocument, sortedKeyNames); + final String type = (String) value.getValue(); + return isFieldOfType(fieldName, type, rawBsonDocument, comparisonValuesDocument); } else if (parseNode instanceof EqualParseNode) { final EqualParseNode equalParseNode = (EqualParseNode) parseNode; final LiteralParseNode lhs = (LiteralParseNode) equalParseNode.getLHS(); @@ -601,6 +611,58 @@ public final class SQLComparisonExpressionUtils { return false; } + /** + * Returns true if the type of the value of the field key is same as the provided type. The + * provided type should be one of the following: {N,BS,L,B,NULL,M,S,SS,NS,BOOL}. + * @param fieldKey The field key for which value is checked for field_type. + * @param type The type to check against the type of 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 is of the given type, false otherwise + */ + private static boolean isFieldOfType(final String fieldKey, final String type, + final RawBsonDocument rawBsonDocument, final BsonDocument comparisonValuesDocument) { + BsonValue topLevelValue = rawBsonDocument.get(fieldKey); + BsonValue fieldValue = topLevelValue != null + ? topLevelValue + : CommonComparisonExpressionUtils.getFieldFromDocument(fieldKey, rawBsonDocument); + if (fieldValue == null) { + return false; + } + BsonValue typeBsonVal = comparisonValuesDocument.get(type); + if (typeBsonVal == null) { + throw new BsonConditionInvalidArgumentException( + "Value for type was not found in the comparison values document."); + } + switch (((BsonString) typeBsonVal).getValue()) { + case "S": + return fieldValue.isString(); + case "N": + return fieldValue.isNumber(); + case "B": + return fieldValue.isBinary(); + case "BOOL": + return fieldValue.isBoolean(); + case "NULL": + return fieldValue.isNull(); + case "L": + return fieldValue.isArray(); + case "M": + return fieldValue.isDocument(); + case "SS": + return CommonComparisonExpressionUtils.isBsonStringSet(fieldValue); + case "NS": + return CommonComparisonExpressionUtils.isBsonNumberSet(fieldValue); + case "BS": + return CommonComparisonExpressionUtils.isBsonBinarySet(fieldValue); + default: + throw new BsonConditionInvalidArgumentException( + "Unsupported type in field_type() for BsonConditionExpression: " + type + + ", valid types: valid types: {N,BS,L,B,NULL,M,S,SS,NS,BOOL}"); + } + } + private static boolean areEqual(BsonValue value1, BsonValue value2) { if (value1 == null && value2 == null) { return true; diff --git a/phoenix-core-client/src/main/java/org/apache/phoenix/parse/DocumentFieldTypeParseNode.java b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/DocumentFieldTypeParseNode.java new file mode 100644 index 0000000000..92a5115be4 --- /dev/null +++ b/phoenix-core-client/src/main/java/org/apache/phoenix/parse/DocumentFieldTypeParseNode.java @@ -0,0 +1,60 @@ +/* + * 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 java.sql.SQLException; +import java.util.Arrays; +import java.util.List; +import org.apache.phoenix.compile.ColumnResolver; + +/** + * Parse Node to help determine whether the document field is of a given type. + */ +public class DocumentFieldTypeParseNode extends CompoundParseNode { + + DocumentFieldTypeParseNode(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("field_type("); + 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 757f81ea07..083ad9279f 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 @@ -310,6 +310,10 @@ public class ParseNodeFactory { return new DocumentFieldContainsParseNode(fieldKey, value); } + public DocumentFieldTypeParseNode documentFieldType(ParseNode fieldKey, ParseNode value) { + return new DocumentFieldTypeParseNode(fieldKey, value); + } + public ColumnDef columnDef(ColumnName columnDefName, String sqlTypeName, boolean isArray, Integer arrSize, Boolean isNull, Integer maxLength, Integer scale, boolean isPK, SortOrder sortOrder, String expressionStr, Integer encodedQualifier, boolean isRowTimestamp) { 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 faabeaca05..c0441afa1f 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 @@ -248,6 +248,28 @@ public class Bson1IT extends ParallelStatsDisabledIT { assertEquals(bsonDocument2, document2); assertFalse(rs.next()); + + conditionExpression = "field_type(#attr_5, :L)"; + conditionDoc = new BsonDocument(); + conditionDoc.put("$EXPR", new BsonString(conditionExpression)); + conditionDoc.put("$VAL", compareValuesDocument); + keyDoc = new BsonDocument(); + keyDoc.put("#attr_5", new BsonString("attr_5")); + conditionDoc.put("$KEYS", keyDoc); + query = "SELECT * FROM " + tableName + " WHERE BSON_CONDITION_EXPRESSION(COL, '" + + conditionDoc.toJson() + "')"; + rs = conn.createStatement().executeQuery(query); + assertTrue(rs.next()); + assertTrue(rs.next()); + + conditionExpression = "attribute_type(attr_5, :NS)"; + 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); + assertFalse(rs.next()); } } @@ -259,7 +281,8 @@ public class Bson1IT extends ParallelStatsDisabledIT { + " \":Ids1\" : \"12\",\n" + " \":NMap1_NList1\" : \"NListVal01\",\n" + " \":InPublication\" : false,\n" + " \":NestedList1_xyz0123\" : \"xyz0123\",\n" + " \":Attr5Value\" : \"str001\",\n" + " \":NestedList1String\" : \"1234abcd\",\n" - + " \":NonExistentValue\" : \"does_not_exist\"\n" + "}"; + + " \":NonExistentValue\" : \"does_not_exist\"\n" + " \":L\" : \"L\"\n" + + " \":NS\" : \"NS\"\n" + "}"; return RawBsonDocument.parse(json); } 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 35b8c4dfa5..8ae3db4bb1 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 @@ -1892,4 +1892,283 @@ public class ComparisonExpressionUtilsTest { return RawBsonDocument.parse(json); } + @Test + public void testFieldTypeFunction() { + RawBsonDocument rawBsonDocument = getFieldTypeTestDocument(); + RawBsonDocument compareValues = getFieldTypeCompareValDocument(); + + // Test String type (S) + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringField, :TypeS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringField, :TypeN)", rawBsonDocument, compareValues)); + + // Test Number type (N) - testing both integer and double + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NumberFieldInt, :TypeN)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NumberFieldDouble, :TypeN)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NumberFieldInt, :TypeS)", rawBsonDocument, compareValues)); + + // Test Binary type (B) + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(BinaryField, :TypeB)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(BinaryField, :TypeS)", rawBsonDocument, compareValues)); + + // Test Boolean type (BOOL) + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(BooleanFieldTrue, :TypeBOOL)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(BooleanFieldFalse, :TypeBOOL)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(BooleanFieldTrue, :TypeS)", rawBsonDocument, compareValues)); + + // Test Null type (NULL) + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NullField, :TypeNULL)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NullField, :TypeS)", rawBsonDocument, compareValues)); + + // Test List/Array type (L) + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(ArrayField, :TypeL)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(ArrayField, :TypeS)", rawBsonDocument, compareValues)); + + // Test Map/Document type (M) + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(DocumentField, :TypeM)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(DocumentField, :TypeS)", rawBsonDocument, compareValues)); + + // Test String Set type (SS) + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringSetField, :TypeSS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringSetField, :TypeNS)", rawBsonDocument, compareValues)); + + // Test Number Set type (NS) + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NumberSetField, :TypeNS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NumberSetField, :TypeSS)", rawBsonDocument, compareValues)); + + // Test Binary Set type (BS) + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(BinarySetField, :TypeBS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(BinarySetField, :TypeSS)", rawBsonDocument, compareValues)); + + // Test nested field types + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NestedDoc.NestedString, :TypeS)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NestedDoc.NestedNumber, :TypeN)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NestedDoc.NestedString, :TypeN)", rawBsonDocument, compareValues)); + + // Test non-existent field + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NonExistentField, :TypeS)", rawBsonDocument, compareValues)); + + // Test complex expressions with field_type + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringField, :TypeS) AND field_type(NumberFieldInt, :TypeN)", rawBsonDocument, + compareValues)); + + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringField, :TypeN) OR field_type(NumberFieldInt, :TypeN)", rawBsonDocument, + compareValues)); + + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringField, :TypeN) AND field_type(NumberFieldInt, :TypeS)", rawBsonDocument, + compareValues)); + + // Test NOT with field_type + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "NOT field_type(StringField, :TypeN)", rawBsonDocument, compareValues)); + + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "NOT field_type(StringField, :TypeS)", rawBsonDocument, compareValues)); + + // Test field_type combined with other functions + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringField, :TypeS) AND field_exists(StringField)", rawBsonDocument, + compareValues)); + + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NonExistentField, :TypeS) AND field_exists(NonExistentField)", rawBsonDocument, + compareValues)); + + // Test attribute_type (alternative syntax for field_type) + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(StringField, :TypeS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(StringField, :TypeN)", rawBsonDocument, compareValues)); + + // Test attribute_type with different data types + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(NumberFieldInt, :TypeN)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(BooleanFieldTrue, :TypeBOOL)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(ArrayField, :TypeL)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(DocumentField, :TypeM)", rawBsonDocument, compareValues)); + + // Test attribute_type with sets + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(StringSetField, :TypeSS)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(NumberSetField, :TypeNS)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(BinarySetField, :TypeBS)", rawBsonDocument, compareValues)); + + // Test attribute_type with nested fields + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(NestedDoc.NestedString, :TypeS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(NestedDoc.NestedString, :TypeN)", rawBsonDocument, compareValues)); + + // Test attribute_type in complex expressions + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(StringField, :TypeS) AND attribute_type(NumberFieldInt, :TypeN)", + rawBsonDocument, compareValues)); + + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(StringField, :TypeN) OR attribute_type(NumberFieldInt, :TypeN)", + rawBsonDocument, compareValues)); + + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(StringField, :TypeN) AND attribute_type(NumberFieldInt, :TypeS)", + rawBsonDocument, compareValues)); + + // Test NOT with attribute_type + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "NOT attribute_type(StringField, :TypeN)", rawBsonDocument, compareValues)); + + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "NOT attribute_type(StringField, :TypeS)", rawBsonDocument, compareValues)); + + // Test mixing field_type and attribute_type + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringField, :TypeS) AND attribute_type(NumberFieldInt, :TypeN)", rawBsonDocument, + compareValues)); + + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringField, :TypeN) OR attribute_type(NumberFieldInt, :TypeN)", rawBsonDocument, + compareValues)); + + // Test attribute_type with non-existent field + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(NonExistentField, :TypeS)", rawBsonDocument, compareValues)); + + // Test attribute_type combined with other functions + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(StringField, :TypeS) AND attribute_exists(StringField)", rawBsonDocument, + compareValues)); + + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(NonExistentField, :TypeS) AND attribute_not_exists(NonExistentField)", + rawBsonDocument, compareValues)); + + // Test empty sets - should return false for type checking + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(EmptyStringSet, :TypeSS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(EmptyNumberSet, :TypeNS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(EmptyBinarySet, :TypeBS)", rawBsonDocument, compareValues)); + + // Test empty sets with attribute_type syntax + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(EmptyStringSet, :TypeSS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(EmptyNumberSet, :TypeNS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(EmptyBinarySet, :TypeBS)", rawBsonDocument, compareValues)); + + // Test empty sets in complex expressions + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(StringSetField, :TypeSS) AND NOT field_type(EmptyStringSet, :TypeSS)", + rawBsonDocument, compareValues)); + + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(EmptyStringSet, :TypeSS) AND field_type(StringSetField, :TypeSS)", + rawBsonDocument, compareValues)); + + // Test array element access at specific indices + // ArrayField contains: [ "item1", "item2", 123 ] + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(ArrayField[0], :TypeS)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(ArrayField[1], :TypeS)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(ArrayField[2], :TypeN)", rawBsonDocument, compareValues)); + + // Test negative cases for array element types + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(ArrayField[0], :TypeN)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(ArrayField[2], :TypeS)", rawBsonDocument, compareValues)); + + // Test array element access with attribute_type + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(ArrayField[0], :TypeS)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(ArrayField[2], :TypeN)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "attribute_type(ArrayField[1], :TypeN)", rawBsonDocument, compareValues)); + + // Test nested array element access + // NestedDoc.NestedArray contains: [ 1, 2, 3 ] + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NestedDoc.NestedArray[0], :TypeN)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NestedDoc.NestedArray[1], :TypeN)", rawBsonDocument, compareValues)); + assertTrue(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NestedDoc.NestedArray[2], :TypeN)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NestedDoc.NestedArray[0], :TypeS)", rawBsonDocument, compareValues)); + + // Test non-existent array indices + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(ArrayField[5], :TypeS)", rawBsonDocument, compareValues)); + assertFalse(SQLComparisonExpressionUtils.evaluateConditionExpression( + "field_type(NestedDoc.NestedArray[10], :TypeN)", rawBsonDocument, compareValues)); + } + + private static RawBsonDocument getFieldTypeTestDocument() { + String json = "{\n" + " \"StringField\" : \"Hello World\",\n" + " \"NumberFieldInt\" : 42,\n" + + " \"NumberFieldDouble\" : 3.14159,\n" + " \"BinaryField\" : {\n" + " \"$binary\" : {\n" + + " \"base64\" : \"SGVsbG8gV29ybGQ=\",\n" + " \"subType\" : \"00\"\n" + " }\n" + + " },\n" + " \"BooleanFieldTrue\" : true,\n" + " \"BooleanFieldFalse\" : false,\n" + + " \"NullField\" : null,\n" + " \"ArrayField\" : [ \"item1\", \"item2\", 123 ],\n" + + " \"DocumentField\" : {\n" + " \"nestedKey\" : \"nestedValue\",\n" + + " \"nestedNum\" : 999\n" + " },\n" + + " \"StringSetField\" : { \"$set\" : [ \"apple\", \"banana\", \"cherry\" ] },\n" + + " \"NumberSetField\" : { \"$set\" : [ 10, 20, 30 ] },\n" + + " \"BinarySetField\" : { \"$set\" : [ {\n" + " \"$binary\" : {\n" + + " \"base64\" : \"VGVzdA==\",\n" + " \"subType\" : \"00\"\n" + " }\n" + + " }, {\n" + " \"$binary\" : {\n" + " \"base64\" : \"RGF0YQ==\",\n" + + " \"subType\" : \"00\"\n" + " }\n" + " } ] },\n" + + " \"EmptyStringSet\" : { \"$set\" : [ ] },\n" + + " \"EmptyNumberSet\" : { \"$set\" : [ ] },\n" + + " \"EmptyBinarySet\" : { \"$set\" : [ ] },\n" + " \"NestedDoc\" : {\n" + + " \"NestedString\" : \"nested value\",\n" + " \"NestedNumber\" : 777,\n" + + " \"NestedArray\" : [ 1, 2, 3 ]\n" + " }\n" + "}"; + return RawBsonDocument.parse(json); + } + + private static RawBsonDocument getFieldTypeCompareValDocument() { + String json = + "{\n" + " \":TypeS\" : \"S\",\n" + " \":TypeN\" : \"N\",\n" + " \":TypeB\" : \"B\",\n" + + " \":TypeBOOL\" : \"BOOL\",\n" + " \":TypeNULL\" : \"NULL\",\n" + + " \":TypeL\" : \"L\",\n" + " \":TypeM\" : \"M\",\n" + " \":TypeSS\" : \"SS\",\n" + + " \":TypeNS\" : \"NS\",\n" + " \":TypeBS\" : \"BS\"\n" + "}"; + return RawBsonDocument.parse(json); + } + }