Fix internal error from CollateExpr in SQL/JSON DEFAULT expressions
authorAmit Langote <[email protected]>
Thu, 9 Oct 2025 05:07:36 +0000 (01:07 -0400)
committerAmit Langote <[email protected]>
Thu, 9 Oct 2025 05:07:36 +0000 (01:07 -0400)
SQL/JSON functions such as JSON_VALUE could fail with "unrecognized
node type" errors when a DEFAULT clause contained an explicit COLLATE
expression. That happened because assign_collations_walker() could
invoke exprSetCollation() on a JsonBehavior expression whose DEFAULT
still contained a CollateExpr, which exprSetCollation() does not
handle.

For example:

  SELECT JSON_VALUE('{"a":1}', '$.c' RETURNING text
                    DEFAULT 'A' COLLATE "C" ON EMPTY);

Fix by validating in transformJsonBehavior() that the DEFAULT
expression's collation matches the enclosing JSON expression’s
collation. In exprSetCollation(), replace the recursive call on the
JsonBehavior expression with an assertion that its collation already
matches the target, since the parser now enforces that condition.

Reported-by: Jian He <[email protected]>
Author: Jian He <[email protected]>
Reviewed-by: Amit Langote <[email protected]>
Discussion: https://postgr.es/m/CACJufxHVwYYSyiVQ6o+PsRX6zQ7rAFinh_fv1kCfTsT1xG4Zeg@mail.gmail.com
Backpatch-through: 17

src/backend/nodes/nodeFuncs.c
src/backend/parser/parse_expr.c
src/test/regress/expected/collate.icu.utf8.out
src/test/regress/sql/collate.icu.utf8.sql

index d2e2af4f8116d410f39c1d31aac56d12969edb1e..8f17c16b76a4770fcc695cfb9fdd7f6ca30422cc 100644 (file)
@@ -1266,12 +1266,8 @@ exprSetCollation(Node *expr, Oid collation)
            }
            break;
        case T_JsonBehavior:
-           {
-               JsonBehavior *behavior = (JsonBehavior *) expr;
-
-               if (behavior->expr)
-                   exprSetCollation(behavior->expr, collation);
-           }
+           Assert(((JsonBehavior *) expr)->expr == NULL ||
+                  exprCollation(((JsonBehavior *) expr)->expr) == collation);
            break;
        case T_NullTest:
            /* NullTest's result is boolean ... */
index 8423542021f305779c7973305c960449eb77e175..c8a9ccd6ed0c90f517f3e79473ae7157325467a0 100644 (file)
@@ -96,7 +96,8 @@ static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func);
 static void transformJsonPassingArgs(ParseState *pstate, const char *constructName,
                                     JsonFormatType format, List *args,
                                     List **passing_values, List **passing_names);
-static JsonBehavior *transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior,
+static JsonBehavior *transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr,
+                                          JsonBehavior *behavior,
                                           JsonBehaviorType default_behavior,
                                           JsonReturning *returning);
 static Node *GetJsonBehaviorConst(JsonBehaviorType btype, int location);
@@ -4517,13 +4518,16 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
            {
                jsexpr->returning->typid = BOOLOID;
                jsexpr->returning->typmod = -1;
+               jsexpr->collation = InvalidOid;
            }
 
            /* JSON_TABLE() COLUMNS can specify a non-boolean type. */
            if (jsexpr->returning->typid != BOOLOID)
                jsexpr->use_json_coercion = true;
 
-           jsexpr->on_error = transformJsonBehavior(pstate, func->on_error,
+           jsexpr->on_error = transformJsonBehavior(pstate,
+                                                    jsexpr,
+                                                    func->on_error,
                                                     JSON_BEHAVIOR_FALSE,
                                                     jsexpr->returning);
            break;
@@ -4538,6 +4542,8 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
                ret->typmod = -1;
            }
 
+           jsexpr->collation = get_typcollation(jsexpr->returning->typid);
+
            /*
             * Keep quotes on scalar strings by default, omitting them only if
             * OMIT QUOTES is specified.
@@ -4554,11 +4560,15 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
                jsexpr->use_json_coercion = true;
 
            /* Assume NULL ON EMPTY when ON EMPTY is not specified. */
