From d873f2db7b809a55b11f5d783d3ec3d332c5451b Mon Sep 17 00:00:00 2001
From: Richard Guo <guofenglinux@gmail.com>
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 <li.evan.chao@gmail.com>
Reviewed-by: Ewan Young <kdbase.hack@gmail.com>
Reviewed-by: Richard Guo <guofenglinux@gmail.com>
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)

