1
0
mirror of https://github.com/postgres/postgres.git synced 2025-10-15 05:46:52 +03:00

Fix internal error from CollateExpr in SQL/JSON DEFAULT expressions

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 <jian.universality@gmail.com>
Author: Jian He <jian.universality@gmail.com>
Reviewed-by: Amit Langote <amitlangote09@gmail.com>
Discussion: https://postgr.es/m/CACJufxHVwYYSyiVQ6o+PsRX6zQ7rAFinh_fv1kCfTsT1xG4Zeg@mail.gmail.com
Backpatch-through: 17
This commit is contained in:
Amit Langote
2025-10-09 01:07:59 -04:00
parent a5a68dd6d5
commit ef5e60a9d3
4 changed files with 113 additions and 14 deletions

View File

@@ -1274,12 +1274,8 @@ exprSetCollation(Node *expr, Oid collation)
} }
break; break;
case T_JsonBehavior: case T_JsonBehavior:
{ Assert(((JsonBehavior *) expr)->expr == NULL ||
JsonBehavior *behavior = (JsonBehavior *) expr; exprCollation(((JsonBehavior *) expr)->expr) == collation);
if (behavior->expr)
exprSetCollation(behavior->expr, collation);
}
break; break;
case T_NullTest: case T_NullTest:
/* NullTest's result is boolean ... */ /* NullTest's result is boolean ... */

View File

