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 8a1c65ca..979fd446 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 @@ -85,7 +85,9 @@ import org.hypertrace.core.documentstore.commons.DocStoreConstants; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.ArrayRelationalFilterExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayType; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; @@ -300,6 +302,24 @@ public Stream provideArguments(final ExtensionContext context) { } } + /** + * Provides arguments for testing array operations with different expression types. Returns: + * (datastoreName, expressionType) - "WITH_TYPE": ArrayIdentifierExpression WITH ArrayType + * (optimized, type-aware casting) - "WITHOUT_TYPE": ArrayIdentifierExpression WITHOUT ArrayType + * (fallback, text[] casting) + */ + private static class PostgresArrayTypeProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(final ExtensionContext context) { + return Stream.of( + Arguments.of(POSTGRES_STORE, "WITH_TYPE"), // ArrayIdentifierExpression WITH ArrayType + Arguments.of( + POSTGRES_STORE, "WITHOUT_TYPE") // ArrayIdentifierExpression WITHOUT ArrayType + ); + } + } + @ParameterizedTest @ArgumentsSource(AllProvider.class) public void testFindAll(String dataStoreName) throws IOException { @@ -3267,6 +3287,228 @@ void testFlatPostgresCollectionCount(String dataStoreName) { assertEquals(3, soapCountQuery); } + /** + * Tests IN and NOT_IN operators on primitive (non-JSON) fields in flat collections. These + * operators should use simple SQL IN clause instead of array overlap operator for optimal index + * usage. + */ + @ParameterizedTest + @ArgumentsSource(PostgresProvider.class) + void testFlatPostgresCollectionInAndNotInOperators(String dataStoreName) { + Datastore datastore = datastoreMap.get(dataStoreName); + Collection flatCollection = + datastore.getCollectionForType(FLAT_COLLECTION_NAME, DocumentType.FLAT); + + // Test 1: IN operator on _id field + // Expected: 3 documents (IDs 1, 3, 5) + Query idInQuery = + Query.builder() + .setFilter( + RelationalExpression.of( + IdentifierExpression.of("_id"), + IN, + ConstantExpression.ofNumbers(List.of(1, 3, 5)))) + .build(); + + long idInCount = flatCollection.count(idInQuery); + assertEquals(3, idInCount, "IN operator on _id should find 3 documents"); + + // Test 2: IN operator on item field (string) + // Expected: 5 documents (IDs 1, 3, 4 for Shampoo and 1, 5, 8 for Soap) + Query itemInQuery = + Query.builder() + .setFilter( + RelationalExpression.of( + IdentifierExpression.of("item"), + IN, + ConstantExpression.ofStrings(List.of("Soap", "Shampoo")))) + .build(); + + long itemInCount = flatCollection.count(itemInQuery); + assertEquals( + 5, itemInCount, "IN operator on item should find 5 documents (3 Soap + 2 Shampoo)"); + + // Test 3: IN operator on price field (numeric) + // Expected: 5 documents (IDs 1, 8 for price=10 and 3, 4 for price=5) + Query priceInQuery = + Query.builder() + .setFilter( + RelationalExpression.of( + IdentifierExpression.of("price"), + IN, + ConstantExpression.ofNumbers(List.of(5, 10)))) + .build(); + + long priceInCount = flatCollection.count(priceInQuery); + assertEquals(4, priceInCount, "IN operator on price should find 4 documents"); + + // Test 4: NOT_IN operator on _id field + // Expected: 7 documents (all except IDs 1, 3, 5) + Query idNotInQuery = + Query.builder() + .setFilter( + RelationalExpression.of( + IdentifierExpression.of("_id"), + NOT_IN, + ConstantExpression.ofNumbers(List.of(1, 3, 5)))) + .build(); + + long idNotInCount = flatCollection.count(idNotInQuery); + assertEquals(7, idNotInCount, "NOT_IN operator on _id should find 7 documents"); + + // Test 5: NOT_IN operator on item field + // Expected: 5 documents (all except Soap items: IDs 2, 3, 4, 6, 7, 9, 10) + Query itemNotInQuery = + Query.builder() + .setFilter( + RelationalExpression.of( + IdentifierExpression.of("item"), + NOT_IN, + ConstantExpression.ofStrings(List.of("Soap")))) + .build(); + + long itemNotInCount = flatCollection.count(itemNotInQuery); + assertEquals(7, itemNotInCount, "NOT_IN operator on item should find 7 documents"); + + // Test 6: Combined IN with other filters (AND) + // Filter: _id IN (1, 3, 5, 7) AND price >= 10 + // Expected: 2 documents (ID 1 with price=10, ID 5 with price=20) + Query combinedQuery = + Query.builder() + .setFilter( + LogicalExpression.builder() + .operator(LogicalOperator.AND) + .operand( + RelationalExpression.of( + IdentifierExpression.of("_id"), + IN, + ConstantExpression.ofNumbers(List.of(1, 3, 5, 7)))) + .operand( + RelationalExpression.of( + IdentifierExpression.of("price"), GTE, ConstantExpression.of(10))) + .build()) + .build(); + + long combinedCount = flatCollection.count(combinedQuery); + assertEquals(2, combinedCount, "Combined IN with >= filter should find 2 documents"); + } + + /** + * Tests IN and NOT_IN operators on array fields in flat collections. Array fields use the + * PostgreSQL array overlap operator (&&) for IN operations, which checks if the array contains + * ANY of the provided values. + * + *

This test is parameterized to test three scenarios: 1. ArrayIdentifierExpression WITH + * ArrayType - optimized queries with type-aware casting 2. ArrayIdentifierExpression WITHOUT + * ArrayType - fallback with text[] casting both sides 3. IdentifierExpression - backward + * compatibility with text[] casting both sides + */ + @ParameterizedTest + @ArgumentsSource(PostgresArrayTypeProvider.class) + void testFlatPostgresCollectionInAndNotInOperatorsForArrays( + String dataStoreName, String expressionType) { + Datastore datastore = datastoreMap.get(dataStoreName); + Collection flatCollection = + datastore.getCollectionForType(FLAT_COLLECTION_NAME, DocumentType.FLAT); + + String typeDesc = + expressionType.equals("WITH_TYPE") + ? "WITH ArrayType (optimized)" + : "WITHOUT ArrayType (fallback)"; + + // Test 1: IN operator on tags array field (string array) + // Find documents where tags contains "hygiene" OR "grooming" + // Expected: IDs 1, 5, 8 (hygiene) + IDs 6, 7 (grooming) = 5 documents + Query tagsInQuery = + Query.builder() + .setFilter( + RelationalExpression.of( + expressionType.equals("WITH_TYPE") + ? ArrayIdentifierExpression.of("tags", ArrayType.TEXT) + : ArrayIdentifierExpression.of("tags"), + IN, + ConstantExpression.ofStrings(List.of("hygiene", "grooming")))) + .build(); + + long tagsInCount = flatCollection.count(tagsInQuery); + assertEquals( + 5, + tagsInCount, + String.format( + "IN operator on tags array %s should find 5 documents with hygiene or grooming", + typeDesc)); + + // Test 2: IN operator on numbers array field (numeric array) + // Find documents where numbers array contains 1 OR 10 + // Expected: ID 1 has {1,2,3}, ID 2 has {10,20} = 2 documents + Query numbersInQuery = + Query.builder() + .setFilter( + RelationalExpression.of( + expressionType.equals("WITH_TYPE") + ? ArrayIdentifierExpression.of("numbers", ArrayType.INTEGER) + : ArrayIdentifierExpression.of("numbers"), + IN, + ConstantExpression.ofNumbers(List.of(1, 10)))) + .build(); + + long numbersInCount = flatCollection.count(numbersInQuery); + assertEquals( + 2, + numbersInCount, + String.format("IN operator on numbers array %s should find 2 documents", typeDesc)); + + // Test 3: NOT_IN operator on tags array field + // Find documents where tags does NOT contain "hygiene" + // Expected: All documents except IDs 1, 5, 8 = 7 documents + // Note: This includes NULL tags (ID 9) and empty array (ID 10) + Query tagsNotInQuery = + Query.builder() + .setFilter( + RelationalExpression.of( + expressionType.equals("WITH_TYPE") + ? ArrayIdentifierExpression.of("tags", ArrayType.TEXT) + : ArrayIdentifierExpression.of("tags"), + NOT_IN, + ConstantExpression.ofStrings(List.of("hygiene")))) + .build(); + + long tagsNotInCount = flatCollection.count(tagsNotInQuery); + assertEquals( + 7, + tagsNotInCount, + String.format( + "NOT_IN operator on tags array %s should find 7 documents without hygiene", + typeDesc)); + + // Test 4: Combined array IN with scalar filter + // Find documents where tags contains "premium" AND price >= 5 + // Expected: ID 1 (premium, price=10) + ID 3 (premium, price=5) = 2 documents + Query combinedArrayQuery = + Query.builder() + .setFilter( + LogicalExpression.builder() + .operator(LogicalOperator.AND) + .operand( + RelationalExpression.of( + expressionType.equals("WITH_TYPE") + ? ArrayIdentifierExpression.of("tags", ArrayType.TEXT) + : ArrayIdentifierExpression.of("tags"), + IN, + ConstantExpression.ofStrings(List.of("premium")))) + .operand( + RelationalExpression.of( + IdentifierExpression.of("price"), GTE, ConstantExpression.of(5))) + .build()) + .build(); + + long combinedArrayCount = flatCollection.count(combinedArrayQuery); + assertEquals( + 2, + combinedArrayCount, + String.format("Combined array IN with >= filter %s should find 2 documents", typeDesc)); + } + /** * This test is disabled for now because flat collections do not support search on nested * queries in JSONB fields (ex. props.brand) diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ArrayIdentifierExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ArrayIdentifierExpression.java index e3e3d31f..fce4cc5d 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ArrayIdentifierExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ArrayIdentifierExpression.java @@ -1,6 +1,8 @@ package org.hypertrace.core.documentstore.expression.impl; +import java.util.Optional; import lombok.EqualsAndHashCode; +import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; /** * Represents an identifier expression for array-typed fields. This allows parsers to apply @@ -12,11 +14,36 @@ @EqualsAndHashCode(callSuper = true) public class ArrayIdentifierExpression extends IdentifierExpression { + private final ArrayType arrayType; + public ArrayIdentifierExpression(String name) { + this(name, null); + } + + public ArrayIdentifierExpression(String name, ArrayType arrayType) { super(name); + this.arrayType = arrayType; } public static ArrayIdentifierExpression of(String name) { return new ArrayIdentifierExpression(name); } + + public static ArrayIdentifierExpression of(String name, ArrayType arrayType) { + return new ArrayIdentifierExpression(name, arrayType); + } + + /** Returns the array type if specified, empty otherwise */ + public Optional getArrayType() { + return Optional.ofNullable(arrayType); + } + + /** + * Accepts a SelectTypeExpressionVisitor and dispatches to the ArrayIdentifierExpression-specific + * visit method. + */ + @Override + public T accept(final SelectTypeExpressionVisitor visitor) { + return visitor.visit(this); + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ArrayType.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ArrayType.java new file mode 100644 index 00000000..d021b928 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/ArrayType.java @@ -0,0 +1,16 @@ +package org.hypertrace.core.documentstore.expression.impl; + +import lombok.Getter; + +public enum ArrayType { + TEXT("text[]"), + INTEGER("integer[]"), + BOOLEAN("boolean[]"), + DOUBLE_PRECISION("double precision[]"); + + @Getter private final String postgresType; + + ArrayType(String postgresType) { + this.postgresType = postgresType; + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JsonIdentifierExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JsonIdentifierExpression.java index a9de5946..88191739 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JsonIdentifierExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JsonIdentifierExpression.java @@ -4,6 +4,7 @@ import lombok.EqualsAndHashCode; import lombok.Getter; import org.hypertrace.core.documentstore.parser.FieldTransformationVisitor; +import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; import org.hypertrace.core.documentstore.postgres.utils.BasicPostgresSecurityValidator; /** @@ -68,6 +69,15 @@ public T accept(final FieldTransformationVisitor visitor) { return visitor.visit(this); } + /** + * Accepts a SelectTypeExpressionVisitor and dispatches to the JsonIdentifierExpression-specific + * visit method. + */ + @Override + public T accept(final SelectTypeExpressionVisitor visitor) { + return visitor.visit(this); + } + @Override public String toString() { return String.format( 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 a6e0ed0e..d90fc64b 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,10 +2,12 @@ import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression; 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.JsonIdentifierExpression; public interface SelectTypeExpressionVisitor { T visit(final AggregateExpression expression); @@ -19,4 +21,20 @@ public interface SelectTypeExpressionVisitor { T visit(final IdentifierExpression expression); T visit(final AliasedIdentifierExpression expression); + + /** + * Visit an ArrayIdentifierExpression. Default implementation delegates to + * visit(IdentifierExpression) since ArrayIdentifierExpression extends IdentifierExpression. + */ + default T visit(final ArrayIdentifierExpression expression) { + return visit((IdentifierExpression) expression); + } + + /** + * Visit a JsonIdentifierExpression. Default implementation delegates to + * visit(IdentifierExpression) since JsonIdentifierExpression extends IdentifierExpression. + */ + default T visit(final JsonIdentifierExpression expression) { + return visit((IdentifierExpression) expression); + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresContainsParserSelector.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresContainsParserSelector.java new file mode 100644 index 00000000..a09deeb9 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresContainsParserSelector.java @@ -0,0 +1,66 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter; + +import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression; +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.JsonIdentifierExpression; +import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresContainsRelationalFilterParserNonJsonField; + +class PostgresContainsParserSelector implements SelectTypeExpressionVisitor { + + private static final PostgresContainsRelationalFilterParserInterface jsonFieldContainsParser = + new PostgresContainsRelationalFilterParser(); + private static final PostgresContainsRelationalFilterParserInterface nonJsonFieldContainsParser = + new PostgresContainsRelationalFilterParserNonJsonField(); + + private final boolean isFlatCollection; + + PostgresContainsParserSelector(boolean isFlatCollection) { + this.isFlatCollection = isFlatCollection; + } + + @Override + public PostgresRelationalFilterParser visit(JsonIdentifierExpression expression) { + return jsonFieldContainsParser; + } + + @Override + public PostgresRelationalFilterParser visit(ArrayIdentifierExpression expression) { + return isFlatCollection ? nonJsonFieldContainsParser : jsonFieldContainsParser; + } + + @Override + public PostgresRelationalFilterParser visit(IdentifierExpression expression) { + return isFlatCollection ? nonJsonFieldContainsParser : jsonFieldContainsParser; + } + + @Override + public PostgresRelationalFilterParser visit(AggregateExpression expression) { + return isFlatCollection ? nonJsonFieldContainsParser : jsonFieldContainsParser; + } + + @Override + public PostgresRelationalFilterParser visit(ConstantExpression expression) { + return isFlatCollection ? nonJsonFieldContainsParser : jsonFieldContainsParser; + } + + @Override + public PostgresRelationalFilterParser visit(DocumentConstantExpression expression) { + return isFlatCollection ? nonJsonFieldContainsParser : jsonFieldContainsParser; + } + + @Override + public PostgresRelationalFilterParser visit(FunctionExpression expression) { + return isFlatCollection ? nonJsonFieldContainsParser : jsonFieldContainsParser; + } + + @Override + public PostgresRelationalFilterParser visit(AliasedIdentifierExpression expression) { + return isFlatCollection ? nonJsonFieldContainsParser : jsonFieldContainsParser; + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresInParserSelector.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresInParserSelector.java new file mode 100644 index 00000000..a7a86460 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresInParserSelector.java @@ -0,0 +1,71 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter; + +import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression; +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.JsonIdentifierExpression; +import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserArrayField; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserScalarField; + +class PostgresInParserSelector implements SelectTypeExpressionVisitor { + + private static final PostgresInRelationalFilterParserInterface jsonFieldInFilterParser = + new PostgresInRelationalFilterParser(); + private static final PostgresInRelationalFilterParserInterface scalarFieldInFilterParser = + new PostgresInRelationalFilterParserScalarField(); + private static final PostgresInRelationalFilterParserInterface arrayFieldInFilterParser = + new PostgresInRelationalFilterParserArrayField(); + + private final boolean isFlatCollection; + + PostgresInParserSelector(boolean isFlatCollection) { + this.isFlatCollection = isFlatCollection; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(JsonIdentifierExpression expression) { + return jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(ArrayIdentifierExpression expression) { + // Array fields with explicit ArrayIdentifierExpression use optimized array parser + return isFlatCollection ? arrayFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(IdentifierExpression expression) { + // IdentifierExpression: use scalar parser in flat, JSON parser in nested + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(AggregateExpression expression) { + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(ConstantExpression expression) { + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(DocumentConstantExpression expression) { + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(FunctionExpression expression) { + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(AliasedIdentifierExpression expression) { + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresNotInParserSelector.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresNotInParserSelector.java new file mode 100644 index 00000000..a86b249d --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresNotInParserSelector.java @@ -0,0 +1,72 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter; + +import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression; +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.JsonIdentifierExpression; +import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserArrayField; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserScalarField; + +class PostgresNotInParserSelector implements SelectTypeExpressionVisitor { + + private static final PostgresInRelationalFilterParserInterface jsonFieldInFilterParser = + new PostgresInRelationalFilterParser(); + private static final PostgresInRelationalFilterParserInterface scalarFieldInFilterParser = + new PostgresInRelationalFilterParserScalarField(); + private static final PostgresInRelationalFilterParserInterface arrayFieldInFilterParser = + new PostgresInRelationalFilterParserArrayField(); + + private final boolean isFlatCollection; + + PostgresNotInParserSelector(boolean isFlatCollection) { + this.isFlatCollection = isFlatCollection; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(JsonIdentifierExpression expression) { + return jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(ArrayIdentifierExpression expression) { + // Array fields with explicit ArrayIdentifierExpression use optimized array parser + return isFlatCollection ? arrayFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(IdentifierExpression expression) { + // IdentifierExpression: use scalar parser in flat, JSON parser in nested + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(AggregateExpression expression) { + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(ConstantExpression expression) { + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(DocumentConstantExpression expression) { + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } + + @Override + public PostgresInRelationalFilterParserInterface visit(FunctionExpression expression) { + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } + + @SuppressWarnings("unchecked") + @Override + public PostgresInRelationalFilterParserInterface visit(AliasedIdentifierExpression expression) { + return isFlatCollection ? scalarFieldInFilterParser : jsonFieldInFilterParser; + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresNotInRelationalFilterParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresNotInRelationalFilterParser.java index ce204c5b..1f66bf7b 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresNotInRelationalFilterParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresNotInRelationalFilterParser.java @@ -1,42 +1,20 @@ package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter; import org.hypertrace.core.documentstore.DocumentType; -import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.RelationalExpression; -import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserNonJsonField; class PostgresNotInRelationalFilterParser implements PostgresRelationalFilterParser { - private static final PostgresInRelationalFilterParserInterface jsonFieldInFilterParser = - new PostgresInRelationalFilterParser(); - private static final PostgresInRelationalFilterParserInterface nonJsonFieldInFilterParser = - new PostgresInRelationalFilterParserNonJsonField(); - @Override public String parse( final RelationalExpression expression, final PostgresRelationalFilterContext context) { final String parsedLhs = expression.getLhs().accept(context.lhsParser()); + boolean isFlatCollection = context.getPgColTransformer().getDocumentType() == DocumentType.FLAT; PostgresInRelationalFilterParserInterface inFilterParser = - getInFilterParser(expression, context); + expression.getLhs().accept(new PostgresNotInParserSelector(isFlatCollection)); final String parsedInExpression = inFilterParser.parse(expression, context); return String.format("%s IS NULL OR NOT (%s)", parsedLhs, parsedInExpression); } - - private PostgresInRelationalFilterParserInterface getInFilterParser( - final RelationalExpression expression, PostgresRelationalFilterContext context) { - // Check if LHS is a JSON field (JSONB column access) - boolean isJsonField = expression.getLhs() instanceof JsonIdentifierExpression; - - // Check if the collection type is flat or nested - boolean isFlatCollection = context.getPgColTransformer().getDocumentType() == DocumentType.FLAT; - - // Use JSON parser for: - // 1. Nested collections - !isFlatCollection - // 2. JSON fields within flat collections - isJsonField - boolean useJsonParser = !isFlatCollection || isJsonField; - - return useJsonParser ? jsonFieldInFilterParser : nonJsonFieldInFilterParser; - } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresRelationalFilterParserFactoryImpl.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresRelationalFilterParserFactoryImpl.java index 7df0d94a..41a13b7c 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresRelationalFilterParserFactoryImpl.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresRelationalFilterParserFactoryImpl.java @@ -13,12 +13,9 @@ import com.google.common.collect.Maps; import java.util.Map; import org.hypertrace.core.documentstore.DocumentType; -import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.RelationalExpression; import org.hypertrace.core.documentstore.expression.operators.RelationalOperator; import org.hypertrace.core.documentstore.postgres.query.v1.PostgresQueryParser; -import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresContainsRelationalFilterParserNonJsonField; -import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserNonJsonField; public class PostgresRelationalFilterParserFactoryImpl implements PostgresRelationalFilterParserFactory { @@ -34,18 +31,6 @@ public class PostgresRelationalFilterParserFactoryImpl entry(LIKE, new PostgresLikeRelationalFilterParser()), entry(STARTS_WITH, new PostgresStartsWithRelationalFilterParser()))); - // IN filter parsers - private static final PostgresInRelationalFilterParserInterface jsonFieldInFilterParser = - new PostgresInRelationalFilterParser(); - private static final PostgresInRelationalFilterParserInterface nonJsonFieldInFilterParser = - new PostgresInRelationalFilterParserNonJsonField(); - - // CONTAINS parser implementations - private static final PostgresContainsRelationalFilterParserInterface jsonFieldContainsParser = - new PostgresContainsRelationalFilterParser(); - private static final PostgresContainsRelationalFilterParserInterface nonJsonFieldContainsParser = - new PostgresContainsRelationalFilterParserNonJsonField(); - private static final PostgresStandardRelationalFilterParser postgresStandardRelationalFilterParser = new PostgresStandardRelationalFilterParser(); @@ -53,19 +38,13 @@ public class PostgresRelationalFilterParserFactoryImpl public PostgresRelationalFilterParser parser( final RelationalExpression expression, final PostgresQueryParser postgresQueryParser) { - // Check if LHS is a JSON field (JSONB column access) - boolean isJsonField = expression.getLhs() instanceof JsonIdentifierExpression; - - // Check if the collection type is flat or nested boolean isFlatCollection = postgresQueryParser.getPgColTransformer().getDocumentType() == DocumentType.FLAT; - boolean useJsonParser = !isFlatCollection || isJsonField; - if (expression.getOperator() == CONTAINS) { - return useJsonParser ? jsonFieldContainsParser : nonJsonFieldContainsParser; + return expression.getLhs().accept(new PostgresContainsParserSelector(isFlatCollection)); } else if (expression.getOperator() == IN) { - return useJsonParser ? jsonFieldInFilterParser : nonJsonFieldInFilterParser; + return expression.getLhs().accept(new PostgresInParserSelector(isFlatCollection)); } return parserMap.getOrDefault(expression.getOperator(), postgresStandardRelationalFilterParser); diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresArrayTypeExtractor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresArrayTypeExtractor.java new file mode 100644 index 00000000..6d58153b --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresArrayTypeExtractor.java @@ -0,0 +1,80 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field; + +import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayType; +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.JsonIdentifierExpression; +import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; + +/** + * Visitor to extract PostgreSQL array type information from {@link ArrayIdentifierExpression}. + * + *

This visitor is specifically designed to work ONLY with {@link ArrayIdentifierExpression}. Any + * other expression type will throw {@link UnsupportedOperationException} to catch programming + * errors early. + * + *

Returns: + * + *

+ */ +class PostgresArrayTypeExtractor implements SelectTypeExpressionVisitor { + + public PostgresArrayTypeExtractor() {} + + @Override + public String visit(ArrayIdentifierExpression expression) { + return expression.getArrayType().map(ArrayType::getPostgresType).orElse(null); + } + + @Override + public String visit(JsonIdentifierExpression expression) { + throw unsupportedExpression("JsonIdentifierExpression"); + } + + @Override + public String visit(IdentifierExpression expression) { + throw new UnsupportedOperationException( + "PostgresArrayTypeExtractor should only be used with ArrayIdentifierExpression. " + + "Use IdentifierExpression only for scalar fields, not arrays."); + } + + @Override + public String visit(AggregateExpression expression) { + throw unsupportedExpression("AggregateExpression"); + } + + @Override + public String visit(ConstantExpression expression) { + throw unsupportedExpression("ConstantExpression"); + } + + @Override + public String visit(DocumentConstantExpression expression) { + throw unsupportedExpression("DocumentConstantExpression"); + } + + @Override + public String visit(FunctionExpression expression) { + throw unsupportedExpression("FunctionExpression"); + } + + @Override + public String visit(AliasedIdentifierExpression expression) { + throw unsupportedExpression("AliasedIdentifierExpression"); + } + + private static UnsupportedOperationException unsupportedExpression(String expressionType) { + return new UnsupportedOperationException( + "PostgresArrayTypeExtractor should only be used with ArrayIdentifierExpression, not " + + expressionType); + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresInRelationalFilterParserArrayField.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresInRelationalFilterParserArrayField.java new file mode 100644 index 00000000..b30446f8 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresInRelationalFilterParserArrayField.java @@ -0,0 +1,69 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field; + +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import org.hypertrace.core.documentstore.expression.impl.RelationalExpression; +import org.hypertrace.core.documentstore.postgres.Params; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.PostgresInRelationalFilterParserInterface; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.PostgresRelationalFilterParser; + +/** + * Implementation of PostgresInRelationalFilterParserInterface for handling IN operations on array + * fields (non-JSON array columns), using the PostgreSQL array overlap operator (&&). + * + *

For array fields like "tags", the IN operator semantics are: "does the array contain ANY of + * the provided values?" This is implemented using the PostgreSQL array overlap operator (&&). + * + *

Example: tags IN ('hygiene', 'premium') translates to: tags && ARRAY['hygiene', + * 'premium']::text[] + */ +public class PostgresInRelationalFilterParserArrayField + implements PostgresInRelationalFilterParserInterface { + + @Override + public String parse( + final RelationalExpression expression, + final PostgresRelationalFilterParser.PostgresRelationalFilterContext context) { + final String parsedLhs = expression.getLhs().accept(context.lhsParser()); + final Iterable parsedRhs = expression.getRhs().accept(context.rhsParser()); + + String arrayTypeCast = expression.getLhs().accept(new PostgresArrayTypeExtractor()); + + return prepareFilterStringForInOperator( + parsedLhs, parsedRhs, arrayTypeCast, context.getParamsBuilder()); + } + + private String prepareFilterStringForInOperator( + final String parsedLhs, + final Iterable parsedRhs, + final String arrayType, + final Params.Builder paramsBuilder) { + + String placeholders = + StreamSupport.stream(parsedRhs.spliterator(), false) + .map( + value -> { + paramsBuilder.addObjectParam(value); + return "?"; + }) + .collect(Collectors.joining(", ")); + + // Use array overlap operator for array fields + if (arrayType != null) { + // Type-aware optimization + if (arrayType.equals("text[]")) { + // cast RHS to text[] otherwise JDBC binds it as character varying[]. + return String.format("%s && ARRAY[%s]::text[]", parsedLhs, placeholders); + } else { + // INTEGER/BOOLEAN arrays: No casting needed, JDBC binds them correctly + // "numbers" && ARRAY[?, ?] (PostgreSQL infers integer[]) + // "flags" && ARRAY[?, ?] (PostgreSQL infers boolean[]) + return String.format("%s && ARRAY[%s]", parsedLhs, placeholders); + } + } else { + // Fallback: Cast both LHS and RHS to text[] to avoid type mismatch issues. This has the worst + // performance because casting LHS doesn't let PG use indexes on this col + return String.format("%s::text[] && ARRAY[%s]::text[]", parsedLhs, placeholders); + } + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresInRelationalFilterParserNonJsonField.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresInRelationalFilterParserScalarField.java similarity index 88% rename from document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresInRelationalFilterParserNonJsonField.java rename to document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresInRelationalFilterParserScalarField.java index fd5a5f4d..274c12a8 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresInRelationalFilterParserNonJsonField.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresInRelationalFilterParserScalarField.java @@ -11,7 +11,7 @@ * Implementation of PostgresInRelationalFilterParserInterface for handling IN operations on * first-class fields (non-JSON columns), using the standard IN clause syntax. */ -public class PostgresInRelationalFilterParserNonJsonField +public class PostgresInRelationalFilterParserScalarField implements PostgresInRelationalFilterParserInterface { @Override @@ -38,7 +38,6 @@ private String prepareFilterStringForInOperator( }) .collect(Collectors.joining(", ")); - // return String.format("%s IN (%s)", parsedLhs, placeholders); - return String.format("ARRAY[%s]::text[] && ARRAY[%s]::text[]", parsedLhs, placeholders); + return String.format("%s IN (%s)", parsedLhs, placeholders); } } 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 67b0e63b..0bdac88e 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 @@ -677,7 +677,7 @@ void testFindWithSortingAndPagination() { + "\"quantity\" AS \"quantity\", " + "\"date\" AS \"date\" " + "FROM \"testCollection\" " - + "WHERE ARRAY[\"item\"]::text[] && ARRAY[?, ?, ?, ?]::text[] " + + "WHERE \"item\" IN (?, ?, ?, ?) " + "ORDER BY \"quantity\" DESC NULLS LAST,\"item\" ASC NULLS FIRST " + "OFFSET ? LIMIT ?", postgresQueryParser.parse()); @@ -1499,7 +1499,7 @@ void testNotInWithFlatCollectionNonJsonField() { String sql = postgresQueryParser.parse(); assertEquals( - "SELECT * FROM \"testCollection\" WHERE \"category\" IS NULL OR NOT (ARRAY[\"category\"]::text[] && ARRAY[?, ?]::text[])", + "SELECT * FROM \"testCollection\" WHERE \"category\" IS NULL OR NOT (\"category\" IN (?, ?))", sql); Params params = postgresQueryParser.getParamsBuilder().build(); diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresContainsParserSelectorTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresContainsParserSelectorTest.java new file mode 100644 index 00000000..7bb73acc --- /dev/null +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresContainsParserSelectorTest.java @@ -0,0 +1,113 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayType; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; +import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression; +import org.hypertrace.core.documentstore.expression.operators.AggregationOperator; +import org.hypertrace.core.documentstore.expression.operators.FunctionOperator; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresContainsRelationalFilterParserNonJsonField; +import org.junit.jupiter.api.Test; + +class PostgresContainsParserSelectorTest { + + @Test + void testVisitArrayIdentifierExpression_flatCollection() { + PostgresContainsParserSelector selector = new PostgresContainsParserSelector(true); + ArrayIdentifierExpression expr = ArrayIdentifierExpression.of("tags", ArrayType.TEXT); + PostgresRelationalFilterParser result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresContainsRelationalFilterParserNonJsonField.class, result); + } + + @Test + void testVisitArrayIdentifierExpression_nestedCollection() { + PostgresContainsParserSelector selector = new PostgresContainsParserSelector(false); + ArrayIdentifierExpression expr = ArrayIdentifierExpression.of("tags", ArrayType.TEXT); + PostgresRelationalFilterParser result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresContainsRelationalFilterParser.class, result); + } + + @Test + void testVisitJsonIdentifierExpression() { + PostgresContainsParserSelector selector = new PostgresContainsParserSelector(true); + JsonIdentifierExpression expr = JsonIdentifierExpression.of("customAttr", "field"); + PostgresRelationalFilterParser result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresContainsRelationalFilterParser.class, result); + } + + @Test + void testVisitIdentifierExpression_flatCollection() { + PostgresContainsParserSelector selector = new PostgresContainsParserSelector(true); + IdentifierExpression expr = IdentifierExpression.of("item"); + PostgresRelationalFilterParser result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresContainsRelationalFilterParserNonJsonField.class, result); + } + + @Test + void testVisitIdentifierExpression_nestedCollection() { + PostgresContainsParserSelector selector = new PostgresContainsParserSelector(false); + IdentifierExpression expr = IdentifierExpression.of("item"); + PostgresRelationalFilterParser result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresContainsRelationalFilterParser.class, result); + } + + @Test + void testVisitAggregateExpression_flatCollection() { + PostgresContainsParserSelector selector = new PostgresContainsParserSelector(true); + AggregateExpression expr = + AggregateExpression.of(AggregationOperator.COUNT, IdentifierExpression.of("item")); + PostgresRelationalFilterParser result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitConstantExpression() { + PostgresContainsParserSelector selector = new PostgresContainsParserSelector(true); + ConstantExpression expr = ConstantExpression.of("test"); + PostgresRelationalFilterParser result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitDocumentConstantExpression() { + PostgresContainsParserSelector selector = new PostgresContainsParserSelector(true); + ConstantExpression.DocumentConstantExpression expr = + (ConstantExpression.DocumentConstantExpression) + ConstantExpression.of((org.hypertrace.core.documentstore.Document) null); + PostgresRelationalFilterParser result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitFunctionExpression() { + PostgresContainsParserSelector selector = new PostgresContainsParserSelector(true); + FunctionExpression expr = + FunctionExpression.builder() + .operator(FunctionOperator.LENGTH) + .operand(IdentifierExpression.of("item")) + .build(); + PostgresRelationalFilterParser result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitAliasedIdentifierExpression() { + PostgresContainsParserSelector selector = new PostgresContainsParserSelector(true); + AliasedIdentifierExpression expr = + AliasedIdentifierExpression.builder().name("item").contextAlias("i").build(); + PostgresRelationalFilterParser result = selector.visit(expr); + assertNotNull(result); + } +} diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresInParserSelectorTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresInParserSelectorTest.java new file mode 100644 index 00000000..8f843a30 --- /dev/null +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresInParserSelectorTest.java @@ -0,0 +1,114 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayType; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; +import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression; +import org.hypertrace.core.documentstore.expression.operators.AggregationOperator; +import org.hypertrace.core.documentstore.expression.operators.FunctionOperator; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserArrayField; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserScalarField; +import org.junit.jupiter.api.Test; + +class PostgresInParserSelectorTest { + + @Test + void testVisitArrayIdentifierExpression_flatCollection() { + PostgresInParserSelector selector = new PostgresInParserSelector(true); + ArrayIdentifierExpression expr = ArrayIdentifierExpression.of("tags", ArrayType.TEXT); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresInRelationalFilterParserArrayField.class, result); + } + + @Test + void testVisitArrayIdentifierExpression_nestedCollection() { + PostgresInParserSelector selector = new PostgresInParserSelector(false); + ArrayIdentifierExpression expr = ArrayIdentifierExpression.of("tags", ArrayType.TEXT); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresInRelationalFilterParser.class, result); + } + + @Test + void testVisitJsonIdentifierExpression() { + PostgresInParserSelector selector = new PostgresInParserSelector(true); + JsonIdentifierExpression expr = JsonIdentifierExpression.of("customAttr", "field"); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresInRelationalFilterParser.class, result); + } + + @Test + void testVisitIdentifierExpression_flatCollection() { + PostgresInParserSelector selector = new PostgresInParserSelector(true); + IdentifierExpression expr = IdentifierExpression.of("item"); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresInRelationalFilterParserScalarField.class, result); + } + + @Test + void testVisitIdentifierExpression_nestedCollection() { + PostgresInParserSelector selector = new PostgresInParserSelector(false); + IdentifierExpression expr = IdentifierExpression.of("item"); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresInRelationalFilterParser.class, result); + } + + @Test + void testVisitAggregateExpression_flatCollection() { + PostgresInParserSelector selector = new PostgresInParserSelector(true); + AggregateExpression expr = + AggregateExpression.of(AggregationOperator.COUNT, IdentifierExpression.of("item")); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitConstantExpression() { + PostgresInParserSelector selector = new PostgresInParserSelector(true); + ConstantExpression expr = ConstantExpression.of("test"); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitDocumentConstantExpression() { + PostgresInParserSelector selector = new PostgresInParserSelector(true); + ConstantExpression.DocumentConstantExpression expr = + (ConstantExpression.DocumentConstantExpression) + ConstantExpression.of((org.hypertrace.core.documentstore.Document) null); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitFunctionExpression() { + PostgresInParserSelector selector = new PostgresInParserSelector(true); + FunctionExpression expr = + FunctionExpression.builder() + .operator(FunctionOperator.LENGTH) + .operand(IdentifierExpression.of("item")) + .build(); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitAliasedIdentifierExpression() { + PostgresInParserSelector selector = new PostgresInParserSelector(true); + AliasedIdentifierExpression expr = + AliasedIdentifierExpression.builder().name("item").contextAlias("i").build(); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + } +} diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresNotInParserSelectorTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresNotInParserSelectorTest.java new file mode 100644 index 00000000..20c4b668 --- /dev/null +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresNotInParserSelectorTest.java @@ -0,0 +1,114 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayType; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; +import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression; +import org.hypertrace.core.documentstore.expression.operators.AggregationOperator; +import org.hypertrace.core.documentstore.expression.operators.FunctionOperator; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserArrayField; +import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserScalarField; +import org.junit.jupiter.api.Test; + +class PostgresNotInParserSelectorTest { + + @Test + void testVisitArrayIdentifierExpression_flatCollection() { + PostgresNotInParserSelector selector = new PostgresNotInParserSelector(true); + ArrayIdentifierExpression expr = ArrayIdentifierExpression.of("tags", ArrayType.TEXT); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresInRelationalFilterParserArrayField.class, result); + } + + @Test + void testVisitArrayIdentifierExpression_nestedCollection() { + PostgresNotInParserSelector selector = new PostgresNotInParserSelector(false); + ArrayIdentifierExpression expr = ArrayIdentifierExpression.of("tags", ArrayType.TEXT); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresInRelationalFilterParser.class, result); + } + + @Test + void testVisitJsonIdentifierExpression() { + PostgresNotInParserSelector selector = new PostgresNotInParserSelector(true); + JsonIdentifierExpression expr = JsonIdentifierExpression.of("customAttr", "field"); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresInRelationalFilterParser.class, result); + } + + @Test + void testVisitIdentifierExpression_flatCollection() { + PostgresNotInParserSelector selector = new PostgresNotInParserSelector(true); + IdentifierExpression expr = IdentifierExpression.of("item"); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresInRelationalFilterParserScalarField.class, result); + } + + @Test + void testVisitIdentifierExpression_nestedCollection() { + PostgresNotInParserSelector selector = new PostgresNotInParserSelector(false); + IdentifierExpression expr = IdentifierExpression.of("item"); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + assertInstanceOf(PostgresInRelationalFilterParser.class, result); + } + + @Test + void testVisitAggregateExpression_flatCollection() { + PostgresNotInParserSelector selector = new PostgresNotInParserSelector(true); + AggregateExpression expr = + AggregateExpression.of(AggregationOperator.COUNT, IdentifierExpression.of("item")); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitConstantExpression() { + PostgresNotInParserSelector selector = new PostgresNotInParserSelector(true); + ConstantExpression expr = ConstantExpression.of("test"); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitDocumentConstantExpression() { + PostgresNotInParserSelector selector = new PostgresNotInParserSelector(true); + ConstantExpression.DocumentConstantExpression expr = + (ConstantExpression.DocumentConstantExpression) + ConstantExpression.of((org.hypertrace.core.documentstore.Document) null); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitFunctionExpression() { + PostgresNotInParserSelector selector = new PostgresNotInParserSelector(true); + FunctionExpression expr = + FunctionExpression.builder() + .operator(FunctionOperator.LENGTH) + .operand(IdentifierExpression.of("item")) + .build(); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + } + + @Test + void testVisitAliasedIdentifierExpression() { + PostgresNotInParserSelector selector = new PostgresNotInParserSelector(true); + AliasedIdentifierExpression expr = + AliasedIdentifierExpression.builder().name("item").contextAlias("i").build(); + PostgresInRelationalFilterParserInterface result = selector.visit(expr); + assertNotNull(result); + } +} diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresArrayTypeExtractorTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresArrayTypeExtractorTest.java new file mode 100644 index 00000000..39ed5de0 --- /dev/null +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/nonjson/field/PostgresArrayTypeExtractorTest.java @@ -0,0 +1,101 @@ +package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayType; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; +import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; +import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression; +import org.hypertrace.core.documentstore.expression.operators.FunctionOperator; +import org.junit.jupiter.api.Test; + +class PostgresArrayTypeExtractorTest { + + private final PostgresArrayTypeExtractor extractor = new PostgresArrayTypeExtractor(); + + @Test + void testVisitArrayIdentifierExpression_withType() { + ArrayIdentifierExpression expr = ArrayIdentifierExpression.of("tags", ArrayType.TEXT); + String result = extractor.visit(expr); + assertEquals("text[]", result); + } + + @Test + void testVisitArrayIdentifierExpression_withIntegerType() { + ArrayIdentifierExpression expr = ArrayIdentifierExpression.of("numbers", ArrayType.INTEGER); + String result = extractor.visit(expr); + assertEquals("integer[]", result); + } + + @Test + void testVisitArrayIdentifierExpression_withBooleanType() { + ArrayIdentifierExpression expr = ArrayIdentifierExpression.of("flags", ArrayType.BOOLEAN); + String result = extractor.visit(expr); + assertEquals("boolean[]", result); + } + + @Test + void testVisitArrayIdentifierExpression_withoutType() { + ArrayIdentifierExpression expr = ArrayIdentifierExpression.of("tags"); + String result = extractor.visit(expr); + assertNull(result); + } + + @Test + void testVisitJsonIdentifierExpression() { + JsonIdentifierExpression expr = JsonIdentifierExpression.of("customAttr", "field"); + assertThrows(UnsupportedOperationException.class, () -> extractor.visit(expr)); + } + + @Test + void testVisitIdentifierExpression() { + IdentifierExpression expr = IdentifierExpression.of("item"); + assertThrows(UnsupportedOperationException.class, () -> extractor.visit(expr)); + } + + @Test + void testVisitAggregateExpression() { + AggregateExpression expr = + AggregateExpression.of( + org.hypertrace.core.documentstore.expression.operators.AggregationOperator.COUNT, + IdentifierExpression.of("item")); + assertThrows(UnsupportedOperationException.class, () -> extractor.visit(expr)); + } + + @Test + void testVisitConstantExpression() { + ConstantExpression expr = ConstantExpression.of("test"); + assertThrows(UnsupportedOperationException.class, () -> extractor.visit(expr)); + } + + @Test + void testVisitDocumentConstantExpression() { + ConstantExpression.DocumentConstantExpression expr = + (ConstantExpression.DocumentConstantExpression) + ConstantExpression.of((org.hypertrace.core.documentstore.Document) null); + assertThrows(UnsupportedOperationException.class, () -> extractor.visit(expr)); + } + + @Test + void testVisitFunctionExpression() { + FunctionExpression expr = + FunctionExpression.builder() + .operator(FunctionOperator.LENGTH) + .operand(IdentifierExpression.of("item")) + .build(); + assertThrows(UnsupportedOperationException.class, () -> extractor.visit(expr)); + } + + @Test + void testVisitAliasedIdentifierExpression() { + AliasedIdentifierExpression expr = + AliasedIdentifierExpression.builder().name("item").contextAlias("i").build(); + assertThrows(UnsupportedOperationException.class, () -> extractor.visit(expr)); + } +}