Skip to content

Commit 1e7c791

Browse files
authored
Postgres Query Parser Bugfixes (#253)
1 parent 6526d3c commit 1e7c791

15 files changed

+2008
-969
lines changed

document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java

Lines changed: 1617 additions & 838 deletions
Large diffs are not rendered by default.
Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
[
22
{
3-
"item": "Soap",
4-
"price": 10
3+
"item": "Soap"
54
},
65
{
7-
"item": "Shampoo",
8-
"price": 5
6+
"item": "Shampoo"
7+
},
8+
{
9+
"item": "Shampoo"
10+
},
11+
{
12+
"item": "Comb"
13+
},
14+
{
15+
"item": "Soap"
916
}
1017
]
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
[
22
{
3-
"item": "Shampoo",
4-
"price": 5
3+
"item": "Shampoo"
54
}
65
]
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
[
22
{
3-
"item": "Mirror",
4-
"price": 20
3+
"item": "Mirror"
4+
},
5+
{
6+
"item": "Shampoo"
7+
},
8+
{
9+
"item": "Comb"
10+
},
11+
{
12+
"item": "Soap"
513
}
614
]
Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
22
"statements": [
3-
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n1, 'Soap', 10, 2, '2014-03-01T08:00:00Z',\n'{\"hygiene\", \"personal-care\", \"premium\"}',\n'{\"Hygiene\", \"PersonalCare\"}',\n'{\"colors\": [\"Blue\", \"Green\"], \"brand\": \"Dettol\", \"size\": \"M\", \"seller\": {\"name\": \"Metro Chemicals Pvt. Ltd.\", \"address\": {\"city\": \"Mumbai\", \"pincode\": 400004}}}',\nNULL,\n'{1, 2, 3}',\n'{4.5, 9.2}',\n'{true, false}'\n)",
4-
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n2, 'Mirror', 20, 1, '2014-03-01T09:00:00Z',\n'{\"home-decor\", \"reflective\", \"glass\"}',\n'{\"HomeDecor\"}',\nNULL,\nNULL,\n'{10, 20}',\nNULL,\nNULL\n)",
5-
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n3, 'Shampoo', 5, 10, '2014-03-15T09:00:00Z',\n'{\"hair-care\", \"personal-care\", \"premium\", \"herbal\"}',\n'{\"HairCare\", \"PersonalCare\"}',\n'{\"colors\": [\"Black\"], \"brand\": \"Sunsilk\", \"size\": \"L\", \"seller\": {\"name\": \"Metro Chemicals Pvt. Ltd.\", \"address\": {\"city\": \"Mumbai\", \"pincode\": 400004}}}',\nNULL,\nNULL,\n'{3.14, 2.71}',\nNULL\n)",
6-
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n4, 'Shampoo', 5, 20, '2014-04-04T11:21:39.736Z',\n'{\"hair-care\", \"budget\", \"bulk\"}',\n'{\"HairCare\"}',\nNULL,\nNULL,\nNULL,\nNULL,\n'{true, true}'\n)",
7-
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n5, 'Soap', 20, 5, '2014-04-04T21:23:13.331Z',\n'{\"hygiene\", \"antibacterial\", \"family-pack\"}',\n'{\"Hygiene\"}',\n'{\"colors\": [\"Orange\", \"Blue\"], \"brand\": \"Lifebuoy\", \"size\": \"S\", \"seller\": {\"name\": \"Hans and Co.\", \"address\": {\"city\": \"Kolkata\", \"pincode\": 700007}}}',\nNULL,\nNULL,\nNULL,\nNULL\n)",
8-
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n6, 'Comb', 7.5, 5, '2015-06-04T05:08:13Z',\n'{\"grooming\", \"plastic\", \"essential\"}',\n'{\"Grooming\"}',\nNULL,\nNULL,\nNULL,\nNULL,\nNULL\n)",
9-
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n7, 'Comb', 7.5, 10, '2015-09-10T08:43:00Z',\n'{\"grooming\", \"bulk\", \"wholesale\"}',\n'{\"Grooming\"}',\n'{\"colors\": [], \"seller\": {\"name\": \"Go Go Plastics\", \"address\": {\"city\": \"Kolkata\", \"pincode\": 700007}}}',\nNULL,\nNULL,\nNULL,\nNULL\n)",
10-
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n8, 'Soap', 10, 5, '2016-02-06T20:20:13Z',\n'{\"hygiene\", \"budget\", \"basic\"}',\n'{\"Hygiene\"}',\nNULL,\nNULL,\nNULL,\nNULL,\nNULL\n)",
11-
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n9, 'Bottle', 15, 3, '2016-03-01T10:00:00Z',\nNULL,\nNULL,\nNULL,\nNULL,\nNULL,\nNULL,\nNULL\n)",
12-
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n10, 'Cup', 8, 2, '2016-04-01T10:00:00Z',\n'{}',\n'{}',\nNULL,\nNULL,\nNULL,\nNULL,\nNULL\n)"
3+
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"in_stock\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n1, 'Soap', 10, 2, '2014-03-01T08:00:00Z', true,\n'{\"hygiene\", \"personal-care\", \"premium\"}',\n'{\"Hygiene\", \"PersonalCare\"}',\n'{\"colors\": [\"Blue\", \"Green\"], \"brand\": \"Dettol\", \"size\": \"M\", \"product-code\": \"SOAP-DET-001\", \"source-loc\": [\"warehouse-A\", \"store-1\"], \"seller\": {\"name\": \"Metro Chemicals Pvt. Ltd.\", \"address\": {\"city\": \"Mumbai\", \"pincode\": 400004}}}',\nNULL,\n'{1, 2, 3}',\n'{4.5, 9.2}',\n'{true, false}'\n)",
4+
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"in_stock\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n2, 'Mirror', 20, 1, '2014-03-01T09:00:00Z', true,\n'{\"home-decor\", \"reflective\", \"glass\"}',\n'{\"HomeDecor\"}',\nNULL,\nNULL,\n'{10, 20}',\n'{1.5, 2.5, 3.5}',\n'{false, false}'\n)",
5+
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"in_stock\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n3, 'Shampoo', 5, 10, '2014-03-15T09:00:00Z', true,\n'{\"hair-care\", \"personal-care\", \"premium\", \"herbal\"}',\n'{\"HairCare\", \"PersonalCare\"}',\n'{\"colors\": [\"Black\"], \"brand\": \"Sunsilk\", \"size\": \"L\", \"product-code\": \"SHAMP-SUN-003\", \"source-loc\": [\"warehouse-B\", \"store-2\", \"online\"], \"seller\": {\"name\": \"Metro Chemicals Pvt. Ltd.\", \"address\": {\"city\": \"Mumbai\", \"pincode\": 400004}}}',\nNULL,\n'{5, 10, 15}',\n'{3.14, 2.71}',\n'{true, false, true}'\n)",
6+
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"in_stock\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n4, 'Shampoo', 5, 20, '2014-04-04T11:21:39.736Z', false,\n'{\"hair-care\", \"budget\", \"bulk\"}',\n'{\"HairCare\"}',\nNULL,\nNULL,\n'{1, 2}',\n'{5.0, 10.0}',\n'{true, true}'\n)",
7+
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"in_stock\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n5, 'Soap', 20, 5, '2014-04-04T21:23:13.331Z', true,\n'{\"hygiene\", \"antibacterial\", \"family-pack\"}',\n'{\"Hygiene\"}',\n'{\"colors\": [\"Orange\", \"Blue\"], \"brand\": \"Lifebuoy\", \"size\": \"S\", \"product-code\": \"SOAP-LIF-005\", \"source-loc\": [\"warehouse-C\"], \"seller\": {\"name\": \"Hans and Co.\", \"address\": {\"city\": \"Kolkata\", \"pincode\": 700007}}}',\nNULL,\n'{3, 6, 9}',\n'{7.5}',\n'{false}'\n)",
8+
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"in_stock\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n6, 'Comb', 7.5, 5, '2015-06-04T05:08:13Z', true,\n'{\"grooming\", \"plastic\", \"essential\"}',\n'{\"Grooming\"}',\nNULL,\nNULL,\n'{20, 30}',\n'{6.0, 8.0}',\n'{true, false}'\n)",
9+
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"in_stock\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n7, 'Comb', 7.5, 10, '2015-09-10T08:43:00Z', false,\n'{\"grooming\", \"bulk\", \"wholesale\"}',\n'{\"Grooming\"}',\n'{\"colors\": [], \"product-code\": null, \"source-loc\": [], \"seller\": {\"name\": \"Go Go Plastics\", \"address\": {\"city\": \"Kolkata\", \"pincode\": 700007}}}',\nNULL,\n'{10}',\n'{3.0}',\n'{false, false, false}'\n)",
10+
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"in_stock\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n8, 'Soap', 10, 5, '2016-02-06T20:20:13Z', true,\n'{\"hygiene\", \"budget\", \"basic\"}',\n'{\"Hygiene\"}',\nNULL,\nNULL,\n'{1, 10, 20}',\n'{2.5, 5.0}',\n'{true}'\n)",
11+
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"in_stock\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n9, 'Bottle', 15, 3, '2016-03-01T10:00:00Z', false,\nNULL,\nNULL,\nNULL,\nNULL,\nNULL,\nNULL,\nNULL\n)",
12+
"INSERT INTO \"myTestFlat\" (\n\"_id\", \"item\", \"price\", \"quantity\", \"date\", \"in_stock\", \"tags\", \"categoryTags\", \"props\", \"sales\", \"numbers\", \"scores\", \"flags\"\n) VALUES (\n10, 'Cup', 8, 2, '2016-04-01T10:00:00Z', true,\n'{}',\n'{}',\nNULL,\nNULL,\nNULL,\nNULL,\nNULL\n)"
1313
]
1414
}

