From 33157b6dc653f81a543bd7fdc38a3ae62b9af0ca Mon Sep 17 00:00:00 2001
From: Amit Langote <amitlan@postgresql.org>
Date: Thu, 16 Apr 2026 08:54:18 +0900
Subject: [PATCH v1] Fix pg_overexplain to emit valid output with RANGE_TABLE
 option.

overexplain_range_table() emitted the "Unprunable RTIs" and "Result
RTIs" properties before closing the "Range Table" group.  In the JSON
and YAML formats the Range Table group is rendered as an array of RTE
objects, so emitting key/value pairs inside it produced structurally
invalid output.  The XML format had a related oddity, with these
elements nested inside <Range-Table> rather than appearing as its
siblings.

These fields are properties of the PlannedStmt as a whole, not of any
individual RTE, so close the Range Table group before emitting them.
They now appear as siblings of "Range Table" in the parent Query
object, which is what was intended.

Also add a test exercising FORMAT JSON with RANGE_TABLE so that any
future regression in the output structure is caught.

Reported-by: Satyanarayana Narlapuram <satyanarlapuram@gmail.com>
Author: Satyanarayana Narlapuram <satyanarlapuram@gmail.com>
Reviewed-by: Amit Langote <amitlangote09@gmail.com>
Discussion: https://postgr.es/m/CAHg+QDdDrdqMr98a_OBYDYmK3RaT7XwCEShZfvDYKZpZTfOEjQ@mail.gmail.com
Backpatch-through: 18
---
 .../expected/pg_overexplain.out               | 122 +++++++++++++++++-
 contrib/pg_overexplain/pg_overexplain.c       |  12 +-
 contrib/pg_overexplain/sql/pg_overexplain.sql |   6 +
 3 files changed, 134 insertions(+), 6 deletions(-)

diff --git a/contrib/pg_overexplain/expected/pg_overexplain.out b/contrib/pg_overexplain/expected/pg_overexplain.out
index 05c6686d677..12ab92629ab 100644
--- a/contrib/pg_overexplain/expected/pg_overexplain.out
+++ b/contrib/pg_overexplain/expected/pg_overexplain.out
@@ -294,13 +294,131 @@ $$);
          <Security-Barrier>false</Security-Barrier>                 +
          <Lateral>false</Lateral>                                   +
        </Range-Table-Entry>                                         +
-       <Unprunable-RTIs>1 3 4</Unprunable-RTIs>                     +
-       <Result-RTIs>none</Result-RTIs>                              +
      </Range-Table>                                                 +
+     <Unprunable-RTIs>1 3 4</Unprunable-RTIs>                       +
+     <Result-RTIs>none</Result-RTIs>                                +
    </Query>                                                         +
  </explain>
 (1 row)
 
