From d678d8a1db747d266da4e57fd814837aba0ee99c Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Mon, 9 Nov 2026 21:08:29 +0900 Subject: [PATCH v2-REL_17] SQL/JSON: Fix PASSING of string types other than text and varchar For PASSING arguments, transformJsonValueExpr() passes values of any type of the string category to the jsonpath executor as they are, without converting them to jsonb. However, JsonItemFromDatum() only handled text and varchar among those, so passing a char(n) or name value (such as current_user), or a value of a domain over text, failed at run time: ERROR: could not convert value of type character to jsonpath Fix by making JsonItemFromDatum() handle those types too. bpchar is handled like text, keeping its padding as to_jsonb() does, and name directly. Any other type of the string category, including domains over string types and types provided by extensions, is converted through its output function, again as to_jsonb() does. Doing this in the executor rather than coercing the argument in the parser leaves parse trees unchanged, so that existing views and rules using such PASSING arguments start working too. Oversight in commit 6185c9737cf, so back-patch to 17. Reported-by: Ilya Staroverov Reported-by: chunling qin <303677365@qq.com> Author: Ilya Staroverov Discussion: https://postgr.es/m/4601EAD4-F3E7-4182-9E49-5FFD3DA8CC8E@localhost.localdomain Discussion: https://postgr.es/m/19813-6a460679ae295ed6@postgresql.org Backpatch-through: 17 --- src/backend/utils/adt/jsonpath_exec.c | 50 +++++++++++++-- .../regress/expected/sqljson_queryfuncs.out | 64 +++++++++++++++++++ src/test/regress/sql/sqljson_queryfuncs.sql | 25 ++++++++ 3 files changed, 135 insertions(+), 4 deletions(-) diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index eb6c675b38a..d643d5c1958 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -3062,7 +3062,12 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) break; case TEXTOID: case VARCHAROID: + case BPCHAROID: { + /* + * Note that bpchar values keep their padding, as in + * to_jsonb() + */ text *txt = DatumGetTextPP(val); res->type = jbvString; @@ -3070,6 +3075,15 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) res->val.string.len = VARSIZE_ANY_EXHDR(txt); break; } + case NAMEOID: + { + char *str = NameStr(*DatumGetName(val)); + + res->type = jbvString; + res->val.string.val = str; + res->val.string.len = strlen(str); + break; + } case DATEOID: case TIMEOID: case TIMETZOID: @@ -3111,10 +3125,38 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) break; } default: - ereport(ERROR, - errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("could not convert value of type %s to jsonpath", - format_type_be(typid))); + { + char typcategory; + bool typispreferred; + + /* + * transformJsonValueExpr() passes values of any type of the + * string category as they are, so handle those not covered + * above (such as extension types) using their output + * function, as to_jsonb() does. + */ + get_type_category_preferred(typid, &typcategory, + &typispreferred); + if (typcategory == TYPCATEGORY_STRING) + { + Oid typoutput; + bool typisvarlena; + char *str; + + getTypeOutputInfo(typid, &typoutput, &typisvarlena); + str = OidOutputFunctionCall(typoutput, val); + + res->type = jbvString; + res->val.string.val = str; + res->val.string.len = strlen(str); + break; + } + + ereport(ERROR, + errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("could not convert value of type %s to jsonpath", + format_type_be(typid))); + } } } diff --git a/src/test/regress/expected/sqljson_queryfuncs.out b/src/test/regress/expected/sqljson_queryfuncs.out index f81cda8ea17..0889857a957 100644 --- a/src/test/regress/expected/sqljson_queryfuncs.out +++ b/src/test/regress/expected/sqljson_queryfuncs.out @@ -1435,6 +1435,70 @@ SELECT JSON_VALUE(jsonb 'null', '$a' PASSING t AS a) = t AS ok (2 rows) DROP TABLE test_passing_toast; +-- Test PASSING of string types other than text and varchar; they are +-- converted the same way as by to_jsonb(), so char(n) keeps its padding +CREATE DOMAIN queryfuncs_text AS text; +CREATE DOMAIN queryfuncs_char3 AS char(3); +SELECT JSON_EXISTS(jsonb '["abc"]', '$[*] ? (@ == $c)' PASSING 'abc'::char(3) AS c); + json_exists +------------- + t +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$c' PASSING 'ab'::char(3) AS c), + to_jsonb('ab'::char(3)); + json_query | to_jsonb +------------+---------- + "ab " | "ab " +(1 row) + +SELECT JSON_VALUE(jsonb 'null', '$c' PASSING 'abc'::name AS c); + json_value +------------ + abc +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$c' PASSING 'abc'::queryfuncs_text AS c); + json_query +------------ + "abc" +(1 row) + +SELECT JSON_QUERY(jsonb 'null', '$c' PASSING 'ab'::queryfuncs_char3 AS c); + json_query +------------ + "ab " +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '[1]', '$[*]' PASSING 'abc'::char(3) AS c + COLUMNS (v text PATH '$c')); + v +----- + abc +(1 row) + +DROP DOMAIN queryfuncs_text; +DROP DOMAIN queryfuncs_char3; +-- ... including a non-built-in string type, which uses its output function +CREATE TYPE queryfuncs_str; +CREATE FUNCTION queryfuncs_str_in(cstring) RETURNS queryfuncs_str + AS 'textin' LANGUAGE internal IMMUTABLE STRICT; +NOTICE: return type queryfuncs_str is only a shell +CREATE FUNCTION queryfuncs_str_out(queryfuncs_str) RETURNS cstring + AS 'textout' LANGUAGE internal IMMUTABLE STRICT; +NOTICE: argument type queryfuncs_str is only a shell +CREATE TYPE queryfuncs_str (INPUT = queryfuncs_str_in, OUTPUT = queryfuncs_str_out, + LIKE = text, CATEGORY = 'S'); +SELECT JSON_QUERY(jsonb 'null', '$c' PASSING 'abc'::queryfuncs_str AS c); + json_query +------------ + "abc" +(1 row) + +DROP TYPE queryfuncs_str CASCADE; +NOTICE: drop cascades to 2 other objects +DETAIL: drop cascades to function queryfuncs_str_in(cstring) +drop cascades to function queryfuncs_str_out(queryfuncs_str) -- Test ON ERROR / EMPTY value validity for the function; all fail. SELECT JSON_EXISTS(jsonb '1', '$' DEFAULT 1 ON ERROR); ERROR: invalid ON ERROR behavior diff --git a/src/test/regress/sql/sqljson_queryfuncs.sql b/src/test/regress/sql/sqljson_queryfuncs.sql index 7b2e443fdcd..47e1ea5c0f6 100644 --- a/src/test/regress/sql/sqljson_queryfuncs.sql +++ b/src/test/regress/sql/sqljson_queryfuncs.sql @@ -495,6 +495,31 @@ SELECT JSON_VALUE(jsonb 'null', '$a' PASSING t AS a) = t AS ok FROM test_passing_toast; DROP TABLE test_passing_toast; +-- Test PASSING of string types other than text and varchar; they are +-- converted the same way as by to_jsonb(), so char(n) keeps its padding +CREATE DOMAIN queryfuncs_text AS text; +CREATE DOMAIN queryfuncs_char3 AS char(3); +SELECT JSON_EXISTS(jsonb '["abc"]', '$[*] ? (@ == $c)' PASSING 'abc'::char(3) AS c); +SELECT JSON_QUERY(jsonb 'null', '$c' PASSING 'ab'::char(3) AS c), + to_jsonb('ab'::char(3)); +SELECT JSON_VALUE(jsonb 'null', '$c' PASSING 'abc'::name AS c); +SELECT JSON_QUERY(jsonb 'null', '$c' PASSING 'abc'::queryfuncs_text AS c); +SELECT JSON_QUERY(jsonb 'null', '$c' PASSING 'ab'::queryfuncs_char3 AS c); +SELECT * FROM JSON_TABLE(jsonb '[1]', '$[*]' PASSING 'abc'::char(3) AS c + COLUMNS (v text PATH '$c')); +DROP DOMAIN queryfuncs_text; +DROP DOMAIN queryfuncs_char3; +-- ... including a non-built-in string type, which uses its output function +CREATE TYPE queryfuncs_str; +CREATE FUNCTION queryfuncs_str_in(cstring) RETURNS queryfuncs_str + AS 'textin' LANGUAGE internal IMMUTABLE STRICT; +CREATE FUNCTION queryfuncs_str_out(queryfuncs_str) RETURNS cstring + AS 'textout' LANGUAGE internal IMMUTABLE STRICT; +CREATE TYPE queryfuncs_str (INPUT = queryfuncs_str_in, OUTPUT = queryfuncs_str_out, + LIKE = text, CATEGORY = 'S'); +SELECT JSON_QUERY(jsonb 'null', '$c' PASSING 'abc'::queryfuncs_str AS c); +DROP TYPE queryfuncs_str CASCADE; + -- Test ON ERROR / EMPTY value validity for the function; all fail. SELECT JSON_EXISTS(jsonb '1', '$' DEFAULT 1 ON ERROR); SELECT JSON_VALUE(jsonb '1', '$' EMPTY ON ERROR); -- 2.47.3