-           jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty,
+           jsexpr->on_empty = transformJsonBehavior(pstate,
+                                                    jsexpr,
+                                                    func->on_empty,
                                                     JSON_BEHAVIOR_NULL,
                                                     jsexpr->returning);
            /* Assume NULL ON ERROR when ON ERROR is not specified. */
-           jsexpr->on_error = transformJsonBehavior(pstate, func->on_error,
+           jsexpr->on_error = transformJsonBehavior(pstate,
+                                                    jsexpr,
+                                                    func->on_error,
                                                     JSON_BEHAVIOR_NULL,
                                                     jsexpr->returning);
            break;
@@ -4570,6 +4580,7 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
                jsexpr->returning->typid = TEXTOID;
                jsexpr->returning->typmod = -1;
            }
+           jsexpr->collation = get_typcollation(jsexpr->returning->typid);
 
            /*
             * Override whatever transformJsonOutput() set these to, which
@@ -4595,11 +4606,15 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
            }
 
            /* Assume NULL ON EMPTY when ON EMPTY is not specified. */
-           jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty,
+           jsexpr->on_empty = transformJsonBehavior(pstate,
+                                                    jsexpr,
+                                                    func->on_empty,
                                                     JSON_BEHAVIOR_NULL,
                                                     jsexpr->returning);
            /* Assume NULL ON ERROR when ON ERROR is not specified. */
-           jsexpr->on_error = transformJsonBehavior(pstate, func->on_error,
+           jsexpr->on_error = transformJsonBehavior(pstate,
+                                                    jsexpr,
+                                                    func->on_error,
                                                     JSON_BEHAVIOR_NULL,
                                                     jsexpr->returning);
            break;
@@ -4610,6 +4625,7 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
                jsexpr->returning->typid = exprType(jsexpr->formatted_expr);
                jsexpr->returning->typmod = -1;
            }
+           jsexpr->collation = get_typcollation(jsexpr->returning->typid);
 
            /*
             * Assume EMPTY ARRAY ON ERROR when ON ERROR is not specified.
@@ -4617,7 +4633,9 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
             * ON EMPTY cannot be specified at the top level but it can be for
             * the individual columns.
             */
