From 18a852b7bddd3355fb9f41f9ef467beef3160866 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <akorotkov@postgresql.org>
Date: Thu, 9 Jul 2026 21:39:31 +0300
Subject: [PATCH v3 2/5] Fix JSON_TABLE PLAN deparse to keep parentheses around
 nested joins

get_json_table_plan() parenthesized the child of a parent/child
(OUTER/INNER) plan only when that child was a sibling (UNION/CROSS)
join, not when it was itself a parent/child join.  A plan such as
PLAN (p0 OUTER (p1 INNER p11)) was therefore deparsed as
PLAN (p0 OUTER p1 INNER p11), which does not parse back to the same
plan tree -- a dump/restore hazard.  Parenthesize the child whenever it
is not a bare path name, matching the logic already used for the
operands of sibling joins.

Reported-by: Thom Brown <thom@linux.com>
Discussion: https://postgr.es/m/CAA-aLv7aZGSExnbjJRw8eKkoXbu34TdoKLLA2gPye3aHjO5OSA@mail.gmail.com
---
 src/backend/utils/adt/ruleutils.c             |  3 +-
 .../regress/expected/sqljson_jsontable.out    | 29 +++++++++++++++++++
 src/test/regress/sql/sqljson_jsontable.sql    | 15 ++++++++++
 3 files changed, 46 insertions(+), 1 deletion(-)

diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index b83c8b9d7b5..0ea38c18ca4 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -12768,7 +12768,8 @@ get_json_table_plan(TableFunc *tf, JsonTablePlan *plan, deparse_context *context
 			appendStringInfoString(context->buf,
 								   s->outerJoin ? " OUTER " : " INNER ");
 			get_json_table_plan(tf, s->child, context,
-								IsA(s->child, JsonTableSiblingJoin));
+								IsA(s->child, JsonTableSiblingJoin) ||
+								castNode(JsonTablePathScan, s->child)->child);
 		}
 	}
 	else if (IsA(plan, JsonTableSiblingJoin))
diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out
index e714e36baba..7f2327c55f5 100644
--- a/src/test/regress/expected/sqljson_jsontable.out
+++ b/src/test/regress/expected/sqljson_jsontable.out
@@ -504,6 +504,35 @@ DROP VIEW jsonb_table_view4;
 DROP VIEW jsonb_table_view5;
 DROP VIEW jsonb_table_view6;
 DROP DOMAIN jsonb_test_domain;
+-- Parentheses around a nested parent/child plan must survive a dump, so the
+-- plan parses back to the same tree.
+CREATE VIEW jsonb_table_view_plan AS
+SELECT * FROM JSON_TABLE(
+	jsonb 'null', '$' AS p0
+	COLUMNS (
+		NESTED PATH '$.a[*]' AS p1 COLUMNS (
+			NESTED PATH '$.b[*]' AS p11 COLUMNS (b int PATH '$')
+		)
+	)
+	PLAN (p0 OUTER (p1 INNER p11))
+);
+\sv jsonb_table_view_plan
+CREATE OR REPLACE VIEW public.jsonb_table_view_plan AS
+ SELECT b
+   FROM JSON_TABLE(
+            'null'::jsonb, '$' AS p0
+            COLUMNS (
+                NESTED PATH '$."a"[*]' AS p1
+                COLUMNS (
+                    NESTED PATH '$."b"[*]' AS p11
+                    COLUMNS (
+                        b integer PATH '$'
+                    )
+                )
+            )
+            PLAN (p0 OUTER (p1 INNER p11))
+        )
+DROP VIEW jsonb_table_view_plan;
 -- JSON_TABLE: only one FOR ORDINALITY columns allowed
 SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, a int PATH '$.a' ERROR ON EMPTY)) jt;
 ERROR:  only one FOR ORDINALITY column is allowed
diff --git a/src/test/regress/sql/sqljson_jsontable.sql b/src/test/regress/sql/sqljson_jsontable.sql
index 22f9c5ae92f..e3f5ee1cab2 100644
--- a/src/test/regress/sql/sqljson_jsontable.sql
+++ b/src/test/regress/sql/sqljson_jsontable.sql
@@ -237,6 +237,21 @@ DROP VIEW jsonb_table_view5;
 DROP VIEW jsonb_table_view6;
 DROP DOMAIN jsonb_test_domain;
 
+-- Parentheses around a nested parent/child plan must survive a dump, so the
+-- plan parses back to the same tree.
+CREATE VIEW jsonb_table_view_plan AS
+SELECT * FROM JSON_TABLE(
+	jsonb 'null', '$' AS p0
+	COLUMNS (
+		NESTED PATH '$.a[*]' AS p1 COLUMNS (
+			NESTED PATH '$.b[*]' AS p11 COLUMNS (b int PATH '$')
+		)
+	)
+	PLAN (p0 OUTER (p1 INNER p11))
+);
+\sv jsonb_table_view_plan
+DROP VIEW jsonb_table_view_plan;
+
 -- JSON_TABLE: only one FOR ORDINALITY columns allowed
 SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, id2 FOR ORDINALITY, a int PATH '$.a' ERROR ON EMPTY)) jt;
 SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (id FOR ORDINALITY, a int PATH '$' ERROR ON EMPTY)) jt;
-- 
2.50.1 (Apple Git-155)

