Skip to content

Commit 7490e35

Browse files
craig[bot]petermattis
andcommitted
Merge #151226
151226: jsonpath: fix empty array comparison behavior in lax mode r=petermattis a=petermattis Fix handling of empty arrays in JSONPath lax mode comparisons. Empty arrays should return false for comparisons in lax mode and null in strict mode, matching PostgreSQL behavior. Changes: - Updated eval.go to return empty slice instead of nil for empty arrays - Modified operation.go to distinguish between nil (path not found) and empty slice (empty array) in comparison operations - Added comprehensive test cases for empty array comparisons Fixes #145099 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Co-authored-by: Peter Mattis <[email protected]>
2 parents 71da658 + d374f59 commit 7490e35

File tree

3 files changed

+170
-1
lines changed

3 files changed

+170
-1
lines changed

pkg/sql/logictest/testdata/logic_test/jsonb_path_query

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,11 +947,141 @@ SELECT jsonb_path_query('{"a": [1, 2], "b": "hello"}', '$.a ? ($.b == "hello") '
947947
1
948948
2
949949

950+
# Test empty array comparisons in lax mode (should return false) - Issue #145099
951+
query T
952+
SELECT jsonb_path_query('{"a": [1], "b": []}', '$.a == $.b')
953+
----
954+
false
955+
956+
query T
957+
SELECT jsonb_path_query('{"a": [], "b": []}', '$.a == $.b')
958+
----
959+
false
960+
961+
query T
962+
SELECT jsonb_path_query('{"a": [], "b": {}}', '$.a == $.b')
963+
----
964+
false
965+
966+
query T
967+
SELECT jsonb_path_query('{"a": [], "b": {"c": 1}}', '$.a == $.b')
968+
----
969+
false
970+
971+
# Test empty array comparisons with different operators in lax mode
972+
query T
973+
SELECT jsonb_path_query('{"a": [], "b": []}', '$.a != $.b')
974+
----
975+
false
976+
977+
query T
978+
SELECT jsonb_path_query('{"a": [1], "b": []}', '$.a != $.b')
979+
----
980+
false
981+
982+
query T
983+
SELECT jsonb_path_query('{"a": [], "b": [1]}', '$.a < $.b')
984+
----
985+
false
986+
987+
query T
988+
SELECT jsonb_path_query('{"a": [], "b": [1]}', '$.a <= $.b')
989+
----
990+
false
991+
992+
query T
993+
SELECT jsonb_path_query('{"a": [], "b": [1]}', '$.a > $.b')
994+
----
995+
false
996+
997+
query T
998+
SELECT jsonb_path_query('{"a": [], "b": [1]}', '$.a >= $.b')
999+
----
1000+
false
1001+
1002+
# Test empty array comparisons in strict mode (should return null)
1003+
query T
1004+
SELECT jsonb_path_query('{"a": [1], "b": []}', 'strict $.a == $.b')
1005+
----
1006+
null
1007+
1008+
query T
1009+
SELECT jsonb_path_query('{"a": [], "b": []}', 'strict $.a == $.b')
1010+
----
1011+
null
1012+
1013+
query T
1014+
SELECT jsonb_path_query('{"a": [], "b": {}}', 'strict $.a == $.b')
1015+
----
1016+
null
1017+
1018+
query T
1019+
SELECT jsonb_path_query('{"a": [], "b": {"c": 1}}', 'strict $.a == $.b')
1020+
----
1021+
null
1022+
1023+
# Test mixed empty/non-empty arrays
1024+
query T
1025+
SELECT jsonb_path_query('{"a": [1, 2], "b": []}', '$.a == $.b')
1026+
----
1027+
false
1028+
1029+
query T
1030+
SELECT jsonb_path_query('{"a": [], "b": [1, 2]}', '$.a == $.b')
1031+
----
1032+
false
1033+
1034+
# Test empty array vs specific value types
1035+
query T
1036+
SELECT jsonb_path_query('{"a": [], "b": [null]}', '$.a == $.b')
1037+
----
1038+
false
1039+
1040+
query T
1041+
SELECT jsonb_path_query('{"a": [], "b": [0]}', '$.a == $.b')
1042+
----
1043+
false
1044+
1045+
query T
1046+
SELECT jsonb_path_query('{"a": [], "b": [false]}', '$.a == $.b')
1047+
----
1048+
false
1049+
1050+
query T
1051+
SELECT jsonb_path_query('{"a": [], "b": [""]}', '$.a == $.b')
1052+
----
1053+
false
1054+
1055+
# Regression tests - ensure existing behavior is preserved
1056+
query T
1057+
SELECT jsonb_path_query('{"a": [1], "b": [1]}', '$.a == $.b')
1058+
----
1059+
true
1060+
1061+
query T
1062+
SELECT jsonb_path_query('{"a": [1], "b": [2]}', '$.a == $.b')
1063+
----
1064+
false
1065+
1066+
query T
1067+
SELECT jsonb_path_query('{"a": "test", "b": "test"}', '$.a == $.b')
1068+
----
1069+
true
1070+
9501071
query T
9511072
SELECT jsonb_path_query('{"a": [1, 2], "b": "hello"}', 'strict $.a ? ($.b == "hello") ');
9521073
----
9531074
[1, 2]
9541075

1076+
# Test logical operators with empty arrays
1077+
query T
1078+
SELECT jsonb_path_query('{"a": [], "b": []}', '$ ? ($.a == $.b || $.a != $.b)')
1079+
----
1080+
1081+
query T
1082+
SELECT jsonb_path_query('{"a": [], "b": []}', '$ ? ($.a == $.b && $.a != $.b)')
1083+
----
1084+
9551085
query T rowsort
9561086
SELECT jsonb_path_query('{"a": "world", "b": 2, "c": true}', '$.*');
9571087
----

pkg/util/jsonpath/eval/eval.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,23 @@ func jsonpathQuery(
144144
return ctx.eval(expr.Path, ctx.root, !ctx.strict /* unwrap */)
145145
}
146146

147+
// eval evaluates a JSONPath expression against a JSON value and returns a
148+
// slice of results.
149+
//
150+
// Return value semantics are critical for proper JSONPath behavior:
151+
// - nil slice: Path evaluation failed or path does not exist (e.g., $.nonexistent)
152+
// In comparisons: returns unknown/null in strict mode, false in lax mode
153+
// - Empty slice ([]json.JSON{}): Path exists but contains no items (e.g., empty array [])
154+
// In comparisons: returns false in lax mode (no items to compare).
155+
// - Non-empty slice: Path found one or more matching items.
156+
//
157+
// This distinction is essential for JSONPath comparison operations to match
158+
// PostgreSQL behavior.
159+
//
160+
// Many of jsonpath operations require automatic unwrapping of arrays in lax
161+
// mode. If the input value is an array the operation is performed not on the
162+
// array itself, but on all of its members one by one. The unwrap parameter
163+
// indicates whether array unwrapping is needed.
147164
func (ctx *jsonpathCtx) eval(
148165
jsonPath jsonpath.Path, jsonValue json.JSON, unwrap bool,
149166
) ([]json.JSON, error) {
@@ -219,7 +236,8 @@ func (ctx *jsonpathCtx) executeAnyItem(
219236
jsonPath jsonpath.Path, jsonValue json.JSON, unwrapNext bool,
220237
) ([]json.JSON, error) {
221238
if jsonValue.Len() == 0 {
222-
return nil, nil
239+
// Return empty slice (not nil) to indicate "empty array found" vs "path not found".
240+
return []json.JSON{}, nil
223241
}
224242
var agg []json.JSON
225243
processItem := func(item json.JSON) error {
@@ -272,6 +290,11 @@ func (ctx *jsonpathCtx) evalAndUnwrapResult(
272290
return nil, err
273291
}
274292
if unwrap && !ctx.strict {
293+
// If evalResults is nil, preserve nil to indicate path evaluation
294+
// failure.
295+
if evalResults == nil {
296+
return nil, nil
297+
}
275298
var agg []json.JSON
276299
for _, j := range evalResults {
277300
if j.Type() == json.ArrayJSONType {
@@ -285,6 +308,12 @@ func (ctx *jsonpathCtx) evalAndUnwrapResult(
285308
agg = append(agg, j)
286309
}
287310
}
311+
// If agg is nil, return an empty slice to distinguish empty arrays
312+
// from missing paths. Note that agg can be nil even if evalResults is
313+
// non-nil if unwrapping an argument produces an empty array.
314+
if agg == nil {
315+
return []json.JSON{}, nil
316+
}
288317
return agg, nil
289318
}
290319
return evalResults, nil

pkg/util/jsonpath/eval/operation.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,16 @@ func (ctx *jsonpathCtx) evalPredicate(
290290
right = append(right, nil)
291291
}
292292

293+
// In lax mode, if either operand is empty (empty array unwrapped),
294+
// no items to compare means no matches found -> false
295+
// Note: For OpLikeRegex, evalRight is false and right contains [nil], so we only check left
296+
if !ctx.strict && len(left) == 0 {
297+
return jsonpathBoolFalse, nil
298+
}
299+
if !ctx.strict && evalRight && len(right) == 0 {
300+
return jsonpathBoolFalse, nil
301+
}
302+
293303
errored := false
294304
found := false
295305
for _, l := range left {

0 commit comments

Comments
 (0)