From b012252edc536bf571d9321b8d02eb549ccfa546 Mon Sep 17 00:00:00 2001 From: Ishjaap Singh Date: Wed, 11 Nov 2026 00:47:29 +0530 Subject: [PATCH v1] Fix jsonpath printing of operations followed by an accessor When an operation such as "-$.a" or "$.a + 1" is followed by an accessor or method, as in "(-$.a).abs()", printJsonPathItem() decided whether to parenthesize it only from operator priority. If the operation was itself an operand of another operator, the parentheses were dropped and the accessor moved onto the last operand: SELECT '(-$.a).abs() + 1'::jsonpath; jsonpath -------------------- (-$."a".abs() + 1) For {"a": -2} the original path returns 3 and the printed one -1. On v15 and later some printed forms can't be read back at all, e.g. '($.a + 1).abs() == 2' prints as ($."a" + 1.abs() == 2), which is rejected as trailing junk after a numeric literal when read back. "!", "is unknown" and "exists" followed by an accessor were printed without the outer parentheses even at the top level, so those were always rejected. Since pg_dump and the view, rule and index definition functions use this output, a view using such a path returns different results after a dump and restore, and a table column holding one can fail to restore. Parenthesize any operation or predicate that has an accessor after it, the same way numeric literals already are. --- src/backend/utils/adt/jsonpath.c | 31 +++++++++-- src/test/regress/expected/jsonpath.out | 77 ++++++++++++++++++++++++++ src/test/regress/sql/jsonpath.sql | 21 +++++++ 3 files changed, 123 insertions(+), 6 deletions(-) diff --git a/src/backend/utils/adt/jsonpath.c b/src/backend/utils/adt/jsonpath.c index 7bfc18c9888..90df45b8de6 100644 --- a/src/backend/utils/adt/jsonpath.c +++ b/src/backend/utils/adt/jsonpath.c @@ -575,7 +575,14 @@ printJsonPathItem(StringInfo buf, JsonPathItem *v, bool inKey, case jpiDiv: case jpiMod: case jpiStartsWith: - if (printBracketes) + + /* + * An operation followed by an accessor, as in "($ + 1).abs()", + * must be parenthesized whatever its priority, or the accessor + * would be read as applying to the right operand only. The same + * goes for the other operations below. + */ + if (printBracketes || jspHasNext(v)) appendStringInfoChar(buf, '('); jspGetLeftArg(v, &elem); printJsonPathItem(buf, &elem, false, @@ -588,31 +595,39 @@ printJsonPathItem(StringInfo buf, JsonPathItem *v, bool inKey, printJsonPathItem(buf, &elem, false, operationPriority(elem.type) <= operationPriority(v->type)); - if (printBracketes) + if (printBracketes || jspHasNext(v)) appendStringInfoChar(buf, ')'); break; case jpiNot: + if (jspHasNext(v)) + appendStringInfoChar(buf, '('); appendStringInfoString(buf, "!("); jspGetArg(v, &elem); printJsonPathItem(buf, &elem, false, false); appendStringInfoChar(buf, ')'); + if (jspHasNext(v)) + appendStringInfoChar(buf, ')'); break; case jpiIsUnknown: + if (jspHasNext(v)) + appendStringInfoChar(buf, '('); appendStringInfoChar(buf, '('); jspGetArg(v, &elem); printJsonPathItem(buf, &elem, false, false); appendStringInfoString(buf, ") is unknown"); + if (jspHasNext(v)) + appendStringInfoChar(buf, ')'); break; case jpiPlus: case jpiMinus: - if (printBracketes) + if (printBracketes || jspHasNext(v)) appendStringInfoChar(buf, '('); appendStringInfoChar(buf, v->type == jpiPlus ? '+' : '-'); jspGetArg(v, &elem); printJsonPathItem(buf, &elem, false, operationPriority(elem.type) <= operationPriority(v->type)); - if (printBracketes) + if (printBracketes || jspHasNext(v)) appendStringInfoChar(buf, ')'); break; case jpiAnyArray: @@ -696,10 +711,14 @@ printJsonPathItem(StringInfo buf, JsonPathItem *v, bool inKey, appendStringInfoChar(buf, ')'); break; case jpiExists: + if (jspHasNext(v)) + appendStringInfoChar(buf, '('); appendStringInfoString(buf, "exists ("); jspGetArg(v, &elem); printJsonPathItem(buf, &elem, false, false); appendStringInfoChar(buf, ')'); + if (jspHasNext(v)) + appendStringInfoChar(buf, ')'); break; case jpiType: appendStringInfoString(buf, ".type()"); @@ -735,7 +754,7 @@ printJsonPathItem(StringInfo buf, JsonPathItem *v, bool inKey, appendStringInfoString(buf, "last"); break; case jpiLikeRegex: - if (printBracketes) + if (printBracketes || jspHasNext(v)) appendStringInfoChar(buf, '('); jspInitByBuffer(&elem, v->base, v->content.like_regex.expr); @@ -767,7 +786,7 @@ printJsonPathItem(StringInfo buf, JsonPathItem *v, bool inKey, appendStringInfoChar(buf, '"'); } - if (printBracketes) + if (printBracketes || jspHasNext(v)) appendStringInfoChar(buf, ')'); break; case jpiBigint: diff --git a/src/test/regress/expected/jsonpath.out b/src/test/regress/expected/jsonpath.out index ea971e79854..02b74e2d170 100644 --- a/src/test/regress/expected/jsonpath.out +++ b/src/test/regress/expected/jsonpath.out @@ -812,6 +812,83 @@ select '((($ + 1)).a + ((2)).b ? ((((@ > 1)) || (exists(@.c)))))'::jsonpath; (($ + 1)."a" + (2)."b"?(@ > 1 || exists (@."c"))) (1 row) +-- an operation followed by an accessor keeps its parentheses when printed +select '(-$.a).abs() + 1'::jsonpath; + jsonpath +---------------------- + ((-$."a").abs() + 1) +(1 row) + +select '($.a + 1).abs() == 2'::jsonpath; + jsonpath +-------------------------- + (($."a" + 1).abs() == 2) +(1 row) + +select '($.a + $.b).c == 1'::jsonpath; + jsonpath +---------------------------- + (($."a" + $."b")."c" == 1) +(1 row) + +select '(1 + 2)[0] == 3'::jsonpath; + jsonpath +------------------- + ((1 + 2)[0] == 3) +(1 row) + +select '$[(1 + 2).floor() to last]'::jsonpath; + jsonpath +---------------------------- + $[(1 + 2).floor() to last] +(1 row) + +select '$ ? (!((-@.a).abs() == 2))'::jsonpath; + jsonpath +---------------------------- + $?(!((-@."a").abs() == 2)) +(1 row) + +select '($ like_regex "a").type() == "boolean"'::jsonpath; + jsonpath +------------------------------------------ + (($ like_regex "a").type() == "boolean") +(1 row) + +select '(!($ == 1)).type()'::jsonpath; + jsonpath +-------------------- + (!($ == 1)).type() +(1 row) + +select '(($ == 1) is unknown).type()'::jsonpath; + jsonpath +------------------------------ + (($ == 1) is unknown).type() +(1 row) + +select '(exists ($.a)).type()'::jsonpath; + jsonpath +------------------------- + (exists ($."a")).type() +(1 row) + +-- and the printed form gives the same results as the original +select p, jsonb_path_query_array(j, p::jsonpath) as result, + jsonb_path_query_array(j, p::jsonpath::text::jsonpath) as after_reparse +from (values ('(-$.a).abs() + 1'), + ('($.a + $.b).abs() == 3'), + ('(-$.a) ? (@ > 0) == 2'), + ('$ ? (!((-$.a).abs() == 2))')) v(p), + (values ('{"a": -2, "b": -1}'::jsonb)) d(j); + p | result | after_reparse +----------------------------+--------+--------------- + (-$.a).abs() + 1 | [3] | [3] + ($.a + $.b).abs() == 3 | [true] | [true] + (-$.a) ? (@ > 0) == 2 | [true] | [true] + $ ? (!((-$.a).abs() == 2)) | [] | [] +(4 rows) + select '$ ? (@.a < 1)'::jsonpath; jsonpath --------------- diff --git a/src/test/regress/sql/jsonpath.sql b/src/test/regress/sql/jsonpath.sql index 44178d8b45a..88002b6d137 100644 --- a/src/test/regress/sql/jsonpath.sql +++ b/src/test/regress/sql/jsonpath.sql @@ -157,6 +157,27 @@ select '($)'::jsonpath; select '(($))'::jsonpath; select '((($ + 1)).a + ((2)).b ? ((((@ > 1)) || (exists(@.c)))))'::jsonpath; +-- an operation followed by an accessor keeps its parentheses when printed +select '(-$.a).abs() + 1'::jsonpath; +select '($.a + 1).abs() == 2'::jsonpath; +select '($.a + $.b).c == 1'::jsonpath; +select '(1 + 2)[0] == 3'::jsonpath; +select '$[(1 + 2).floor() to last]'::jsonpath; +select '$ ? (!((-@.a).abs() == 2))'::jsonpath; +select '($ like_regex "a").type() == "boolean"'::jsonpath; +select '(!($ == 1)).type()'::jsonpath; +select '(($ == 1) is unknown).type()'::jsonpath; +select '(exists ($.a)).type()'::jsonpath; + +-- and the printed form gives the same results as the original +select p, jsonb_path_query_array(j, p::jsonpath) as result, + jsonb_path_query_array(j, p::jsonpath::text::jsonpath) as after_reparse +from (values ('(-$.a).abs() + 1'), + ('($.a + $.b).abs() == 3'), + ('(-$.a) ? (@ > 0) == 2'), + ('$ ? (!((-$.a).abs() == 2))')) v(p), + (values ('{"a": -2, "b": -1}'::jsonb)) d(j); + select '$ ? (@.a < 1)'::jsonpath; select '$ ? (@.a < -1)'::jsonpath; select '$ ? (@.a < +1)'::jsonpath; -- 2.49.0.windows.1