From d873f2db7b809a55b11f5d783d3ec3d332c5451b Mon Sep 17 00:00:00 2001 From: Richard Guo Date: Thu, 23 Jul 2026 15:34:55 +0900 Subject: [PATCH v2] Fix deparsing of JSON_ARRAY(subquery) with a FORMAT clause Commit 8d829f5a0 introduced the JSCTOR_JSON_ARRAY_QUERY constructor type so that ruleutils.c could deparse JSON_ARRAY(subquery) using its original syntax, storing the transformed subquery in a new orig_query field. However, the input FORMAT clause of JSON_ARRAY(subquery FORMAT ...) was not preserved for deparsing. The format was recorded only in the executable expression kept in the func field, which ruleutils.c does not inspect, so ruleutils.c silently dropped it. This is more than cosmetic, because FORMAT JSON changes the result: without it a text value is treated as a string to be quoted, while with it the value is treated as already-formatted JSON. To fix, record the input FORMAT in a new deparse-only field of JsonConstructorExpr, alongside orig_query, and emit it in ruleutils.c. Bump catalog version. Author: Chao Li Reviewed-by: Ewan Young Reviewed-by: Richard Guo Discussion: https://postgr.es/m/4C89B193-7D54-4705-9CF9-F0D484B9E099@gmail.com Backpatch-through: 19 --- src/backend/parser/parse_expr.c | 3 +++ src/backend/utils/adt/ruleutils.c | 1 + src/include/nodes/primnodes.h | 5 +++++ src/test/regress/expected/sqljson.out | 7 +++++++ src/test/regress/sql/sqljson.sql | 8 ++++++++ 5 files changed, 24 insertions(+) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e6ea34a7809..30c889f505f 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -3808,6 +3808,8 @@ transformJsonObjectConstructor(ParseState *pstate, JsonObjectConstructor *ctor) * - orig_query: the transformed Query of the user's original subquery, so * that ruleutils.c can deparse the original JSON_ARRAY(SELECT ...) syntax * for view definitions. + * + * - format: the input FORMAT clause, so that ruleutils.c can deparse it. */ static Node * transformJsonArrayQueryConstructor(ParseState *pstate, @@ -3944,6 +3946,7 @@ transformJsonArrayQueryConstructor(ParseState *pstate, false, ctor->absent_on_null, ctor->location); ((JsonConstructorExpr *) result)->orig_query = (Node *) query; + ((JsonConstructorExpr *) result)->format = ctor->format; return result; } diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 1b44b7a78d2..043e43b6309 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -12291,6 +12291,7 @@ get_json_constructor(JsonConstructorExpr *ctor, deparse_context *context, context->prettyFlags, context->wrapColumn, context->indentLevel); + get_json_format(ctor->format, buf); get_json_constructor_options(ctor, buf); appendStringInfoChar(buf, ')'); diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index cacef7d4151..1f712666511 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1718,6 +1718,10 @@ typedef enum JsonConstructorType * orig_query holds the user's original subquery for JSON_ARRAY(query), used * only by ruleutils.c for deparsing; it is not walked because func is * authoritative for all other purposes. + * + * format likewise holds the input FORMAT clause of JSON_ARRAY(query), which + * is otherwise only represented inside func; it is used only by ruleutils.c + * for deparsing. */ typedef struct JsonConstructorExpr { @@ -1728,6 +1732,7 @@ typedef struct JsonConstructorExpr Expr *coercion; /* coercion to RETURNING type */ JsonReturning *returning; /* RETURNING clause */ Node *orig_query; /* original subquery for deparsing */ + JsonFormat *format; /* input FORMAT for JSON_ARRAY(query) */ bool absent_on_null; /* ABSENT ON NULL? */ bool unique; /* WITH UNIQUE KEYS? (JSON_OBJECT[AGG] only) */ ParseLoc location; diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index 091a0b98574..d72278d67ca 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1233,6 +1233,13 @@ CREATE OR REPLACE VIEW public.json_array_subquery_view AS SELECT JSON_ARRAY( SELECT foo.i FROM ( VALUES (1), (2), (NULL::integer), (4)) foo(i) RETURNING text) AS "json_array" DROP VIEW json_array_subquery_view; +-- JSON_ARRAY(subquery) with an input FORMAT clause +CREATE VIEW json_array_subquery_view AS +SELECT JSON_ARRAY(SELECT '{"a": 1}'::text FORMAT JSON); +\sv json_array_subquery_view +CREATE OR REPLACE VIEW public.json_array_subquery_view AS + SELECT JSON_ARRAY( SELECT '{"a": 1}'::text AS text FORMAT JSON RETURNING json) AS "json_array" +DROP VIEW json_array_subquery_view; -- Test mutability of JSON_OBJECTAGG, JSON_ARRAYAGG, JSON_ARRAY, JSON_OBJECT create type comp1 as (a int, b date); create domain d_comp1 as comp1; diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 2550da15c45..96217a55935 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -443,6 +443,14 @@ SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING DROP VIEW json_array_subquery_view; +-- JSON_ARRAY(subquery) with an input FORMAT clause +CREATE VIEW json_array_subquery_view AS +SELECT JSON_ARRAY(SELECT '{"a": 1}'::text FORMAT JSON); + +\sv json_array_subquery_view + +DROP VIEW json_array_subquery_view; + -- Test mutability of JSON_OBJECTAGG, JSON_ARRAYAGG, JSON_ARRAY, JSON_OBJECT create type comp1 as (a int, b date); create domain d_comp1 as comp1; -- 2.39.5 (Apple Git-154)