@@ -94,7 +94,8 @@ static Node *transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func);
static void transformJsonPassingArgs(ParseState *pstate, const char *constructName, static void transformJsonPassingArgs(ParseState *pstate, const char *constructName,
JsonFormatType format, List *args, JsonFormatType format, List *args,
List **passing_values, List **passing_names); 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, JsonBehaviorType default_behavior,
JsonReturning *returning); JsonReturning *returning);
static Node *GetJsonBehaviorConst(JsonBehaviorType btype, int location); static Node *GetJsonBehaviorConst(JsonBehaviorType btype, int location);
@@ -4529,13 +4530,16 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
{ {
jsexpr->returning->typid = BOOLOID; jsexpr->returning->typid = BOOLOID;
jsexpr->returning->typmod = -1; jsexpr->returning->typmod = -1;
jsexpr->collation = InvalidOid;
} }
/* JSON_TABLE() COLUMNS can specify a non-boolean type. */ /* JSON_TABLE() COLUMNS can specify a non-boolean type. */
if (jsexpr->returning->typid != BOOLOID) if (jsexpr->returning->typid != BOOLOID)
jsexpr->use_json_coercion = true; 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, JSON_BEHAVIOR_FALSE,
jsexpr->returning); jsexpr->returning);
break; break;
@@ -4550,6 +4554,8 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
ret->typmod = -1; ret->typmod = -1;
} }
jsexpr->collation = get_typcollation(jsexpr->returning->typid);
/* /*
* Keep quotes on scalar strings by default, omitting them only if * Keep quotes on scalar strings by default, omitting them only if
* OMIT QUOTES is specified. * OMIT QUOTES is specified.
@@ -4566,11 +4572,15 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
jsexpr->use_json_coercion = true; jsexpr->use_json_coercion = true;
/* Assume NULL ON EMPTY when ON EMPTY is not specified. */ /* 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, JSON_BEHAVIOR_NULL,
jsexpr->returning); jsexpr->returning);
/* Assume NULL ON ERROR when ON ERROR is not specified. */ /* 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, JSON_BEHAVIOR_NULL,
jsexpr->returning); jsexpr->returning);
break; break;
@@ -4582,6 +4592,7 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
jsexpr->returning->typid = TEXTOID; jsexpr->returning->typid = TEXTOID;
jsexpr->returning->typmod = -1; jsexpr->returning->typmod = -1;
} }
jsexpr->collation = get_typcollation(jsexpr->returning->typid);
/* /*
* Override whatever transformJsonOutput() set these to, which * Override whatever transformJsonOutput() set these to, which
@@ -4607,11 +4618,15 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
} }
/* Assume NULL ON EMPTY when ON EMPTY is not specified. */ /* 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, JSON_BEHAVIOR_NULL,
jsexpr->returning); jsexpr->returning);
/* Assume NULL ON ERROR when ON ERROR is not specified. */ /* 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, JSON_BEHAVIOR_NULL,
jsexpr->returning); jsexpr->returning);
break; break;
@@ -4622,6 +4637,7 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
jsexpr->returning->typid = exprType(jsexpr->formatted_expr); jsexpr->returning->typid = exprType(jsexpr->formatted_expr);
jsexpr->returning->typmod = -1; jsexpr->returning->typmod = -1;
} }
jsexpr->collation = get_typcollation(jsexpr->returning->typid);
/* /*
* Assume EMPTY ARRAY ON ERROR when ON ERROR is not specified. * Assume EMPTY ARRAY ON ERROR when ON ERROR is not specified.
@@ -4629,7 +4645,9 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func)
* ON EMPTY cannot be specified at the top level but it can be for * ON EMPTY cannot be specified at the top level but it can be for
* the individual columns. * the individual columns.
*/ */
jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, jsexpr->on_error = transformJsonBehavior(pstate,
jsexpr,
func->on_error,
JSON_BEHAVIOR_EMPTY_ARRAY, JSON_BEHAVIOR_EMPTY_ARRAY,
jsexpr->returning); jsexpr->returning);
break; break;
@@ -4705,7 +4723,8 @@ ValidJsonBehaviorDefaultExpr(Node *expr, void *context)
* Transform a JSON BEHAVIOR clause. * Transform a JSON BEHAVIOR clause.
*/ */
static JsonBehavior * static JsonBehavior *
transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior, transformJsonBehavior(ParseState *pstate, JsonExpr *jsexpr,
JsonBehavior *behavior,
JsonBehaviorType default_behavior, JsonBehaviorType default_behavior,
JsonReturning *returning) JsonReturning *returning)
{ {
@@ -4720,7 +4739,11 @@ transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior,
location = behavior->location; location = behavior->location;
if (btype == JSON_BEHAVIOR_DEFAULT) if (btype == JSON_BEHAVIOR_DEFAULT)
{ {
Oid targetcoll = jsexpr->collation;
Oid exprcoll;
expr = transformExprRecurse(pstate, behavior->expr); expr = transformExprRecurse(pstate, behavior->expr);
if (!ValidJsonBehaviorDefaultExpr(expr, NULL)) if (!ValidJsonBehaviorDefaultExpr(expr, NULL))
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH), (errcode(ERRCODE_DATATYPE_MISMATCH),
@@ -4736,6 +4759,24 @@ transformJsonBehavior(ParseState *pstate, JsonBehavior *behavior,
(errcode(ERRCODE_DATATYPE_MISMATCH), (errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("DEFAULT expression must not return a set"), errmsg("DEFAULT expression must not return a set"),
parser_errposition(pstate, exprLocation(expr)))); 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)));
} }
} }

View File

@@ -2694,6 +2694,55 @@ SELECT * FROM t5 ORDER BY c ASC, a ASC;
3 | d1 | d1 3 | d1 | d1
(3 rows) (3 rows)
-- 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 -- cleanup
RESET search_path; RESET search_path;
SET client_min_messages TO warning; SET client_min_messages TO warning;

View File

@@ -997,6 +997,19 @@ INSERT INTO t5 (a, b) values (1, 'D1'), (2, 'D2'), (3, 'd1');
-- rewriting.) -- rewriting.)
SELECT * FROM t5 ORDER BY c ASC, a ASC; SELECT * FROM t5 ORDER BY c ASC, a ASC;
-- 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 -- cleanup
RESET search_path; RESET search_path;