diff --git a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java index 11829bd6..7b75eae8 100644 --- a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java +++ b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java @@ -14,6 +14,7 @@ import static org.hypertrace.core.documentstore.expression.operators.FunctionOperator.MULTIPLY; import static org.hypertrace.core.documentstore.expression.operators.LogicalOperator.AND; import static org.hypertrace.core.documentstore.expression.operators.LogicalOperator.OR; +import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.CONTAINS; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.EQ; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.GT; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.GTE; @@ -21,6 +22,7 @@ import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.LT; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.LTE; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NEQ; +import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NOT_CONTAINS; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NOT_IN; import static org.hypertrace.core.documentstore.expression.operators.SortOrder.ASC; import static org.hypertrace.core.documentstore.expression.operators.SortOrder.DESC; @@ -1483,6 +1485,58 @@ public void testUnnestWithRegularFilterAndNullAndEmptyPreservedAtFirstLevel(Stri dataStoreName, iterator, "query/unwind_preserve_with_regular_filter_first_level.json", 3); } + @ParameterizedTest + @ArgumentsSource(AllProvider.class) + void testContainsAndUnnestFilters(String dataStoreName) throws IOException { + Collection collection = getCollection(dataStoreName); + + org.hypertrace.core.documentstore.query.Query query = + org.hypertrace.core.documentstore.query.Query.builder() + .addSelection(IdentifierExpression.of("item")) + .addSelection(IdentifierExpression.of("sales.medium")) + .addFromClause( + UnnestExpression.builder() + .identifierExpression(IdentifierExpression.of("sales")) + .preserveNullAndEmptyArrays(false) + .filterTypeExpression( + RelationalExpression.of( + IdentifierExpression.of("sales.medium"), + CONTAINS, + ConstantExpression.of( + new JSONDocument("{\"type\": \"retail\",\"volume\": 500}")))) + .build()) + .build(); + Iterator iterator = collection.aggregate(query); + assertDocsAndSizeEqual( + dataStoreName, iterator, "query/unwind_contains_filter_response.json", 3); + } + + @ParameterizedTest + @ArgumentsSource(AllProvider.class) + void testNotContainsAndUnnestFilters(String dataStoreName) throws IOException { + Collection collection = getCollection(dataStoreName); + + org.hypertrace.core.documentstore.query.Query query = + org.hypertrace.core.documentstore.query.Query.builder() + .addSelection(IdentifierExpression.of("item")) + .addSelection(IdentifierExpression.of("sales.medium")) + .addFromClause( + UnnestExpression.builder() + .identifierExpression(IdentifierExpression.of("sales")) + .preserveNullAndEmptyArrays(false) + .filterTypeExpression( + RelationalExpression.of( + IdentifierExpression.of("sales.medium"), + NOT_CONTAINS, + ConstantExpression.of( + new JSONDocument("{\"type\": \"retail\",\"volume\": 500}")))) + .build()) + .build(); + Iterator iterator = collection.aggregate(query); + assertDocsAndSizeEqual( + dataStoreName, iterator, "query/unwind_not_contains_filter_response.json", 2); + } + @ParameterizedTest @ArgumentsSource(AllProvider.class) public void testQueryV1DistinctCountWithSortingSpecs(String dataStoreName) throws IOException { diff --git a/document-store/src/integrationTest/resources/query/unwind_contains_filter_response.json b/document-store/src/integrationTest/resources/query/unwind_contains_filter_response.json new file mode 100644 index 00000000..9a28f9c1 --- /dev/null +++ b/document-store/src/integrationTest/resources/query/unwind_contains_filter_response.json @@ -0,0 +1,59 @@ +[ + { + "item":"Soap", + "sales":{ + "medium":[ + { + "type":"distributionChannel", + "volume":1000 + }, + { + "type":"retail", + "volume":500 + }, + { + "type":"online", + "volume":1000 + } + ] + } + }, + { + "item":"Shampoo", + "sales":{ + "medium":[ + { + "type":"distributionChannel", + "volume":3000 + }, + { + "type":"retail", + "volume":500 + }, + { + "type":"online", + "volume":1000 + } + ] + } + }, + { + "item":"Shampoo", + "sales":{ + "medium":[ + { + "type":"distributionChannel", + "volume":700 + }, + { + "type":"retail", + "volume":500 + }, + { + "type":"online", + "volume":5000 + } + ] + } + } +] \ No newline at end of file diff --git a/document-store/src/integrationTest/resources/query/unwind_not_contains_filter_response.json b/document-store/src/integrationTest/resources/query/unwind_not_contains_filter_response.json new file mode 100644 index 00000000..b1b5084e --- /dev/null +++ b/document-store/src/integrationTest/resources/query/unwind_not_contains_filter_response.json @@ -0,0 +1,25 @@ +[ + { + "item":"Soap", + "sales":{ + "medium":[ + { + "type":"distributionChannel", + "volume":300 + }, + { + "type":"online", + "volume":2000 + } + ] + } + }, + { + "item":"Mirror", + "sales":{ + "medium":[ + + ] + } + } +] \ No newline at end of file diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ConstantExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ConstantExpression.java index 809ce6c2..03bbbfc8 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ConstantExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ConstantExpression.java @@ -4,8 +4,10 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Value; +import lombok.experimental.NonFinal; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.hypertrace.core.documentstore.Document; import org.hypertrace.core.documentstore.expression.type.SelectTypeExpression; import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; @@ -19,10 +21,11 @@ * */ @Value +@NonFinal @AllArgsConstructor(access = AccessLevel.PRIVATE) public class ConstantExpression implements SelectTypeExpression { - Object value; + protected Object value; public static ConstantExpression of(final String value) { return new ConstantExpression(value); @@ -36,6 +39,10 @@ public static ConstantExpression of(final Boolean value) { return new ConstantExpression(value); } + public static ConstantExpression of(final Document value) { + return new DocumentConstantExpression(value); + } + public static ConstantExpression ofStrings(final List values) { return validateAndReturn(values); } @@ -68,4 +75,25 @@ public String toString() { ? StringUtils.wrap(value.toString(), "'") : String.valueOf(value); } + + public static class DocumentConstantExpression extends ConstantExpression { + private DocumentConstantExpression(final Document value) { + super(value); + } + + @Override + public T accept(final SelectTypeExpressionVisitor visitor) { + return visitor.visit(this); + } + + @Override + public Document getValue() { + return (Document) value; + } + + @Override + public String toString() { + return "JSON(" + StringUtils.wrap(getValue().toJson(), "'") + ")"; + } + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/operators/RelationalOperator.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/operators/RelationalOperator.java index 0db5c3ab..1b7d1ccf 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/operators/RelationalOperator.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/operators/RelationalOperator.java @@ -12,6 +12,7 @@ public enum RelationalOperator { LTE("<="), IN("IN"), CONTAINS("CONTAINS"), + NOT_CONTAINS("NOT CONTAINS"), EXISTS("EXISTS"), NOT_EXISTS("NOT EXISTS"), LIKE("~"), diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoConstantExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoConstantExpressionParser.java index e68450a3..0e0bce08 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoConstantExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoConstantExpressionParser.java @@ -1,7 +1,12 @@ package org.hypertrace.core.documentstore.mongo.query.parser; +import static org.hypertrace.core.documentstore.mongo.MongoUtils.sanitizeJsonString; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.mongodb.BasicDBObject; import lombok.NoArgsConstructor; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; @NoArgsConstructor final class MongoConstantExpressionParser extends MongoSelectTypeExpressionParser { @@ -15,6 +20,16 @@ public Object visit(final ConstantExpression expression) { return parse(expression); } + @SuppressWarnings("unchecked") + @Override + public Object visit(final DocumentConstantExpression expression) { + try { + return BasicDBObject.parse(sanitizeJsonString(expression.getValue().toJson())); + } catch (final JsonProcessingException e) { + throw new RuntimeException(e); + } + } + Object parse(final ConstantExpression expression) { return expression.getValue(); } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoRelationalExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoRelationalExpressionParser.java index e5f3cf3a..930be13b 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoRelationalExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoRelationalExpressionParser.java @@ -11,6 +11,7 @@ import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.LT; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.LTE; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NEQ; +import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NOT_CONTAINS; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NOT_EXISTS; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NOT_IN; import static org.hypertrace.core.documentstore.mongo.MongoUtils.PREFIX; @@ -24,8 +25,13 @@ import org.hypertrace.core.documentstore.expression.impl.RelationalExpression; import org.hypertrace.core.documentstore.expression.operators.RelationalOperator; import org.hypertrace.core.documentstore.expression.type.SelectTypeExpression; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; final class MongoRelationalExpressionParser { + private static final Logger LOGGER = + LoggerFactory.getLogger(MongoRelationalExpressionParser.class); + private static final String EXPR = "$expr"; private static final String REGEX = "$regex"; private static final String OPTIONS = "$options"; @@ -46,6 +52,7 @@ final class MongoRelationalExpressionParser { put(LTE, expressionHandler("lte")); put(IN, handler("in")); put(CONTAINS, handler("elemMatch")); + put(NOT_CONTAINS, notContainsHandler()); put(EXISTS, handler("exists")); put(NOT_EXISTS, handler("exists")); put(LIKE, likeHandler()); @@ -125,4 +132,14 @@ private static Map generateMap( throw getUnsupportedOperationException(operator); }; } + + private static BiFunction> + notContainsHandler() { + return (lhs, rhs) -> { + final String parsedLhs = lhs.accept(identifierParser); + final Object parsedRhs = rhs.accept(rhsParser); + return Map.of( + parsedLhs, new BasicDBObject("$not", new BasicDBObject(PREFIX + "elemMatch", parsedRhs))); + }; + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoSelectTypeExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoSelectTypeExpressionParser.java index 290656f5..c2bdb16a 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoSelectTypeExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoSelectTypeExpressionParser.java @@ -7,6 +7,7 @@ import java.util.Map; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; @@ -37,6 +38,11 @@ public T visit(final ConstantExpression expression) { return baseParser.visit(expression); } + @Override + public T visit(final DocumentConstantExpression expression) { + return baseParser.visit(expression); + } + @Override public T visit(final FunctionExpression expression) { return baseParser.visit(expression); diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoUnsupportedSelectTypeExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoUnsupportedSelectTypeExpressionParser.java index 95646ba9..dfd06542 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoUnsupportedSelectTypeExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoUnsupportedSelectTypeExpressionParser.java @@ -6,6 +6,7 @@ import lombok.NoArgsConstructor; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; @@ -24,6 +25,11 @@ public T visit(final ConstantExpression expression) { throw getUnsupportedOperationException(expression); } + @Override + public T visit(final DocumentConstantExpression expression) { + throw getUnsupportedOperationException(expression); + } + @Override public T visit(final FunctionExpression expression) { throw getUnsupportedOperationException(expression); diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsAddingTransformation.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsAddingTransformation.java index ef78749f..0a876eb2 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsAddingTransformation.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsAddingTransformation.java @@ -9,6 +9,7 @@ import lombok.AllArgsConstructor; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; import org.hypertrace.core.documentstore.expression.type.SelectTypeExpression; @@ -105,6 +106,12 @@ public Optional visit(final ConstantExpression expression) { return Optional.empty(); } + @SuppressWarnings("unchecked") + @Override + public Optional visit(final DocumentConstantExpression expression) { + return Optional.empty(); + } + @SuppressWarnings("unchecked") @Override public Optional visit(final FunctionExpression expression) { diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsUpdatingTransformation.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsUpdatingTransformation.java index e2bde12a..b099542c 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsUpdatingTransformation.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsUpdatingTransformation.java @@ -16,6 +16,7 @@ import java.util.function.Function; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; import org.hypertrace.core.documentstore.expression.operators.AggregationOperator; @@ -105,6 +106,12 @@ public SelectionSpec visit(final ConstantExpression expression) { return source; } + @SuppressWarnings("unchecked") + @Override + public SelectionSpec visit(final DocumentConstantExpression expression) { + return source; + } + @SuppressWarnings("unchecked") @Override public SelectionSpec visit(final FunctionExpression expression) { diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java index 45f62714..45890c73 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java @@ -2,6 +2,7 @@ import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; @@ -10,6 +11,8 @@ public interface SelectTypeExpressionVisitor { T visit(final ConstantExpression expression); + T visit(final DocumentConstantExpression expression); + T visit(final FunctionExpression expression); T visit(final IdentifierExpression expression); diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresSelectionQueryTransformer.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresSelectionQueryTransformer.java index a0d52d00..457e577f 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresSelectionQueryTransformer.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresSelectionQueryTransformer.java @@ -6,6 +6,7 @@ import java.util.stream.Collectors; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; import org.hypertrace.core.documentstore.expression.type.GroupTypeExpression; @@ -94,6 +95,11 @@ public Boolean visit(ConstantExpression expression) { return false; } + @Override + public Boolean visit(DocumentConstantExpression expression) { + return false; + } + @Override public Boolean visit(FunctionExpression expression) { return false; @@ -117,6 +123,11 @@ public Boolean visit(ConstantExpression expression) { return false; } + @Override + public Boolean visit(DocumentConstantExpression expression) { + return false; + } + @Override public Boolean visit(FunctionExpression expression) { return false; diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresUnnestQueryTransformer.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresUnnestQueryTransformer.java index 17da50ea..6c25dc9d 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresUnnestQueryTransformer.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresUnnestQueryTransformer.java @@ -10,6 +10,7 @@ import java.util.stream.IntStream; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.KeyExpression; @@ -191,6 +192,11 @@ public List visit(ConstantExpression expression) { return List.of(); } + @Override + public List visit(DocumentConstantExpression expression) { + return List.of(); + } + @Override public List visit(FunctionExpression expression) { return expression.getOperands().stream() diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresConstantExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresConstantExpressionVisitor.java index 78cec2cc..71e8faad 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresConstantExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresConstantExpressionVisitor.java @@ -2,6 +2,7 @@ import lombok.NoArgsConstructor; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.postgres.query.v1.PostgresQueryParser; @NoArgsConstructor @@ -24,4 +25,9 @@ public PostgresQueryParser getPostgresQueryParser() { public Object visit(final ConstantExpression expression) { return expression.getValue(); } + + @Override + public Object visit(final DocumentConstantExpression expression) { + return expression.getValue(); + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresDefaultSelectTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresDefaultSelectTypeExpressionVisitor.java index 77c45bf0..0fef560d 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresDefaultSelectTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresDefaultSelectTypeExpressionVisitor.java @@ -4,6 +4,7 @@ import lombok.NoArgsConstructor; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; import org.hypertrace.core.documentstore.postgres.query.v1.PostgresQueryParser; @@ -24,6 +25,11 @@ public T visit(final ConstantExpression expression) { throw new UnsupportedOperationException(String.valueOf(expression)); } + @Override + public T visit(final DocumentConstantExpression expression) { + throw new UnsupportedOperationException(expression.getValue().toJson()); + } + @Override public T visit(final FunctionExpression expression) { throw new UnsupportedOperationException(String.valueOf(expression)); diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresFilterTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresFilterTypeExpressionVisitor.java index ff24f703..ad0a5a9f 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresFilterTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresFilterTypeExpressionVisitor.java @@ -21,8 +21,12 @@ import org.hypertrace.core.documentstore.expression.type.SelectTypeExpression; import org.hypertrace.core.documentstore.parser.FilterTypeExpressionVisitor; import org.hypertrace.core.documentstore.postgres.query.v1.PostgresQueryParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class PostgresFilterTypeExpressionVisitor implements FilterTypeExpressionVisitor { + private static final Logger LOGGER = + LoggerFactory.getLogger(PostgresFilterTypeExpressionVisitor.class); protected PostgresQueryParser postgresQueryParser; @@ -56,9 +60,12 @@ public String visit(final RelationalExpression expression) { Object value = rhs.accept(rhsVisitor); PostgresSelectTypeExpressionVisitor lhsVisitor = - new PostgresFunctionExpressionVisitor( - new PostgresDataAccessorIdentifierExpressionVisitor( - postgresQueryParser, getType(value))); + isOperatorNeedsFieldAccessor(operator) + ? new PostgresFunctionExpressionVisitor( + new PostgresFieldIdentifierExpressionVisitor(postgresQueryParser)) + : new PostgresFunctionExpressionVisitor( + new PostgresDataAccessorIdentifierExpressionVisitor( + postgresQueryParser, getType(value))); final String parseResult = lhs.accept(lhsVisitor); return prepareParsedNonCompositeFilter( @@ -102,4 +109,14 @@ private Collector getCollectorForLogicalOperator(LogicalOperator operator) { throw new UnsupportedOperationException( String.format("Query operation:%s not supported", operator)); } + + private boolean isOperatorNeedsFieldAccessor(RelationalOperator operator) { + switch (operator) { + case CONTAINS: + case NOT_CONTAINS: + return true; + default: + return false; + } + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresSelectTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresSelectTypeExpressionVisitor.java index a431b556..600fc1f7 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresSelectTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresSelectTypeExpressionVisitor.java @@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; @@ -48,6 +49,11 @@ public T visit(final ConstantExpression expression) { return baseVisitor.visit(expression); } + @Override + public T visit(final DocumentConstantExpression expression) { + return baseVisitor.visit(expression); + } + @Override public T visit(final FunctionExpression expression) { return baseVisitor.visit(expression); diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/utils/PostgresUtils.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/utils/PostgresUtils.java index e4718168..b893690a 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/utils/PostgresUtils.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/utils/PostgresUtils.java @@ -11,6 +11,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.sql.PreparedStatement; @@ -351,6 +352,24 @@ public static String parseNonCompositeFilter( return filterString.toString(); } + private static Object prepareJsonValueForContainsOp(final Object value) { + if (value instanceof Document) { + try { + final Document document = (Document) value; + final JsonNode node = OBJECT_MAPPER.readTree(document.toJson()); + if (node.isArray()) { + return document.toJson(); + } else { + return "[" + document.toJson() + "]"; + } + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } else { + return prepareValueForContainsOp(value); + } + } + private static Object prepareValueForContainsOp(Object value) { String transformedValue = null; try { @@ -381,6 +400,7 @@ public static String prepareParsedNonCompositeFilter( StringBuilder filterString = new StringBuilder(preparedExpression); String sqlOperator; boolean isMultiValued = false; + boolean isContainsOp = false; switch (op) { case "EQ": case "=": @@ -437,8 +457,17 @@ public static String prepareParsedNonCompositeFilter( sqlOperator = " != "; break; case "CONTAINS": + isContainsOp = true; + value = prepareJsonValueForContainsOp(value); + sqlOperator = " @> "; + break; case "NOT_CONTAINS": - // For now, both contains and not_contains are not supported in aggregation filter. + case "NOT CONTAINS": + isContainsOp = true; + filterString = filterString.append(" IS NULL OR NOT ").append(preparedExpression); + value = prepareJsonValueForContainsOp(value); + sqlOperator = " @> "; + break; default: throw new UnsupportedOperationException(UNSUPPORTED_QUERY_OPERATION); } @@ -447,6 +476,10 @@ public static String prepareParsedNonCompositeFilter( if (value != null) { if (isMultiValued) { filterString.append(value); + } else if (isContainsOp) { + filterString.append(QUESTION_MARK); + filterString.append("::jsonb"); + paramsBuilder.addObjectParam(value); } else { filterString.append(QUESTION_MARK); paramsBuilder.addObjectParam(value); diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/PostgresQueryParserTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/PostgresQueryParserTest.java index 46e200a0..159f890c 100644 --- a/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/PostgresQueryParserTest.java +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/PostgresQueryParserTest.java @@ -13,17 +13,21 @@ import static org.hypertrace.core.documentstore.expression.operators.FunctionOperator.MULTIPLY; import static org.hypertrace.core.documentstore.expression.operators.LogicalOperator.AND; import static org.hypertrace.core.documentstore.expression.operators.LogicalOperator.OR; +import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.CONTAINS; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.EQ; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.GT; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.GTE; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.IN; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.LTE; import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NEQ; +import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.NOT_CONTAINS; import static org.hypertrace.core.documentstore.expression.operators.SortOrder.ASC; import static org.hypertrace.core.documentstore.expression.operators.SortOrder.DESC; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.IOException; import java.util.List; +import org.hypertrace.core.documentstore.JSONDocument; import org.hypertrace.core.documentstore.SingleValueKey; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; @@ -1297,4 +1301,93 @@ public void testFindWithKeyAndRelationalFilter() { assertEquals(TENANT_ID + ":7", params.getObjectParams().get(1)); assertEquals("Comb", params.getObjectParams().get(2)); } + + @Test + void testContainsFilter() throws IOException { + Query query = + Query.builder() + .setFilter( + RelationalExpression.of( + IdentifierExpression.of("sales"), + CONTAINS, + ConstantExpression.of(new JSONDocument("\"a\"")))) + .build(); + PostgresQueryParser postgresQueryParser = + new PostgresQueryParser(TEST_COLLECTION, PostgresQueryTransformer.transform(query)); + String sql = postgresQueryParser.parse(); + assertEquals("SELECT * FROM testCollection WHERE document->'sales' @> ?::jsonb", sql); + + Params params = postgresQueryParser.getParamsBuilder().build(); + assertEquals("[\"a\"]", params.getObjectParams().get(1)); + } + + @Test + void testContainsAndUnnestFilters() throws IOException { + org.hypertrace.core.documentstore.query.Query query = + org.hypertrace.core.documentstore.query.Query.builder() + .addSelection(IdentifierExpression.of("item")) + .addSelection(IdentifierExpression.of("sales.medium")) + .addFromClause( + UnnestExpression.builder() + .identifierExpression(IdentifierExpression.of("sales")) + .preserveNullAndEmptyArrays(false) + .filterTypeExpression( + RelationalExpression.of( + IdentifierExpression.of("sales.medium"), + CONTAINS, + ConstantExpression.of( + new JSONDocument("{\"type\": \"retail\",\"volume\": 500}")))) + .build()) + .build(); + + PostgresQueryParser postgresQueryParser = + new PostgresQueryParser(TEST_COLLECTION, PostgresQueryTransformer.transform(query)); + String sql = postgresQueryParser.parse(); + + assertEquals( + "With \n" + + "table0 as (SELECT * from testCollection),\n" + + "table1 as (SELECT * from table0 t0, jsonb_array_elements(document->'sales') p1(sales))\n" + + "SELECT document->'item' AS item, sales->'medium' AS sales_dot_medium FROM table1 WHERE sales->'medium' @> ?::jsonb", + sql); + + Params params = postgresQueryParser.getParamsBuilder().build(); + assertEquals(1, params.getObjectParams().size()); + assertEquals("[{\"type\":\"retail\",\"volume\":500}]", params.getObjectParams().get(1)); + } + + @Test + void testNotContainsAndUnnestFilters() throws IOException { + org.hypertrace.core.documentstore.query.Query query = + org.hypertrace.core.documentstore.query.Query.builder() + .addSelection(IdentifierExpression.of("item")) + .addSelection(IdentifierExpression.of("sales.medium")) + .addFromClause( + UnnestExpression.builder() + .identifierExpression(IdentifierExpression.of("sales")) + .preserveNullAndEmptyArrays(false) + .filterTypeExpression( + RelationalExpression.of( + IdentifierExpression.of("sales.medium"), + NOT_CONTAINS, + ConstantExpression.of( + new JSONDocument("{\"type\": \"retail\",\"volume\": 500}")))) + .build()) + .build(); + + PostgresQueryParser postgresQueryParser = + new PostgresQueryParser(TEST_COLLECTION, PostgresQueryTransformer.transform(query)); + String sql = postgresQueryParser.parse(); + + assertEquals( + "With \n" + + "table0 as (SELECT * from testCollection),\n" + + "table1 as (SELECT * from table0 t0, jsonb_array_elements(document->'sales') p1(sales))\n" + + "SELECT document->'item' AS item, sales->'medium' AS sales_dot_medium FROM table1 WHERE sales->'medium' IS NULL OR NOT sales->'medium' @> ?::jsonb", + sql); + + Params params = postgresQueryParser.getParamsBuilder().build(); + assertEquals(1, params.getObjectParams().size()); + assertEquals("[{\"type\":\"retail\",\"volume\":500}]", params.getObjectParams().get(1)); + } }