11package org .hypertrace .core .documentstore .postgres .query .v1 .parser .filter ;
22
33import org .hypertrace .core .documentstore .expression .impl .ConstantExpression ;
4+ import org .hypertrace .core .documentstore .expression .impl .JsonIdentifierExpression ;
45import org .hypertrace .core .documentstore .expression .impl .RelationalExpression ;
56import org .hypertrace .core .documentstore .postgres .query .v1 .parser .filter .PostgresFieldTypeDetector .FieldCategory ;
67
@@ -13,11 +14,11 @@ public String parse(
1314 // If true (RHS = false):
1415 // Regular fields -> IS NOT NULL
1516 // Arrays -> IS NOT NULL AND cardinality(...) > 0
16- // JSONB arrays: IS NOT NULL AND jsonb_typeof(%s) = 'array' AND jsonb_array_length(...) > 0
17+ // JSONB arrays: Optimized GIN index query with containment check
1718 // If false (RHS = true or other):
1819 // Regular fields -> IS NULL
1920 // Arrays -> IS NULL OR cardinality(...) = 0
20- // JSONB arrays: IS NULL OR (jsonb_typeof(%s) = ' array' AND jsonb_array_length(...) = 0)
21+ // JSONB arrays: COALESCE with array length check
2122 final boolean parsedRhs = ConstantExpression .of (false ).equals (expression .getRhs ());
2223
2324 FieldCategory category = expression .getLhs ().accept (new PostgresFieldTypeDetector ());
@@ -28,24 +29,52 @@ public String parse(
2829 // at-least 1 element in it (so exclude NULL or empty arrays). This is to match Mongo's
2930 // behavior
3031 return parsedRhs
31- ? String .format ("(%s IS NOT NULL AND cardinality(%s) > 0)" , parsedLhs , parsedLhs )
32- : String .format ("(%s IS NULL OR cardinality(%s) = 0)" , parsedLhs , parsedLhs );
32+ ? String .format ("(cardinality(%s) > 0)" , parsedLhs )
33+ // More efficient than: %s IS NULL OR cardinality(%s) = 0)? as we can create
34+ // an index on the COALESCE function itself which will return in a single
35+ // index seek rather than two index seeks in the OR query
36+ : String .format ("COALESCE(cardinality(%s), 0) = 0" , parsedLhs );
3337
3438 case JSONB_ARRAY :
35- return parsedRhs
36- ? String .format (
37- "(%s IS NOT NULL AND jsonb_typeof(%s) = 'array' AND jsonb_array_length(%s) > 0)" ,
38- parsedLhs , parsedLhs , parsedLhs )
39- : String .format (
40- "(%s IS NULL OR (jsonb_typeof(%s) = 'array' AND jsonb_array_length(%s) = 0))" ,
41- parsedLhs , parsedLhs , parsedLhs );
39+ {
40+ // Arrays inside JSONB columns - use optimized GIN index queries
41+ JsonIdentifierExpression jsonExpr = (JsonIdentifierExpression ) expression .getLhs ();
42+ String baseColumn = wrapWithDoubleQuotes (jsonExpr .getColumnName ());
43+ String nestedPath = String .join ("." , jsonExpr .getJsonPath ());
44+
45+ return parsedRhs
46+ ? String .format (
47+ "(%s @> '{\" " + nestedPath + "\" : []}' AND jsonb_array_length(%s) > 0)" ,
48+ baseColumn ,
49+ parsedLhs )
50+ : String .format ("COALESCE(jsonb_array_length(%s), 0) = 0" , parsedLhs );
51+ }
52+
53+ case JSONB_SCALAR :
54+ {
55+ // JSONB scalar fields - use ? operator for GIN index optimization
56+ JsonIdentifierExpression jsonExpr = (JsonIdentifierExpression ) expression .getLhs ();
57+ String baseColumn = wrapWithDoubleQuotes (jsonExpr .getColumnName ());
58+ String nestedPath = String .join ("." , jsonExpr .getJsonPath ());
59+
60+ return parsedRhs
61+ // Uses the GIN index on the parent JSONB col
62+ ? String .format ("%s ? '%s'" , baseColumn , nestedPath )
63+ // Does not use the GIN index but is more computationally efficient than doing a IS
64+ // NULL check
65+ : String .format ("NOT (%s ? '%s')" , baseColumn , nestedPath );
66+ }
4267
4368 case SCALAR :
4469 default :
45- // Regular scalar fields
70+ // Regular scalar fields - use standard NULL checks
4671 return parsedRhs
4772 ? String .format ("%s IS NOT NULL" , parsedLhs )
4873 : String .format ("%s IS NULL" , parsedLhs );
4974 }
5075 }
76+
77+ private String wrapWithDoubleQuotes (String identifier ) {
78+ return "\" " + identifier + "\" " ;
79+ }
5180}
0 commit comments