summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Guo2026-09-14 08:07:17 +0000
committerRichard Guo2026-09-14 08:08:17 +0000
commit23088673d311ae274069f7147cf280ee2540aeea (patch)
treee26887b86d6685a09eb279258b61718ba3ab0d7a
parentccb7d9dba1c3705c544ddee129f2dec677920a03 (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.c35
-rw-r--r--src/include/nodes/primnodes.h10
-rw-r--r--src/test/regress/expected/sqljson.out7
-rw-r--r--src/test/regress/sql/sqljson.sql3
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');