From 4086fdaf25228e87469ac04a089a6a8d562e7a6b Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
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 <lic@highgo.com>
---
 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)

