From 4086fdaf25228e87469ac04a089a6a8d562e7a6b Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Wed, 22 Jul 2026 13:50:02 +0800 Subject: [PATCH v1] Preserve FORMAT when deparsing JSON_ARRAY(query) Commit 8d829f5a0 added support for deparsing JSON_ARRAY(query) using the original transformed subquery. However, the FORMAT clause belongs to the JSON_ARRAY constructor rather than the subquery, and was not retained in JsonConstructorExpr. Consequently, view deparsing omitted the clause, potentially changing the result when the deparsed query was executed or restored. Store the input JsonFormat in JsonConstructorExpr and emit it when deparsing JSON_ARRAY(query). Add regression coverage for a view using FORMAT JSON. Author: Chao Li --- src/backend/nodes/nodeFuncs.c | 3 +++ src/backend/parser/parse_expr.c | 3 +++ src/backend/utils/adt/ruleutils.c | 1 + src/include/nodes/primnodes.h | 3 +++ src/test/regress/expected/sqljson.out | 5 +++++ src/test/regress/sql/sqljson.sql | 5 +++++ 6 files changed, 20 insertions(+) diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 2a2e00b372e..d1fa4788184 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -3468,6 +3468,7 @@ expression_tree_mutator_impl(Node *node, MUTATE(newnode->func, jce->func, Expr *); MUTATE(newnode->coercion, jce->coercion, Expr *); MUTATE(newnode->returning, jce->returning, JsonReturning *); + MUTATE(newnode->format, jce->format, JsonFormat *); return (Node *) newnode; } @@ -4258,6 +4259,8 @@ raw_expression_tree_walker_impl(Node *node, return true; if (WALK(ctor->returning)) return true; + if (WALK(ctor->format)) + return true; } break; case T_JsonIsPredicate: diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index e6ea34a7809..8887b3f8772 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, also retained for deparsing. */ static Node * transformJsonArrayQueryConstructor(ParseState *pstate, @@ -3943,6 +3945,7 @@ transformJsonArrayQueryConstructor(ParseState *pstate, NIL, (Expr *) coalesce, returning, false, ctor->absent_on_null, ctor->location); + ((JsonConstructorExpr *) result)->format = copyObject(ctor->format); ((JsonConstructorExpr *) result)->orig_query = (Node *) query; 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..590ecdea7bc 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1718,6 +1718,8 @@ 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 holds the input FORMAT clause for JSON_ARRAY(query), also for + * deparsing only. */ typedef struct JsonConstructorExpr { @@ -1727,6 +1729,7 @@ typedef struct JsonConstructorExpr Expr *func; /* executable expression or NULL */ Expr *coercion; /* coercion to RETURNING type */ JsonReturning *returning; /* RETURNING clause */ + JsonFormat *format; /* input FORMAT clause */ Node *orig_query; /* original subquery for deparsing */ bool absent_on_null; /* ABSENT ON NULL? */ bool unique; /* WITH UNIQUE KEYS? (JSON_OBJECT[AGG] only) */ diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index 091a0b98574..b4d894087a9 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -1152,6 +1152,11 @@ SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING 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 jsonb) AS "json_array" +CREATE OR REPLACE VIEW json_array_subquery_view AS +SELECT JSON_ARRAY(SELECT '{"a": 1}'::text FORMAT JSON RETURNING jsonb); +\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 jsonb) AS "json_array" EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) ORDER BY i LIMIT 3 RETURNING jsonb); QUERY PLAN diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 2550da15c45..b5451405088 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -404,6 +404,11 @@ SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) RETURNING \sv json_array_subquery_view +CREATE OR REPLACE VIEW json_array_subquery_view AS +SELECT JSON_ARRAY(SELECT '{"a": 1}'::text FORMAT JSON RETURNING jsonb); + +\sv json_array_subquery_view + EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_ARRAY(SELECT i FROM (VALUES (1), (2), (NULL), (4)) foo(i) ORDER BY i LIMIT 3 RETURNING jsonb); -- 2.50.1 (Apple Git-155)