diff options
| author | Richard Guo | 2026-09-14 08:07:17 +0000 |
|---|---|---|
| committer | Richard Guo | 2026-09-14 08:08:17 +0000 |
| commit | 23088673d311ae274069f7147cf280ee2540aeea (patch) | |
| tree | e26887b86d6685a09eb279258b61718ba3ab0d7a | |
| parent | ccb7d9dba1c3705c544ddee129f2dec677920a03 (diff) | |
Fix const-folding of JSON constructors inside a simple CASE
A JSON constructor with a RETURNING clause uses a CaseTestExpr as the
placeholder for its result in the coercion expression. When such a
constructor appears in a WHEN clause of a simple CASE whose test
expression is a constant, eval_const_expressions substituted that
constant for the placeholder, so the coercion produced the CASE's test
value instead of the constructor's result. For instance,
CASE 'x' WHEN JSON_OBJECT('a': 'b' RETURNING text) THEN 1 ELSE 0 END
evaluated to 1.
To fix, keep case_val out of scope while simplifying the coercion, as
is already done for the elemexpr of an ArrayCoerceExpr.
Back-patch to v16, where the SQL/JSON constructor functions were
introduced.
Author: Richard Guo <guofenglinux@gmail.com>
Discussion: https://postgr.es/m/CAMbWs48A=VCFbteTkuCoknO1_0-Cu0aMBT0M07dm7vj1QyixDg@mail.gmail.com
Backpatch-through: 16
| -rw-r--r-- | src/backend/optimizer/util/clauses.c | 35 | ||||
| -rw-r--r-- | src/include/nodes/primnodes.h | 10 | ||||
| -rw-r--r-- | src/test/regress/expected/sqljson.out | 7 | ||||
| -rw-r--r-- | src/test/regress/sql/sqljson.sql | 3 |
4 files changed, 49 insertions, 6 deletions
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 8629cb2455a..c430f701324 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -2922,7 +2922,6 @@ eval_const_expressions_mutator(Node *node, } break; } - case T_JsonValueExpr: { JsonValueExpr *jve = (JsonValueExpr *) node; @@ -2946,7 +2945,41 @@ eval_const_expressions_mutator(Node *node, (Expr *) formatted_expr, copyObject(jve->format)); } + case T_JsonConstructorExpr: + { + JsonConstructorExpr *jce = (JsonConstructorExpr *) node; + JsonConstructorExpr *newjce; + Node *save_case_val; + /* + * Copy the node and const-simplify its arguments. We can't + * use ece_generic_processing() here because we need to mess + * with case_val only while processing the coercion. + */ + newjce = makeNode(JsonConstructorExpr); + memcpy(newjce, jce, sizeof(JsonConstructorExpr)); + newjce->args = (List *) + eval_const_expressions_mutator((Node *) jce->args, + context); + newjce->func = (Expr *) + eval_const_expressions_mutator((Node *) jce->func, + context); + + /* + * Set up for the CaseTestExpr node contained in the coercion. + * We must prevent it from absorbing any outer CASE value. + */ + save_case_val = context->case_val; + context->case_val = NULL; + + newjce->coercion = (Expr *) + eval_const_expressions_mutator((Node *) jce->coercion, + context); + + context->case_val = save_case_val; + + return (Node *) newjce; + } case T_SubPlan: case T_AlternativeSubPlan: diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index b3c077f8d7c..464f6b5998c 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1344,11 +1344,11 @@ typedef struct CaseWhen * * Placeholder for intermediate results in some SQL/JSON expression nodes, * such as JsonConstructorExpr. * - * The uses in CaseExpr and ArrayCoerceExpr are safe only to the extent that - * there is not any other CaseExpr or ArrayCoerceExpr between the value source - * node and its child CaseTestExpr(s). This is true in the parse analysis - * output, but the planner's function-inlining logic has to be careful not to - * break it. + * The uses in CaseExpr, ArrayCoerceExpr, and JsonConstructorExpr are safe + * only to the extent that there is not any other such node between the + * value source node and its child CaseTestExpr(s). This is true in the + * parse analysis output, but the planner's constant-folding of simple CASE + * and its function-inlining logic have to be careful not to break it. * * The nested-assignment-expression case is safe because the only node types * that can be above such CaseTestExprs are FieldStore and SubscriptingRef. diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index fe9dc286747..c130bed82e7 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -571,6 +571,13 @@ SELECT JSON_OBJECT(1: 1, '2': NULL, '3': 1, 4: NULL, '5': 'a' ABSENT ON NULL WIT {"1": 1, "3": 1, "5": "a"} (1 row) +-- the RETURNING coercion must not pick up the test value of an enclosing CASE +SELECT CASE 'x' WHEN JSON_OBJECT('a': 'b' RETURNING text) THEN 1 ELSE 0 END; + case +------ + 0 +(1 row) + -- BUG: https://postgr.es/m/CADXhmgTJtJZK9A3Na_ry%2BXrq-ghjcejBRhcRMzWZvbd__QdgJA%40mail.gmail.com -- datum_to_jsonb_internal() didn't catch keys that are casts instead of a simple scalar CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral'); diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index a995bfa9153..9a34ecb91a9 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -152,6 +152,9 @@ SELECT JSON_OBJECT(1: 1, '2': NULL, '1': 1 ABSENT ON NULL WITH UNIQUE RETURNING SELECT JSON_OBJECT(1: 1, '2': NULL, '1': 1 ABSENT ON NULL WITHOUT UNIQUE RETURNING jsonb); SELECT JSON_OBJECT(1: 1, '2': NULL, '3': 1, 4: NULL, '5': 'a' ABSENT ON NULL WITH UNIQUE RETURNING jsonb); +-- the RETURNING coercion must not pick up the test value of an enclosing CASE +SELECT CASE 'x' WHEN JSON_OBJECT('a': 'b' RETURNING text) THEN 1 ELSE 0 END; + -- BUG: https://postgr.es/m/CADXhmgTJtJZK9A3Na_ry%2BXrq-ghjcejBRhcRMzWZvbd__QdgJA%40mail.gmail.com -- datum_to_jsonb_internal() didn't catch keys that are casts instead of a simple scalar CREATE TYPE mood AS ENUM ('happy', 'sad', 'neutral'); |
