22
33import static java .util .Map .entry ;
44import static org .hypertrace .core .documentstore .expression .operators .RelationalOperator .CONTAINS ;
5+ import static org .hypertrace .core .documentstore .expression .operators .RelationalOperator .EQ ;
56import static org .hypertrace .core .documentstore .expression .operators .RelationalOperator .EXISTS ;
67import static org .hypertrace .core .documentstore .expression .operators .RelationalOperator .IN ;
78import static org .hypertrace .core .documentstore .expression .operators .RelationalOperator .LIKE ;
9+ import static org .hypertrace .core .documentstore .expression .operators .RelationalOperator .NEQ ;
810import static org .hypertrace .core .documentstore .expression .operators .RelationalOperator .NOT_CONTAINS ;
911import static org .hypertrace .core .documentstore .expression .operators .RelationalOperator .NOT_EXISTS ;
1012import static org .hypertrace .core .documentstore .expression .operators .RelationalOperator .NOT_IN ;
1315import com .google .common .collect .Maps ;
1416import java .util .Map ;
1517import org .hypertrace .core .documentstore .DocumentType ;
18+ import org .hypertrace .core .documentstore .expression .impl .ArrayIdentifierExpression ;
19+ import org .hypertrace .core .documentstore .expression .impl .ConstantExpression ;
20+ import org .hypertrace .core .documentstore .expression .impl .JsonFieldType ;
21+ import org .hypertrace .core .documentstore .expression .impl .JsonIdentifierExpression ;
1622import org .hypertrace .core .documentstore .expression .impl .RelationalExpression ;
1723import org .hypertrace .core .documentstore .expression .operators .RelationalOperator ;
24+ import org .hypertrace .core .documentstore .expression .type .SelectTypeExpression ;
1825import org .hypertrace .core .documentstore .postgres .query .v1 .PostgresQueryParser ;
1926
2027public class PostgresRelationalFilterParserFactoryImpl
2128 implements PostgresRelationalFilterParserFactory {
29+
2230 private static final Map <RelationalOperator , PostgresRelationalFilterParser > parserMap =
2331 Maps .immutableEnumMap (
2432 Map .ofEntries (
@@ -41,12 +49,139 @@ public PostgresRelationalFilterParser parser(
4149 boolean isFlatCollection =
4250 postgresQueryParser .getPgColTransformer ().getDocumentType () == DocumentType .FLAT ;
4351
44- if (expression .getOperator () == CONTAINS ) {
52+ RelationalOperator operator = expression .getOperator ();
53+ // Transform EQ/NEQ to CONTAINS/NOT_CONTAINS for array fields with scalar RHS
54+ // (but not for unnested fields, which are already scalar)
55+ if (shouldConvertEqToContains (expression , postgresQueryParser )) {
56+ operator = (expression .getOperator () == EQ ) ? CONTAINS : NOT_CONTAINS ;
57+ }
58+
59+ if (operator == CONTAINS ) {
4560 return expression .getLhs ().accept (new PostgresContainsParserSelector (isFlatCollection ));
46- } else if (expression . getOperator () == IN ) {
61+ } else if (operator == IN ) {
4762 return expression .getLhs ().accept (new PostgresInParserSelector (isFlatCollection ));
63+ } else if (operator == NOT_CONTAINS ) {
64+ return parserMap .get (NOT_CONTAINS );
65+ }
66+
67+ // For EQ/NEQ on array fields with array RHS, use specialized array equality parser (exact match
68+ // instead of containment)
69+ if (shouldUseArrayEqualityParser (expression , postgresQueryParser )) {
70+ return expression .getLhs ().accept (new PostgresArrayEqualityParserSelector ());
4871 }
4972
5073 return parserMap .getOrDefault (expression .getOperator (), postgresStandardRelationalFilterParser );
5174 }
75+
76+ /**
77+ * Determines if EQ/NEQ should be converted to CONTAINS/NOT_CONTAINS.
78+ *
79+ * <p>Conversion happens when:
80+ *
81+ * <ul>
82+ * <li>Operator is EQ or NEQ
83+ * <li>RHS is a SCALAR value (not an array/iterable)
84+ * <li>LHS is a JsonIdentifierExpression with an array field type (STRING_ARRAY, NUMBER_ARRAY,
85+ * etc.) OR
86+ * <li>LHS is an ArrayIdentifierExpression with an array type (TEXT, BIGINT, etc.)
87+ * <li>Field has NOT been unnested (unnested fields are scalar, not arrays)
88+ * </ul>
89+ *
90+ * <p>If RHS is an array, we DO NOT convert - we want exact equality match (= operator), not
91+ * containment (@> operator).
92+ *
93+ * <p>This provides semantic equivalence: checking if an array contains a scalar value is more
94+ * intuitive than checking if the array equals the value.
95+ */
96+ private boolean shouldConvertEqToContains (
97+ final RelationalExpression expression , final PostgresQueryParser postgresQueryParser ) {
98+ if (expression .getOperator () != EQ && expression .getOperator () != NEQ ) {
99+ return false ;
100+ }
101+
102+ // Check if RHS is an array/iterable - if so, don't convert (we want exact match)
103+ if (isArrayRhs (expression .getRhs ())) {
104+ return false ;
105+ }
106+
107+ // Check if LHS is an array field
108+ if (!isArrayField (expression .getLhs ())) {
109+ return false ;
110+ }
111+
112+ // Check if field has been unnested - unnested fields are scalar, not arrays
113+ String fieldName = getFieldName (expression .getLhs ());
114+ return fieldName == null
115+ || !postgresQueryParser
116+ .getPgColumnNames ()
117+ .containsKey (fieldName ); // Field is unnested - treat as scalar
118+ }
119+
120+ /**
121+ * Determines if we should use the specialized array equality parser.
122+ *
123+ * <p>Use this parser when:
124+ *
125+ * <ul>
126+ * <li>Operator is EQ or NEQ
127+ * <li>RHS is an array/iterable (for exact match).
128+ * <li>LHS is either {@link JsonIdentifierExpression} with array type OR {@link
129+ * ArrayIdentifierExpression}
130+ * <li>Field has NOT been unnested (unnested fields are scalar, not arrays)
131+ * </ul>
132+ */
133+ private boolean shouldUseArrayEqualityParser (
134+ final RelationalExpression expression , final PostgresQueryParser postgresQueryParser ) {
135+ if (expression .getOperator () != EQ && expression .getOperator () != NEQ ) {
136+ return false ;
137+ }
138+
139+ // Check if RHS is an array/iterable AND LHS is an array field
140+ if (!isArrayRhs (expression .getRhs ()) || !isArrayField (expression .getLhs ())) {
141+ return false ;
142+ }
143+
144+ // Check if field has been unnested - unnested fields are scalar, not arrays
145+ String fieldName = getFieldName (expression .getLhs ());
146+ return fieldName == null || !postgresQueryParser .getPgColumnNames ().containsKey (fieldName );
147+ }
148+
149+ /**
150+ * Checks if the RHS expression contains an array/iterable value. Currently, we don't have a very
151+ * clean way to get the RHS data type. //todo: Implement a clean way to get the RHS data type
152+ */
153+ private boolean isArrayRhs (final SelectTypeExpression rhs ) {
154+ if (rhs instanceof ConstantExpression ) {
155+ ConstantExpression constExpr = (ConstantExpression ) rhs ;
156+ return constExpr .getValue () instanceof Iterable ;
157+ }
158+ return false ;
159+ }
160+
161+ /** Checks if the LHS expression is an array field. */
162+ private boolean isArrayField (final SelectTypeExpression lhs ) {
163+ if (lhs instanceof JsonIdentifierExpression ) {
164+ JsonIdentifierExpression jsonExpr = (JsonIdentifierExpression ) lhs ;
165+ return jsonExpr
166+ .getFieldType ()
167+ .map (
168+ fieldType ->
169+ fieldType == JsonFieldType .BOOLEAN_ARRAY
170+ || fieldType == JsonFieldType .STRING_ARRAY
171+ || fieldType == JsonFieldType .NUMBER_ARRAY
172+ || fieldType == JsonFieldType .OBJECT_ARRAY )
173+ .orElse (false );
174+ }
175+ return lhs instanceof ArrayIdentifierExpression ;
176+ }
177+
178+ /** Extracts the field name from an identifier expression. */
179+ private String getFieldName (final SelectTypeExpression lhs ) {
180+ if (lhs instanceof JsonIdentifierExpression ) {
181+ return ((JsonIdentifierExpression ) lhs ).getName ();
182+ } else if (lhs instanceof ArrayIdentifierExpression ) {
183+ return ((ArrayIdentifierExpression ) lhs ).getName ();
184+ }
185+ return null ;
186+ }
52187}
0 commit comments