+-- Test JSON format with RANGE_TABLE to verify valid JSON structure.
+SELECT explain_filter($$
+EXPLAIN (RANGE_TABLE, FORMAT JSON, COSTS OFF)
+SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus
+$$);
+                         explain_filter                         
+----------------------------------------------------------------
+ [                                                             +
+   {                                                           +
+     "Plan": {                                                 +
+       "Node Type": "Aggregate",                               +
+       "Strategy": "Sorted",                                   +
+       "Partial Mode": "Simple",                               +
+       "Parallel Aware": false,                                +
+       "Async Capable": false,                                 +
+       "Disabled": false,                                      +
+       "Group Key": ["vegetables.genus"],                      +
+       "Plans": [                                              +
+         {                                                     +
+           "Node Type": "Sort",                                +
+           "Parent Relationship": "Outer",                     +
+           "Parallel Aware": false,                            +
+           "Async Capable": false,                             +
+           "Disabled": false,                                  +
+           "Sort Key": ["vegetables.genus", "vegetables.name"],+
+           "Plans": [                                          +
+             {                                                 +
+               "Node Type": "Append",                          +
+               "Parent Relationship": "Outer",                 +
+               "Parallel Aware": false,                        +
+               "Async Capable": false,                         +
+               "Disabled": false,                              +
+               "Append RTIs": "1",                             +
+               "Child Append RTIs": "none",                    +
+               "Subplans Removed": 0,                          +
+               "Plans": [                                      +
+                 {                                             +
+                   "Node Type": "Seq Scan",                    +
+                   "Parent Relationship": "Member",            +
+                   "Parallel Aware": false,                    +
+                   "Async Capable": false,                     +
+                   "Relation Name": "brassica",                +
+                   "Alias": "vegetables_1",                    +
+                   "Disabled": false,                          +
+                   "Scan RTI": 3                               +
+                 },                                            +
+                 {                                             +
+                   "Node Type": "Seq Scan",                    +
+                   "Parent Relationship": "Member",            +
+                   "Parallel Aware": false,                    +
+                   "Async Capable": false,                     +
+                   "Relation Name": "daucus",                  +
+                   "Alias": "vegetables_2",                    +
+                   "Disabled": false,                          +
+                   "Scan RTI": 4                               +
+                 }                                             +
+               ]                                               +
+             }                                                 +
+           ]                                                   +
+         }                                                     +
+       ]                                                       +
+     },                                                        +
+     "Range Table": [                                          +
+       {                                                       +
+         "RTI": 1,                                             +
+         "Kind": "relation",                                   +
+         "Inherited": true,                                    +
+         "In From Clause": true,                               +
+         "Eref": "vegetables (id, name, genus)",               +
+         "Relation": "vegetables",                             +
+         "Relation Kind": "partitioned_table",                 +
+         "Relation Lock Mode": "AccessShareLock",              +
+         "Permission Info Index": 1,                           +
+         "Security Barrier": false,                            +
+         "Lateral": false                                      +
+       },                                                      +
+       {                                                       +
+         "RTI": 2,                                             +
+         "Kind": "group",                                      +
+         "Inherited": false,                                   +
+         "In From Clause": false,                              +
+         "Eref": "\"*GROUP*\" (genus)",                        +
+         "Security Barrier": false,                            +
+         "Lateral": false                                      +
+       },                                                      +
+       {                                                       +
+         "RTI": 3,                                             +
+         "Kind": "relation",                                   +
+         "Inherited": false,                                   +
+         "In From Clause": true,                               +
+         "Alias": "vegetables (id, name, genus)",              +
+         "Eref": "vegetables (id, name, genus)",               +
+         "Relation": "brassica",                               +
+         "Relation Kind": "relation",                          +
+         "Relation Lock Mode": "AccessShareLock",              +
+         "Security Barrier": false,                            +
+         "Lateral": false                                      +
+       },                                                      +
+       {                                                       +
+         "RTI": 4,                                             +
+         "Kind": "relation",                                   +
+         "Inherited": false,                                   +
+         "In From Clause": true,                               +
+         "Alias": "vegetables (id, name, genus)",              +
+         "Eref": "vegetables (id, name, genus)",               +
+         "Relation": "daucus",                                 +
+         "Relation Kind": "relation",                          +
+         "Relation Lock Mode": "AccessShareLock",              +
+         "Security Barrier": false,                            +
+         "Lateral": false                                      +
+       }                                                       +
+     ],                                                        +
+     "Unprunable RTIs": "1 3 4",                               +
+     "Result RTIs": "none"                                     +
+   }                                                           +
+ ]
+(1 row)
+
 -- Test just the DEBUG option. Verify that it shows information about
 -- disabled nodes, parallel safety, and the parallelModeNeeded flag.
 SET enable_seqscan = false;
diff --git a/contrib/pg_overexplain/pg_overexplain.c b/contrib/pg_overexplain/pg_overexplain.c
index 715eda8dc56..fb277e02308 100644
--- a/contrib/pg_overexplain/pg_overexplain.c
+++ b/contrib/pg_overexplain/pg_overexplain.c
@@ -776,7 +776,14 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 		ExplainCloseGroup("Range Table Entry", NULL, true, es);
 	}
 
-	/* Print PlannedStmt fields that contain RTIs. */
+	/* Close the Range Table array before emitting PlannedStmt-level fields. */
+	ExplainCloseGroup("Range Table", "Range Table", false, es);
+
+	/*
+	 * Print PlannedStmt fields that contain RTIs.  These are properties of
+	 * the PlannedStmt, not of individual RTEs, so they belong outside the
+	 * Range Table array.
+	 */
 	if (es->format != EXPLAIN_FORMAT_TEXT ||
 		!bms_is_empty(plannedstmt->unprunableRelids))
 		overexplain_bitmapset("Unprunable RTIs", plannedstmt->unprunableRelids,
@@ -785,9 +792,6 @@ overexplain_range_table(PlannedStmt *plannedstmt, ExplainState *es)
 		!bms_is_empty(plannedstmt->resultRelationRelids))
 		overexplain_bitmapset("Result RTIs", plannedstmt->resultRelationRelids,
 							  es);
-
-	/* Close group, we're all done */
-	ExplainCloseGroup("Range Table", "Range Table", false, es);
 }
 
 /*
diff --git a/contrib/pg_overexplain/sql/pg_overexplain.sql b/contrib/pg_overexplain/sql/pg_overexplain.sql
index d07f93688a9..3f17b61a2da 100644
--- a/contrib/pg_overexplain/sql/pg_overexplain.sql
+++ b/contrib/pg_overexplain/sql/pg_overexplain.sql
@@ -66,6 +66,12 @@ EXPLAIN (DEBUG, RANGE_TABLE, FORMAT XML, COSTS OFF)
 SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus
 $$);
 
+-- Test JSON format with RANGE_TABLE to verify valid JSON structure.
+SELECT explain_filter($$
+EXPLAIN (RANGE_TABLE, FORMAT JSON, COSTS OFF)
+SELECT genus, array_agg(name ORDER BY name) FROM vegetables GROUP BY genus
+$$);
+
 -- Test just the DEBUG option. Verify that it shows information about
 -- disabled nodes, parallel safety, and the parallelModeNeeded flag.
 SET enable_seqscan = false;
-- 
2.47.3