document-store/src/main/java/org/hypertrace/core/documentstore/postgres/PostgresCollection.java

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,9 +1395,9 @@ private void addColumnToJsonNode(
13951395
break;
13961396

13971397
case "_text": // text array
1398-
Array array = resultSet.getArray(columnIndex);
1399-
if (array != null) {
1400-
String[] stringArray = (String[]) array.getArray();
1398+
Array textArray = resultSet.getArray(columnIndex);
1399+
if (textArray != null) {
1400+
String[] stringArray = (String[]) textArray.getArray();
14011401
ArrayNode arrayNode = MAPPER.createArrayNode();
14021402
for (String item : stringArray) {
14031403
arrayNode.add(item);
@@ -1406,6 +1406,52 @@ private void addColumnToJsonNode(
14061406
}
14071407
break;
14081408

1409+
case "_int4": // integer array
1410+
case "_int8": // bigint array
1411+
Array intArray = resultSet.getArray(columnIndex);
1412+
if (intArray != null) {
1413+
Object[] intObjectArray = (Object[]) intArray.getArray();
1414+
ArrayNode intArrayNode = MAPPER.createArrayNode();
1415+
for (Object item : intObjectArray) {
1416+
if (item instanceof Integer) {
1417+
intArrayNode.add((Integer) item);
1418+
} else if (item instanceof Long) {
1419+
intArrayNode.add((Long) item);
1420+
}
1421+
}
1422+
jsonNode.set(columnName, intArrayNode);
1423+
}
1424+
break;
1425+
1426+
case "_float8": // double precision array
1427+
case "_float4": // real/float array
1428+
Array doubleArray = resultSet.getArray(columnIndex);
1429+
if (doubleArray != null) {
1430+
Object[] doubleObjectArray = (Object[]) doubleArray.getArray();
1431+
ArrayNode doubleArrayNode = MAPPER.createArrayNode();
1432+
for (Object item : doubleObjectArray) {
1433+
if (item instanceof Double) {
1434+
doubleArrayNode.add((Double) item);
1435+
} else if (item instanceof Float) {
1436+
doubleArrayNode.add((Float) item);
1437+
}
1438+
}
1439+
jsonNode.set(columnName, doubleArrayNode);
1440+
}
1441+
break;
1442+
1443+
case "_bool": // boolean array
1444+
Array boolArray = resultSet.getArray(columnIndex);
1445+
if (boolArray != null) {
1446+
Boolean[] boolObjectArray = (Boolean[]) boolArray.getArray();
1447+
ArrayNode boolArrayNode = MAPPER.createArrayNode();
1448+
for (Boolean item : boolObjectArray) {
1449+
boolArrayNode.add(item);
1450+
}
1451+
jsonNode.set(columnName, boolArrayNode);
1452+
}
1453+
break;
1454+
14091455
case "jsonb":
14101456
case "json":
14111457
String jsonString = resultSet.getString(columnIndex);

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

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.hypertrace.core.documentstore.postgres.query.v1.parser.filter;
22

3+
import org.hypertrace.core.documentstore.expression.impl.ArrayIdentifierExpression;
34
import org.hypertrace.core.documentstore.expression.impl.ConstantExpression;
45
import org.hypertrace.core.documentstore.expression.impl.JsonIdentifierExpression;
56
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
@@ -25,16 +26,37 @@ public String parse(
2526

2627
switch (category) {
2728
case ARRAY:
28-
// First-class PostgreSQL array columns (text[], int[], etc.)
29-
return parsedRhs
30-
// We don't need to check that LHS is NOT NULL because WHERE cardinality(NULL) will not
31-
// be included in the result set
32-
? String.format("(cardinality(%s) > 0)", parsedLhs)
33-
: String.format("COALESCE(cardinality(%s), 0) = 0", parsedLhs);
29+
{
30+
// First-class PostgreSQL array columns (text[], int[], etc.)
31+
// Check if this field has been unnested - if so, treat it as a scalar (because the
32+
// unnested array col is not longer an array, but a scalar col)
33+
ArrayIdentifierExpression arrayExpr = (ArrayIdentifierExpression) expression.getLhs();
34+
String arrayFieldName = arrayExpr.getName();
35+
if (context.getPgColumnNames().containsKey(arrayFieldName)) {
36+
// Field is unnested - each element is now a scalar, not an array
37+
// Use simple NULL checks instead of cardinality
38+
return getScalarExpr(parsedRhs, parsedLhs);
39+
}
40+
41+
// Field is NOT unnested - apply cardinality logic
42+
return parsedRhs
43+
// We don't need to check that LHS is NOT NULL because WHERE cardinality(NULL) will
44+
// not be included in the result set
45+
? String.format("(cardinality(%s) > 0)", parsedLhs)
46+
: String.format("COALESCE(cardinality(%s), 0) = 0", parsedLhs);
47+
}
3448

3549
case JSONB_ARRAY:
3650
{
3751
JsonIdentifierExpression jsonExpr = (JsonIdentifierExpression) expression.getLhs();
52+
// Check if this field has been unnested - if so, treat it as a scalar
53+
String fieldName = jsonExpr.getName();
54+
if (context.getPgColumnNames().containsKey(fieldName)) {
55+
// Field is unnested - each element is now a scalar. Treat how we treated the array case
56+
return getScalarExpr(parsedRhs, parsedLhs);
57+
}
58+
59+
// Field is NOT unnested - apply array length logic
3860
String baseColumn = wrapWithDoubleQuotes(jsonExpr.getColumnName());
3961
String nestedPath = String.join(".", jsonExpr.getJsonPath());
4062
return parsedRhs
@@ -49,26 +71,18 @@ public String parse(
4971
}
5072

5173
case JSONB_SCALAR:
52-
{
53-
// JSONB scalar fields - use ? operator for GIN index optimization
54-
JsonIdentifierExpression jsonExpr = (JsonIdentifierExpression) expression.getLhs();
55-
String baseColumn = wrapWithDoubleQuotes(jsonExpr.getColumnName());
56-
String nestedPath = String.join(".", jsonExpr.getJsonPath());
57-
58-
return parsedRhs
59-
? String.format("%s ? '%s'", baseColumn, nestedPath)
60-
: String.format("NOT (%s ? '%s')", baseColumn, nestedPath);
61-
}
62-
6374
case SCALAR:
6475
default:
65-
// Regular scalar fields - use standard NULL checks
66-
return parsedRhs
67-
? String.format("%s IS NOT NULL", parsedLhs)
68-
: String.format("%s IS NULL", parsedLhs);
76+
return getScalarExpr(parsedRhs, parsedLhs);
6977
}
7078
}
7179

80+
private String getScalarExpr(boolean parsedRhs, String parsedLhs) {
81+
return parsedRhs
82+
? String.format("%s IS NOT NULL", parsedLhs)
83+
: String.format("%s IS NULL", parsedLhs);
84+
}
85+
7286
private String wrapWithDoubleQuotes(String identifier) {
7387
return "\"" + identifier + "\"";
7488
}

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

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
*
2323
* <p>This checks if the JSON array contains ANY of the provided values, using efficient JSONB
2424
* containment instead of defensive type checking.
25+
*
26+
* <p>Special case: If the JSONB array field has been unnested, each row contains a scalar value
27+
* (not an array), so we use scalar IN syntax instead of the @> containment operator.
2528
*/
2629
public class PostgresInRelationalFilterParserJsonArray
2730
implements PostgresInRelationalFilterParserInterface {
@@ -42,11 +45,74 @@ public String parse(
4245
new IllegalStateException(
4346
"JsonFieldType must be present - this should have been caught by the selector"));
4447

45-
return prepareFilterStringForInOperator(
48+
// Check if this field has been unnested - if so, treat it as a scalar
49+
String fieldName = jsonExpr.getName();
50+
if (context.getPgColumnNames().containsKey(fieldName)) {
51+
// Field is unnested - each element is now a scalar, not an array
52+
// Use scalar IN operator instead of JSONB containment
53+
return prepareFilterStringForScalarInOperator(
54+
parsedLhs, parsedRhs, context.getParamsBuilder());
55+
}
56+
57+
// Field is NOT unnested - use JSONB containment logic
58+
return prepareFilterStringForArrayInOperator(
4659
parsedLhs, parsedRhs, fieldType, context.getParamsBuilder());
4760
}
4861

49-
private String prepareFilterStringForInOperator(
62+
/**
63+
* Generates SQL for scalar IN operator (used when JSONB array field has been unnested). Example:
64+
* "props_dot_source-loc" IN (?::jsonb, ?::jsonb)
65+
*
66+
* <p>Note: After unnesting with jsonb_array_elements(), each row contains a JSONB scalar value.
67+
* We cast the parameters to jsonb for direct JSONB-to-JSONB comparison, which works for all JSONB
68+
* types (strings, numbers, booleans, objects).
69+
*/
70+
private String prepareFilterStringForScalarInOperator(
71+
final String parsedLhs,
72+
final Iterable<Object> parsedRhs,
73+
final Params.Builder paramsBuilder) {
74+
75+
String placeholders =
76+
StreamSupport.stream(parsedRhs.spliterator(), false)
77+
.map(
78+
value -> {
79+
// Add the value as a JSONB-formatted string
80+
// For strings, this needs to be JSON-quoted (e.g., "warehouse-A" becomes
81+
// "\"warehouse-A\"")
82+
String jsonValue = convertToJsonString(value);
83+
paramsBuilder.addObjectParam(jsonValue);
84+
return "?::jsonb";
85+
})
86+
.collect(Collectors.joining(", "));
87+
88+
// Direct JSONB comparison - no text conversion needed
89+
return String.format("%s IN (%s)", parsedLhs, placeholders);
90+
}
91+
92+
/**
93+
* Converts a Java value to its JSON string representation for JSONB casting. Strings are quoted,
94+
* numbers/booleans are not.
95+
*/
96+
private String convertToJsonString(Object value) {
97+
if (value == null) {
98+
return "null";
99+
} else if (value instanceof String) {
100+
// JSON strings must be quoted
101+
return "\"" + value.toString().replace("\"", "\\\"") + "\"";
102+
} else if (value instanceof Number || value instanceof Boolean) {
103+
// Numbers and booleans are not quoted in JSON
104+
return value.toString();
105+
} else {
106+
// For other types, assume they're already JSON-formatted or treat as string
107+
return "\"" + value.toString().replace("\"", "\\\"") + "\"";
108+
}
109+
}
110+
111+
/**
112+
* Generates SQL for JSONB containment operator (used for non-unnested JSONB array fields).
113+
* Example: document->'tags' @> jsonb_build_array(?::text)
114+
*/
115+
private String prepareFilterStringForArrayInOperator(
50116
final String parsedLhs,
51117
final Iterable<Object> parsedRhs,
52118
final JsonFieldType fieldType,

0 commit comments

Comments
 (0)