Skip to content

Commit 1df7ca8

Browse files
committed
support for contains and in relational filter for first class fields; added quotes around fields
1 parent 35ce440 commit 1df7ca8

12 files changed

+189
-15
lines changed

document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresContainsRelationalFilterParser.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import org.hypertrace.core.documentstore.Document;
77
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
88

9-
class PostgresContainsRelationalFilterParser implements PostgresRelationalFilterParser {
9+
class PostgresContainsRelationalFilterParser
10+
implements PostgresContainsRelationalFilterParserInterface {
1011
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
1112

1213
@Override
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;
2+
3+
/**
4+
* Interface for handling CONTAINS operations in PostgreSQL queries. Implementations can provide
5+
* different strategies for handling containment operations based on the context of the query (e.g.,
6+
* first-class fields vs. JSON fields).
7+
*/
8+
public interface PostgresContainsRelationalFilterParserInterface
9+
extends PostgresRelationalFilterParser {
10+
// Interface inherits the parse method from PostgresRelationalFilterParser
11+
// No additional methods required at this time
12+
}

document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresInRelationalFilterParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
66
import org.hypertrace.core.documentstore.postgres.Params;
77

8-
class PostgresInRelationalFilterParser implements PostgresRelationalFilterParser {
8+
class PostgresInRelationalFilterParser implements PostgresInRelationalFilterParserInterface {
99

1010
@Override
1111
public String parse(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;
2+
3+
/**
4+
* Interface for handling IN operation filters in PostgreSQL queries. Implementations can provide
5+
* different strategies for handling IN operations based on the context of the query (e.g.,
6+
* first-class fields vs. JSON fields).
7+
*/
8+
public interface PostgresInRelationalFilterParserInterface extends PostgresRelationalFilterParser {
9+
// Interface inherits the parse method from PostgresRelationalFilterParser
10+
// No additional methods required at this time
11+
}
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,33 @@
11
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;
22

3-
import static org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.PostgresContainsRelationalFilterParser.prepareJsonValueForContainsOp;
4-
53
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
4+
import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresContainsRelationalFilterParserNonJsonField;
65

76
class PostgresNotContainsRelationalFilterParser implements PostgresRelationalFilterParser {
7+
private static final PostgresContainsRelationalFilterParser jsonContainsParser =
8+
new PostgresContainsRelationalFilterParser();
9+
private static final PostgresContainsRelationalFilterParserNonJsonField nonJsonContainsParser =
10+
new PostgresContainsRelationalFilterParserNonJsonField();
11+
812
@Override
913
public String parse(
1014
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
1115
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
12-
final Object parsedRhs = expression.getRhs().accept(context.rhsParser());
1316

14-
final Object convertedRhs = prepareJsonValueForContainsOp(parsedRhs);
15-
context.getParamsBuilder().addObjectParam(convertedRhs);
17+
boolean isFirstClassField =
18+
context
19+
.getFlatStructureCollectionName()
20+
.map(name -> name.equals(context.getTableIdentifier().getTableName()))
21+
.orElse(false);
1622

17-
return String.format("%s IS NULL OR NOT %s @> ?::jsonb", parsedLhs, parsedLhs);
23+
if (isFirstClassField) {
24+
// Use the non-JSON logic for first-class fields
25+
String containsExpression = nonJsonContainsParser.parse(expression, context);
26+
return String.format("%s IS NULL OR NOT (%s)", parsedLhs, containsExpression);
27+
} else {
28+
// Use the JSON logic for document fields.
29+
jsonContainsParser.parse(expression, context); // This adds the parameter.
30+
return String.format("%s IS NULL OR NOT %s @> ?::jsonb", parsedLhs, parsedLhs);
31+
}
1832
}
1933
}
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;
22

33
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
4+
import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserNonJsonField;
45

56
class PostgresNotInRelationalFilterParser implements PostgresRelationalFilterParser {
6-
private static final PostgresInRelationalFilterParser inRelationalFilterParser =
7+
private static final PostgresInRelationalFilterParserInterface jsonFieldInFilterParser =
78
new PostgresInRelationalFilterParser();
9+
private static final PostgresInRelationalFilterParserInterface nonJsonFieldInFilterParser =
10+
new PostgresInRelationalFilterParserNonJsonField();
811

912
@Override
1013
public String parse(
1114
final RelationalExpression expression, final PostgresRelationalFilterContext context) {
1215
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
13-
final String parsedInExpression = inRelationalFilterParser.parse(expression, context);
16+
17+
PostgresInRelationalFilterParserInterface inFilterParser = getInFilterParser(context);
18+
19+
final String parsedInExpression = inFilterParser.parse(expression, context);
1420
return String.format("%s IS NULL OR NOT (%s)", parsedLhs, parsedInExpression);
1521
}
22+
23+
private PostgresInRelationalFilterParserInterface getInFilterParser(
24+
PostgresRelationalFilterContext context) {
25+
boolean isFirstClassField =
26+
context
27+
.getFlatStructureCollectionName()
28+
.map(name -> name.equals(context.getTableIdentifier().getTableName()))
29+
.orElse(false);
30+
31+
return isFirstClassField ? nonJsonFieldInFilterParser : jsonFieldInFilterParser;
32+
}
1633
}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;
22

33
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
4+
import org.hypertrace.core.documentstore.postgres.query.v1.PostgresQueryParser;
45

56
public interface PostgresRelationalFilterParserFactory {
6-
PostgresRelationalFilterParser parser(final RelationalExpression expression);
7+
PostgresRelationalFilterParser parser(
8+
final RelationalExpression expression, final PostgresQueryParser postgresQueryParser);
79
}

document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/parser/filter/PostgresRelationalFilterParserFactoryImpl.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,55 @@
1414
import java.util.Map;
1515
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
1616
import org.hypertrace.core.documentstore.expression.operators.RelationalOperator;
17+
import org.hypertrace.core.documentstore.postgres.query.v1.PostgresQueryParser;
18+
import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresContainsRelationalFilterParserNonJsonField;
19+
import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field.PostgresInRelationalFilterParserNonJsonField;
1720

1821
public class PostgresRelationalFilterParserFactoryImpl
1922
implements PostgresRelationalFilterParserFactory {
2023
private static final Map<RelationalOperator, PostgresRelationalFilterParser> parserMap =
2124
Maps.immutableEnumMap(
2225
Map.ofEntries(
23-
entry(CONTAINS, new PostgresContainsRelationalFilterParser()),
26+
// CONTAINS is conditionally chosen between JSON and non-JSON versions
2427
entry(NOT_CONTAINS, new PostgresNotContainsRelationalFilterParser()),
2528
entry(EXISTS, new PostgresExistsRelationalFilterParser()),
2629
entry(NOT_EXISTS, new PostgresNotExistsRelationalFilterParser()),
27-
entry(IN, new PostgresInRelationalFilterParser()),
30+
// IN are conditionally chosen between JSON and non-JSON versions
2831
entry(NOT_IN, new PostgresNotInRelationalFilterParser()),
2932
entry(LIKE, new PostgresLikeRelationalFilterParser()),
3033
entry(STARTS_WITH, new PostgresStartsWithRelationalFilterParser())));
34+
35+
// IN filter parsers
36+
private static final PostgresInRelationalFilterParserInterface jsonFieldInFilterParser =
37+
new PostgresInRelationalFilterParser();
38+
private static final PostgresInRelationalFilterParserInterface nonJsonFieldInFilterParser =
39+
new PostgresInRelationalFilterParserNonJsonField();
40+
41+
// CONTAINS parser implementations
42+
private static final PostgresContainsRelationalFilterParserInterface jsonFieldContainsParser =
43+
new PostgresContainsRelationalFilterParser();
44+
private static final PostgresContainsRelationalFilterParserInterface nonJsonFieldContainsParser =
45+
new PostgresContainsRelationalFilterParserNonJsonField();
46+
3147
private static final PostgresStandardRelationalFilterParser
3248
postgresStandardRelationalFilterParser = new PostgresStandardRelationalFilterParser();
3349

3450
@Override
35-
public PostgresRelationalFilterParser parser(final RelationalExpression expression) {
51+
public PostgresRelationalFilterParser parser(
52+
final RelationalExpression expression, final PostgresQueryParser postgresQueryParser) {
53+
54+
boolean isFirstClassField =
55+
postgresQueryParser
56+
.getFlatStructureCollectionName()
57+
.map(name -> name.equals(postgresQueryParser.getTableIdentifier().getTableName()))
58+
.orElse(false);
59+
60+
if (expression.getOperator() == CONTAINS) {
61+
return isFirstClassField ? nonJsonFieldContainsParser : jsonFieldContainsParser;
62+
} else if (expression.getOperator() == IN) {
63+
return isFirstClassField ? nonJsonFieldInFilterParser : jsonFieldInFilterParser;
64+
}
65+
3666
return parserMap.getOrDefault(expression.getOperator(), postgresStandardRelationalFilterParser);
3767
}
3868
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field;
2+
3+
import java.util.Collection;
4+
import java.util.Collections;
5+
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
6+
import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.PostgresContainsRelationalFilterParserInterface;
7+
import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.PostgresRelationalFilterParser;
8+
9+
/**
10+
* Implementation of CONTAINS operator for non-JSON fields (regular PostgreSQL arrays). Uses the
11+
* PostgreSQL array containment operator (@>) for checking if one array contains another.
12+
*
13+
* <p>This class is optimized for first-class array columns rather than JSON document fields.
14+
*/
15+
public class PostgresContainsRelationalFilterParserNonJsonField
16+
implements PostgresContainsRelationalFilterParserInterface {
17+
18+
@Override
19+
public String parse(
20+
final RelationalExpression expression,
21+
final PostgresRelationalFilterParser.PostgresRelationalFilterContext context) {
22+
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
23+
final Object parsedRhs = expression.getRhs().accept(context.rhsParser());
24+
25+
Object normalizedRhs = normalizeValue(parsedRhs);
26+
context.getParamsBuilder().addObjectParam(normalizedRhs);
27+
28+
return String.format("%s @> ARRAY[?]::text[]", parsedLhs);
29+
}
30+
31+
private Object normalizeValue(final Object value) {
32+
if (value == null) {
33+
return null;
34+
} else if (value instanceof Collection) {
35+
return value;
36+
} else {
37+
return Collections.singletonList(value);
38+
}
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.nonjson.field;
2+
3+
import java.util.stream.Collectors;
4+
import java.util.stream.StreamSupport;
5+
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
6+
import org.hypertrace.core.documentstore.postgres.Params;
7+
import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.PostgresInRelationalFilterParserInterface;
8+
import org.hypertrace.core.documentstore.postgres.query.v1.parser.filter.PostgresRelationalFilterParser;
9+
10+
/**
11+
* Implementation of PostgresInRelationalFilterParserInterface for handling IN operations on
12+
* first-class fields (non-JSON columns), using the more efficient array-based syntax.
13+
*/
14+
public class PostgresInRelationalFilterParserNonJsonField
15+
implements PostgresInRelationalFilterParserInterface {
16+
17+
@Override
18+
public String parse(
19+
final RelationalExpression expression,
20+
final PostgresRelationalFilterParser.PostgresRelationalFilterContext context) {
21+
final String parsedLhs = expression.getLhs().accept(context.lhsParser());
22+
final Iterable<Object> parsedRhs = expression.getRhs().accept(context.rhsParser());
23+
24+
return prepareFilterStringForInOperator(parsedLhs, parsedRhs, context.getParamsBuilder());
25+
}
26+
27+
private String prepareFilterStringForInOperator(
28+
final String parsedLhs,
29+
final Iterable<Object> parsedRhs,
30+
final Params.Builder paramsBuilder) {
31+
32+
String placeholders =
33+
StreamSupport.stream(parsedRhs.spliterator(), false)
34+
.map(
35+
value -> {
36+
paramsBuilder.addObjectParam(value);
37+
return "?";
38+
})
39+
.collect(Collectors.joining(", "));
40+
41+
return String.format("%s && ARRAY[%s]::text[]", parsedLhs, placeholders);
42+
}
43+
}

0 commit comments

Comments
 (0)