-           jsexpr->on_error = transformJsonBehavior(pstate, func->on_error,
+           jsexpr->on_error = transformJsonBehavior(pstate,
+                                                    jsexpr,
+                                                    func->on_error,
                                                     JSON_BEHAVIOR_EMPTY_ARRAY,
                                                     jsexpr->returning);
            break;
@@ -4693,7 +4711,8 @@ ValidJsonBehaviorDefaultExpr(Node *expr, void *context)
  * Transform a JSON BEHAVIOR clause.
  */
 static JsonBehavior *
-transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior,
+transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr,
+                     JsonBehavior *behavior,
                      JsonBehaviorType default_behavior,
                      JsonReturning *returning)
 {
@@ -4708,7 +4727,11 @@ transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior,
        location = behavior->location;
        if (btype == JSON_BEHAVIOR_DEFAULT)
        {
+           Oid         targetcoll = jsexpr->collation;
+           Oid         exprcoll;
+
            expr = transformExprRecurse(pstate, behavior->expr);
+
            if (!ValidJsonBehaviorDefaultExpr(expr, NULL))
                ereport(ERROR,
                        (errcode(ERRCODE_DATATYPE_MISMATCH),
@@ -4724,6 +4747,24 @@ transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior,
                        (errcode(ERRCODE_DATATYPE_MISMATCH),
                         errmsg("DEFAULT expression must not return a set"),
                         parser_errposition(pstate, exprLocation(expr))));
+
+           /*
+            * Reject a DEFAULT expression whose collation differs from the
+            * enclosing JSON expression's result collation
+            * (jsexpr->collation), as chosen by the RETURNING clause.
+            */
+           exprcoll = exprCollation(expr);
+           if (!OidIsValid(exprcoll))
+               exprcoll = get_typcollation(exprType(expr));
+           if (OidIsValid(targetcoll) && OidIsValid(exprcoll) &&
+               targetcoll != exprcoll)
+               ereport(ERROR,
+                       errcode(ERRCODE_COLLATION_MISMATCH),
+                       errmsg("the collation of DEFAULT expression conflicts with RETURNING clause"),
+                       errdetail("\"%s\" versus \"%s\"",
+                                 get_collation_name(exprcoll),
+                                 get_collation_name(targetcoll)),
+                       parser_errposition(pstate, exprLocation(expr)));
        }
    }
 
index 7a425afe1f5c752f65580e94ce18ee3f445c0819..60e15a437e4f9edcacb24041e001da51910c2711 100644 (file)
@@ -2260,6 +2260,55 @@ DROP TABLE pagg_tab3;
 RESET enable_partitionwise_aggregate;
 RESET max_parallel_workers_per_gather;
 RESET enable_incremental_sort;
+-- Check that DEFAULT expressions in SQL/JSON functions use the same collation
+-- as the RETURNING type.  Mismatched collations should raise an error.
+CREATE DOMAIN d1 AS text COLLATE case_insensitive;
+CREATE DOMAIN d2 AS text COLLATE "C";
+SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT ('C' COLLATE "C") COLLATE case_insensitive ON EMPTY) = 'a'; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C' ON EMPTY) = 'a'; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C'::d2 ON EMPTY) = 'a'; -- error
+ERROR:  the collation of DEFAULT expression conflicts with RETURNING clause
+LINE 1: ...ON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C'::d2 ON...
+                                                             ^
+DETAIL:  "C" versus "case_insensitive"
+SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C' COLLATE "C" ON EMPTY) = 'a'; -- error
+ERROR:  the collation of DEFAULT expression conflicts with RETURNING clause
+LINE 1: ...ON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C' COLLAT...
+                                                             ^
+DETAIL:  "C" versus "case_insensitive"
+SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' ON EMPTY) = 'a'; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLATE case_insensitive ON EMPTY) = 'a'; -- true
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A'::d2 ON EMPTY) = 'a'; -- error
+ERROR:  the collation of DEFAULT expression conflicts with RETURNING clause
+LINE 1: ...ON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A'::d2 ON...
+                                                             ^
+DETAIL:  "C" versus "case_insensitive"
+SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLATE "C" ON EMPTY) = 'a'; -- error
+ERROR:  the collation of DEFAULT expression conflicts with RETURNING clause
+LINE 1: ...ON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLAT...
+                                                             ^
+DETAIL:  "C" versus "case_insensitive"
+DROP DOMAIN d1, d2;
 -- cleanup
 RESET search_path;
 SET client_min_messages TO warning;
index 4eb1adf0287da598522aa3c99d718cbab1695b8d..c797b5181be589fa11a03344daad54e91b8182a3 100644 (file)
@@ -508,7 +508,6 @@ DROP TABLE test7;
 
 CREATE COLLATION testcoll_rulesx (provider = icu, locale = '', rules = '!!wrong!!');
 
-
 -- nondeterministic collations
 
 CREATE COLLATION ctest_det (provider = icu, locale = '', deterministic = true);
@@ -856,6 +855,20 @@ RESET enable_partitionwise_aggregate;
 RESET max_parallel_workers_per_gather;
 RESET enable_incremental_sort;
 
+-- Check that DEFAULT expressions in SQL/JSON functions use the same collation
+-- as the RETURNING type.  Mismatched collations should raise an error.
+CREATE DOMAIN d1 AS text COLLATE case_insensitive;
+CREATE DOMAIN d2 AS text COLLATE "C";
+SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT ('C' COLLATE "C") COLLATE case_insensitive ON EMPTY) = 'a'; -- true
+SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C' ON EMPTY) = 'a'; -- true
+SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C'::d2 ON EMPTY) = 'a'; -- error
+SELECT JSON_VALUE('{"a": "A"}', '$.a' RETURNING d1 DEFAULT 'C' COLLATE "C" ON EMPTY) = 'a'; -- error
+SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' ON EMPTY) = 'a'; -- true
+SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLATE case_insensitive ON EMPTY) = 'a'; -- true
+SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A'::d2 ON EMPTY) = 'a'; -- error
+SELECT JSON_VALUE('{"a": "A"}', '$.c' RETURNING d1 DEFAULT 'A' COLLATE "C" ON EMPTY) = 'a'; -- error
+DROP DOMAIN d1, d2;
+
 -- cleanup
 RESET search_path;
 SET client_min_messages TO warning;