diff --git a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/ArrayFiltersQueryIntegrationTest.java b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/ArrayFiltersQueryIntegrationTest.java index e6361f213..318bafa67 100644 --- a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/ArrayFiltersQueryIntegrationTest.java +++ b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/ArrayFiltersQueryIntegrationTest.java @@ -296,6 +296,54 @@ void getSolarSystemsWithNoneOfThePlanetsHavingBothWaterAndOxygenTogether( JSONAssert.assertEquals(expected, actual, JSONCompareMode.LENIENT); } + /** + * Tests returning documents where environment IDs are a subset of the provided allowed + * environment IDs. + */ + @ParameterizedTest + @ArgumentsSource(AllProvider.class) + void getDocumentsWithEnvironmentIdsSubsetOfGivenList(final String dataStoreName) + throws JSONException, IOException { + final String testCollectionName = "environment_scope_test"; + final Datastore datastore = datastoreMap.get(dataStoreName); + final Map testDocuments = + Utils.buildDocumentsFromResource("query/array_operators/environment_scope_test.json"); + datastore.deleteCollection(testCollectionName); + datastore.createCollection(testCollectionName, null); + final Collection collection = datastore.getCollection(testCollectionName); + collection.bulkUpsert(testDocuments); + + final java.util.List environmentIds = java.util.List.of("env-1", "env-2", "env-3"); + + final Query query = + Query.builder() + .setFilter( + and( + RelationalExpression.of( + IdentifierExpression.of("scope.environmentScope.environmentIds"), + RelationalOperator.EXISTS, + ConstantExpression.of(true)), + not( + ArrayRelationalFilterExpression.builder() + .operator(ANY) + .filter( + RelationalExpression.of( + IdentifierExpression.of( + "scope.environmentScope.environmentIds"), + RelationalOperator.NOT_IN, + ConstantExpression.ofStrings(environmentIds))) + .build()))) + .build(); + + final Iterator documents = collection.aggregate(query); + final String expected = readResource("environment_ids_subset.json"); + final String actual = iteratorToJson(documents); + + datastore.deleteCollection(testCollectionName); + + JSONAssert.assertEquals(expected, actual, JSONCompareMode.LENIENT); + } + private String readResource(final String fileName) { try { return new String( diff --git a/document-store/src/integrationTest/resources/query/array_operators/environment_ids_subset.json b/document-store/src/integrationTest/resources/query/array_operators/environment_ids_subset.json new file mode 100644 index 000000000..199378982 --- /dev/null +++ b/document-store/src/integrationTest/resources/query/array_operators/environment_ids_subset.json @@ -0,0 +1,18 @@ +[ + { + "scope": { + "environmentScope": { + "environmentIds": ["env-1", "env-2"] + } + }, + "name": "Document A" + }, + { + "scope": { + "environmentScope": { + "environmentIds": ["env-1", "env-2", "env-3"] + } + }, + "name": "Document B" + } +] diff --git a/document-store/src/integrationTest/resources/query/array_operators/environment_scope_test.json b/document-store/src/integrationTest/resources/query/array_operators/environment_scope_test.json new file mode 100644 index 000000000..e10df4d2b --- /dev/null +++ b/document-store/src/integrationTest/resources/query/array_operators/environment_scope_test.json @@ -0,0 +1,42 @@ +[ + { + "_id": 1, + "name": "Document A", + "scope": { + "environmentScope": { + "environmentIds": ["env-1", "env-2"] + } + } + }, + { + "_id": 2, + "name": "Document B", + "scope": { + "environmentScope": { + "environmentIds": ["env-1", "env-2", "env-3"] + } + } + }, + { + "_id": 3, + "name": "Document C", + "scope": { + "environmentScope": { + "environmentIds": ["env-1", "env-4"] + } + } + }, + { + "_id": 4, + "name": "Document D", + "scope": { + "environmentScope": { + "environmentIds": ["env-5", "env-6"] + } + } + }, + { + "_id": 5, + "name": "Document E" + } +] diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoNotInExprRelationalFilterParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoNotInExprRelationalFilterParser.java new file mode 100644 index 000000000..8fd04bdd1 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoNotInExprRelationalFilterParser.java @@ -0,0 +1,26 @@ +package org.hypertrace.core.documentstore.mongo.query.parser.filter; + +import java.util.Map; +import lombok.AllArgsConstructor; +import org.hypertrace.core.documentstore.expression.impl.RelationalExpression; +import org.hypertrace.core.documentstore.mongo.query.parser.filter.MongoRelationalFilterParserFactory.MongoRelationalFilterContext; + +/** + * Note: MongoDB does not have a native "NOT IN" aggregation operator. This parser simulates "NOT + * IN" functionality by combining the $not and $in operators. + */ +@AllArgsConstructor +public class MongoNotInExprRelationalFilterParser implements MongoRelationalFilterParser { + private static final String NOT_OP = "$not"; + private static final String IN_OP = "$in"; + private static final MongoStandardRelationalOperatorMapping mapping = + new MongoStandardRelationalOperatorMapping(); + + @Override + public Map parse( + final RelationalExpression expression, final MongoRelationalFilterContext context) { + final String parsedLhs = expression.getLhs().accept(context.lhsParser()); + final Object parsedRhs = expression.getRhs().accept(context.rhsParser()); + return Map.of(NOT_OP, Map.of(IN_OP, new Object[] {parsedLhs, parsedRhs})); + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactoryImpl.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactoryImpl.java index 22f4d8cf9..4381086c7 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactoryImpl.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactoryImpl.java @@ -26,8 +26,22 @@ public MongoRelationalFilterParser parser( } case IN: + if (INSIDE_EXPR.equals(context.location())) { + return new MongoStandardExprRelationalFilterParser(); + } else if (OUTSIDE_EXPR.equals(context.location())) { + return new MongoStandardNonExprRelationalFilterParser(); + } else { + throw new UnsupportedOperationException("Unsupported location: " + context.location()); + } + case NOT_IN: - return new MongoStandardNonExprRelationalFilterParser(); + if (INSIDE_EXPR.equals(context.location())) { + return new MongoNotInExprRelationalFilterParser(); + } else if (OUTSIDE_EXPR.equals(context.location())) { + return new MongoStandardNonExprRelationalFilterParser(); + } else { + throw new UnsupportedOperationException("Unsupported location: " + context.location()); + } case CONTAINS: return new MongoContainsRelationalFilterParser(); 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 7875119e6..2d9896a16 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 @@ -181,7 +181,7 @@ private String getFilterStringForAnyOperator(final ArrayRelationalFilterExpressi .accept(new PostgresIdentifierExpressionVisitor(postgresQueryParser)); // If the field name is 'elements.inner', alias becomes 'elements_dot_inner' - final String alias = encodeAliasForNestedField(identifierName); + final String alias = encodeAliasForNestedField(identifierName).toLowerCase(); // Any LHS field name (elements) is to be prefixed with current alias (elements_dot_inner) final PostgresWrappingFilterVisitorProvider visitorProvider =