From 924a587d8524dea432b6b3435bbad72aea9ffd05 Mon Sep 17 00:00:00 2001 From: zhenglong li Date: Mon, 24 Aug 2026 17:53:36 +0800 Subject: [PATCH v1] Fix planner's strictness check for JsonExpr contain_nonstrict_functions_walker() had no case for JsonExpr, so the SQL/JSON query functions JSON_EXISTS, JSON_QUERY and JSON_VALUE were implicitly treated as strict. They are not: while a NULL context item or path specification does yield a NULL result, a NULL PASSING argument merely becomes a jsonpath variable containing a JSON null, so the result can still be non-NULL. Likewise, inputs appearing only within the ON EMPTY / ON ERROR expressions need not affect the result at all. This could lead to wrong results in at least two ways. First, inline_function() could inline a STRICT SQL function whose body isn't actually strict in the function's parameters, so that calls with NULL arguments returned non-NULL instead of NULL. Second, when pulling up a subquery underneath an outer join, pullup_replace_vars() could decide that such an expression needs no PlaceHolderVar, allowing it to be evaluated above the join and return non-NULL for unmatched rows, where the correct result is NULL. Fix by treating JsonExpr as non-strict, as we already do for CASE, COALESCE and similar constructs. This is conservative -- a JsonExpr with no PASSING arguments and constant ON EMPTY / ON ERROR behaviors is in fact strict -- but distinguishing those cases hardly seems worth the trouble. --- src/backend/optimizer/util/clauses.c | 13 +++++++++++++ src/test/regress/expected/sqljson_queryfuncs.out | 12 ++++++++++++ src/test/regress/sql/sqljson_queryfuncs.sql | 9 +++++++++ 3 files changed, 34 insertions(+) diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 8da4ed617b5..a87f4037780 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -1147,6 +1147,19 @@ contain_nonstrict_functions_walker(Node *node, void *context) return true; else if (IsA(node, JsonConstructorExpr)) return true; + else if (IsA(node, JsonExpr)) + { + /* + * JSON_EXISTS, JSON_QUERY, and JSON_VALUE are strict with respect to + * their context item, but not their PASSING arguments: a NULL + * PASSING value merely becomes a jsonpath variable containing a JSON + * null, so the result can still be non-NULL. Likewise, inputs + * appearing only within the ON EMPTY/ON ERROR expressions need not + * affect the result at all. So we must treat the whole construct as + * non-strict. + */ + return true; + } else { /* Check other function-containing nodes */ diff --git a/src/test/regress/expected/sqljson_queryfuncs.out b/src/test/regress/expected/sqljson_queryfuncs.out index ff64dce0c59..74e3f2aff0e 100644 --- a/src/test/regress/expected/sqljson_queryfuncs.out +++ b/src/test/regress/expected/sqljson_queryfuncs.out @@ -1557,3 +1557,15 @@ SELECT JSON_VALUE(jsonb '1234', '$' RETURNING bit(3) DEFAULT 1::bit(3) ON ERROR SELECT JSON_VALUE(jsonb '"111"', '$.a' RETURNING bit(3) DEFAULT '1111' ON EMPTY); ERROR: bit string length 4 does not match type bit(3) DROP DOMAIN queryfuncs_d_varbit3; +-- Test that the planner treats JsonExpr as non-strict: JSON_VALUE() etc. can +-- return non-NULL even when their PASSING arguments are NULL. +-- When pulling up a subquery underneath an outer join, its JsonExpr output +-- must be wrapped in a PlaceHolderVar; this must return NULL, not '1'. +SELECT v FROM (VALUES (1)) a +LEFT JOIN (SELECT JSON_VALUE('1', '$' PASSING y AS p) v + FROM (VALUES (1), (2)) b(y)) ss ON false; + v +--- + +(1 row) + diff --git a/src/test/regress/sql/sqljson_queryfuncs.sql b/src/test/regress/sql/sqljson_queryfuncs.sql index a69ef253f66..f1d092bbf33 100644 --- a/src/test/regress/sql/sqljson_queryfuncs.sql +++ b/src/test/regress/sql/sqljson_queryfuncs.sql @@ -519,3 +519,12 @@ SELECT JSON_VALUE(jsonb '1234', '$' RETURNING bit(3) DEFAULT 1 ON ERROR); SELECT JSON_VALUE(jsonb '1234', '$' RETURNING bit(3) DEFAULT 1::bit(3) ON ERROR); SELECT JSON_VALUE(jsonb '"111"', '$.a' RETURNING bit(3) DEFAULT '1111' ON EMPTY); DROP DOMAIN queryfuncs_d_varbit3; + +-- Test that the planner treats JsonExpr as non-strict: JSON_VALUE() etc. can +-- return non-NULL even when their PASSING arguments are NULL. + +-- When pulling up a subquery underneath an outer join, its JsonExpr output +-- must be wrapped in a PlaceHolderVar; this must return NULL, not '1'. +SELECT v FROM (VALUES (1)) a +LEFT JOIN (SELECT JSON_VALUE('1', '$' PASSING y AS p) v + FROM (VALUES (1), (2)) b(y)) ss ON false; -- 2.50.1 (Apple Git-155)