public inbox for [email protected]  
help / color / mirror / Atom feed
From: Alexander Korotkov <[email protected]>
To: Nikita Malakhov <[email protected]>
Cc: Amit Langote <[email protected]>
Cc: pgsql-hackers <[email protected]>
Subject: Re: SQL/JSON json_table plan clause
Date: Wed, 8 Jul 2026 02:38:56 +0300
Message-ID: <CAPpHfdtJB2APBjD7Npa58Cg4P0KqFjNKq_mT4tpNWN2y+5zKoA@mail.gmail.com> (raw)
In-Reply-To: <CAPpHfdu-1oDzhKHXRF0vVSqab5Q3V5Sx1iQ5+tOmL_+nqY5_tA@mail.gmail.com>
References: <CAN-LCVP7HXmGu-WcinsHvdKqMGEdv=1Y67H4U58F6Y=Q0M5GyQ@mail.gmail.com>
	<CA+HiwqGqz1+jvrWHHzYSnzuVZPp4v-vfEyX-hB=3hTP3Asf78Q@mail.gmail.com>
	<CAN-LCVNSSCTdMjSFHKMkPUDcxNiGX1qn=EWMmmph8dmhkOaj_g@mail.gmail.com>
	<CA+HiwqErd3+tMB5yOerBeNJ5HzYWcByYjn=kJpEcSvdU9OADkg@mail.gmail.com>
	<CAN-LCVOH0AKMXT83SxyP7KKaCPbt_KF_Uibbpmn5_tfFvcnbWA@mail.gmail.com>
	<CAN-LCVPSYfLZHcmvct-_nGZ7peTat-Bo--04K9VXGU8p_TcbWg@mail.gmail.com>
	<CAN-LCVMk6pBMBaghaq5h5awLyWje1iaDXUPJNpWhecFvwWonkw@mail.gmail.com>
	<CAN-LCVMUFri=hm=OFxOnw=oY+yR-WQudRy35R2ZBbCxGhcM_Aw@mail.gmail.com>
	<CAPpHfdvnacwjFGhqit7gE2RevJ8TEHM3bqZjMxb=fJ7P6ZNvKw@mail.gmail.com>
	<CAN-LCVMvrkaNM+pUN=KvoJ4_c6yCVaW5gNucQWWNtsAmMxzOCA@mail.gmail.com>
	<CAN-LCVPU_NTSD+DUguOH58h0X1K5jiVeWRWshFSHzNnHys1T5A@mail.gmail.com>
	<CAPpHfdu-1oDzhKHXRF0vVSqab5Q3V5Sx1iQ5+tOmL_+nqY5_tA@mail.gmail.com>

Hi!

On Sun, Jul 5, 2026 at 12:02 AM Alexander Korotkov <[email protected]> wrote:
> On Fri, May 15, 2026 at 2:53 PM Nikita Malakhov <[email protected]> wrote:
> > Again, Alexander and Amit, thanks for the review. I've rebased the patch and made
> > some changes according to Alexander's notes. I've slightly rearranged files between
> > patches for it to be easier to review, so now there are 3 patches:
> > v23-0001-json-table-plan-clause.patch - main code changes
> > v23-0002-json-table-plan-tests.patch - test cases and out file changes
> > v23-0003-json-table-plan-docs.patch - docs package
> >
> > <...>
> > >1. IsA(planstate, JsonTableSiblingJoin) is wrong.  planstate is not a
> > >node, thus IsA() can't be applied to it.  You should instead do
> > >IsA(planstate->plan, JsonTableSiblingJoin).  It wasn't catched,
> > >because regression tests don't exercise this branch.  So, you also
> > >need to improve the coverage.
> > - done;
> >
> > >2. get_json_table() with patch uses JSON_BEHAVIOR_EMPTY as the default
> > >value for deparsing, while parsing still uses
> > >JSON_BEHAVIOR_EMPTY_ARRAY.  Looks plain wrong.  I'm not sure what is
> > >intention here.
> > - looks like leftover from older version, changed to default behavior, so unnecessary
> > emission of ERROR is omitted;
> >
> > >3. PLAN clause is always emitted during deparsing even if user didn't
> > >specify anything.  I would prefer to skip PLAN clause in this case
> > >unless there is strong reason to do the opposite (this reason must be
> > >pointed if any).
> > - done, this made test cases much more readable;
> >
> > >4. Unused typedefs in src/tools/pgindent/typedefs.list:
> > >JsonTableScanState, JsonPathSpec, JsonTablePlanStateType,
> > >JsonTableJoinState.
> > - done;
> >
> > >5. Empty comment in JsonTablePlanState definition.  Pointed by Amit,
> > >but not fixed.
> > - done;
> >
> > >6. Rename passingArgs to passing_Args for no reason in parse_jsontable.c.
> > - done;
> >
> > >7. Patch lacks documentation (also pointed by Amit)
> > - done, documentation is provided in separate patch;
> >
> > >8. Patch could use pgindent run.
> > - not done yet but would provide a newer version with it.
>
> Thank you.  I've made following additional changes.
>
>  1. Actually, I don't see my previous item 3 fully addressed.  We
> still had PLAN clause deparsed in the case user didn't specify it.  I
> see this as an undesirable behavior.  And there are at least two ways
> to fix this: don't output PLAN clause if it's not the default,
> remember if user specified as a special flag or even original clause.
> I found first way (don't output default) more appealing as it don't
> require additional struct members and we already do this in other
> similar cases (NULLS FIRST/LAST, ON EMPTY/ON ERROR).  So, I did this
> for PLAN clause.
>  2. Simplification for JsonTablePlanNextRow(): no reason to check
> (planstate->advanceNested || planstate->nested) if we already issued
> break for (planstate->nested == NULL).
>  3. Similar simplification for transformJsonTableNestedColumns().

I made some additional changes to the patch.  In the previous versions
PLAN qual required name for root path, but didn't required names for
nested paths.  I've rechecked with standard, and it appears that
standard claims the opposite: name is not required for the root path,
but required for nested paths.  So, I decided to allow skipping name
for both root path (as standard claims), and nested path (our
extension to standard).  Corresponding changes made to code, tests and
documentation.

Also, I merged 3 patches into 1, and edited sql_features.txt.

I'm going to push this if no objections.

------
Regards,
Alexander Korotkov
Supabase


Attachments:

  [application/octet-stream] v25-0001-JSON_TABLE-PLAN-clause.patch (94.4K, ../CAPpHfdtJB2APBjD7Npa58Cg4P0KqFjNKq_mT4tpNWN2y+5zKoA@mail.gmail.com/2-v25-0001-JSON_TABLE-PLAN-clause.patch)
  download | inline diff:
From 10d5cdb84ddda0dae6bdf0d18f9f5dcad9cbea60 Mon Sep 17 00:00:00 2001
From: Alexander Korotkov <[email protected]>
Date: Wed, 8 Jul 2026 02:31:41 +0300
Subject: [PATCH v25] JSON_TABLE PLAN clause
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This patch adds the PLAN clauses for JSON_TABLE, which allow the user
to specify how data from nested paths are joined, allowing
considerable freedom in shaping the tabular output of JSON_TABLE.
PLAN DEFAULT allows the user to specify the global strategies when
dealing with sibling or child nested paths. This is often sufficient
to achieve the necessary goal, and is considerably simpler than the
full PLAN clause, which allows the user to specify the strategy to be
used for each named nested path.

Path names may be attached to the row pattern and to each NESTED path
using AS.  Unlike the SQL/JSON standard, which requires a name for every
NESTED path when a PLAN clause is present, PostgreSQL does not require
them and generates a name for any path left unnamed; a specific PLAN()
can only reference paths by name, so unnamed paths it must mention are
reported as not covered by the plan.

Author: Nikita Malakhov <[email protected]>
Co-authored-by: Nikita Glukhov <[email protected]>
Co-authored-by: Teodor Sigaev <[email protected]>
Co-authored-by: Oleg Bartunov <[email protected]>
Co-authored-by: Alexander Korotkov <[email protected]>
Co-authored-by: Andrew Dunstan <[email protected]>
Co-authored-by: Amit Langote <[email protected]>
Co-authored-by: Anton Melnikov <[email protected]>
Reviewed-by: Andres Freund <[email protected]>
Reviewed-by: Pavel Stehule <[email protected]>
Reviewed-by: Andrew Alsup <[email protected]>
Reviewed-by: Erik Rijkers <[email protected]>
Reviewed-by: Zhihong Yu <[email protected]>
Reviewed-by: Himanshu Upadhyaya <[email protected]>
Reviewed-by: Daniel Gustafsson <[email protected]>
Reviewed-by: Justin Pryzby <[email protected]>
Reviewed-by: Álvaro Herrera <[email protected]>
Reviewed-by: jian he <[email protected]>
Reviewed-by: Vladlen Popolitov <[email protected]>
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/[email protected]
Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org
Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com
Discussion: https://postgr.es/m/CAN-LCVP7HXmGu-WcinsHvdKqMGEdv=1Y67H4U58F6Y=Q0M5GyQ@mail.gmail.com
---
 doc/src/sgml/func/func-json.sgml              | 176 ++++-
 src/backend/catalog/sql_features.txt          |   4 +-
 src/backend/nodes/makefuncs.c                 |  54 ++
 src/backend/parser/gram.y                     | 101 ++-
 src/backend/parser/parse_jsontable.c          | 382 ++++++++--
 src/backend/utils/adt/jsonpath_exec.c         | 171 +++--
 src/backend/utils/adt/ruleutils.c             |  99 +++
 src/include/nodes/makefuncs.h                 |   5 +
 src/include/nodes/parsenodes.h                |  43 ++
 src/include/nodes/primnodes.h                 |   2 +
 .../regress/expected/sqljson_jsontable.out    | 655 +++++++++++++++++-
 src/test/regress/sql/sqljson_jsontable.sql    | 434 ++++++++++++
 src/tools/pgindent/typedefs.list              |   3 +
 13 files changed, 2017 insertions(+), 112 deletions(-)

diff --git a/doc/src/sgml/func/func-json.sgml b/doc/src/sgml/func/func-json.sgml
index 3d97e2b5375..541d02d4a6e 100644
--- a/doc/src/sgml/func/func-json.sgml
+++ b/doc/src/sgml/func/func-json.sgml
@@ -3663,6 +3663,11 @@ JSON_TABLE (
     <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> AS <replaceable>json_path_name</replaceable> </optional> <optional> PASSING { <replaceable>value</replaceable> AS <replaceable>varname</replaceable> } <optional>, ...</optional> </optional>
     COLUMNS ( <replaceable class="parameter">json_table_column</replaceable> <optional>, ...</optional> )
     <optional> { <literal>ERROR</literal> | <literal>EMPTY</literal> <optional>ARRAY</optional>} <literal>ON ERROR</literal> </optional>
+    <optional>
+        PLAN ( <replaceable class="parameter">json_table_plan</replaceable> ) |
+        PLAN DEFAULT ( { INNER | OUTER } <optional> , { CROSS | UNION } </optional>
+                     | { CROSS | UNION } <optional> , { INNER | OUTER } </optional> )
+    </optional>
 )
 
 <phrase>
@@ -3679,6 +3684,16 @@ where <replaceable class="parameter">json_table_column</replaceable> is:
   | <replaceable>name</replaceable> <replaceable>type</replaceable> EXISTS <optional> PATH <replaceable>path_expression</replaceable> </optional>
         <optional> { ERROR | TRUE | FALSE | UNKNOWN } ON ERROR </optional>
   | NESTED <optional> PATH </optional> <replaceable>path_expression</replaceable> <optional> AS <replaceable>json_path_name</replaceable> </optional> COLUMNS ( <replaceable>json_table_column</replaceable> <optional>, ...</optional> )
+<phrase>
+<replaceable>json_table_plan</replaceable> is:
+</phrase>
+    <replaceable>json_path_name</replaceable> <optional> { OUTER | INNER } <replaceable>json_table_plan_primary</replaceable> </optional>
+  | <replaceable>json_table_plan_primary</replaceable> { UNION <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional>
+  | <replaceable>json_table_plan_primary</replaceable> { CROSS <replaceable>json_table_plan_primary</replaceable> } <optional>...</optional>
+<phrase>
+<replaceable>json_table_plan_primary</replaceable> is:
+</phrase>
+    <replaceable>json_path_name</replaceable> | ( <replaceable>json_table_plan</replaceable> )
 </synopsis>
 
   <para>
@@ -3842,6 +3857,11 @@ where <replaceable class="parameter">json_table_column</replaceable> is:
      in a single function invocation rather than chaining several
      <function>JSON_TABLE</function> expressions in an SQL statement.
     </para>
+
+    <para>
+     You can use the <literal>PLAN</literal> clause to define how
+     to join the columns returned by <literal>NESTED PATH</literal> clauses.
+    </para>
     </listitem>
    </varlistentry>
   </variablelist>
@@ -3866,12 +3886,130 @@ where <replaceable class="parameter">json_table_column</replaceable> is:
 
     <para>
      The optional <replaceable>json_path_name</replaceable> serves as an
-     identifier of the provided <replaceable>path_expression</replaceable>.
-     The name must be unique and distinct from the column names.
+     identifier of the provided <replaceable>json_path_specification</replaceable>.
+     The path name must be unique and distinct from the column names.  Each
+     path name can appear in the <literal>PLAN</literal> clause only once.
+    </para>
+    <para>
+     The SQL/JSON standard requires an explicit path name for every
+     <literal>NESTED PATH</literal> when a <literal>PLAN</literal> or
+     <literal>PLAN DEFAULT</literal> clause is present, while the row pattern
+     path name always remains optional.  As an extension,
+     <productname>PostgreSQL</productname> does not require any path name: a
+     name is generated for any path left unnamed.  Note, however, that a
+     specific <literal>PLAN</literal> (<replaceable>json_table_plan</replaceable>)
+     can only refer to paths by name, so every path that such a plan must
+     mention has to be given a name explicitly.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <literal>PLAN</literal> ( <replaceable>json_table_plan</replaceable> )
+    </term>
+    <listitem>
+
+    <para>
+     Defines how to join the data returned by <literal>NESTED PATH</literal>
+     clauses to the constructed view.
+    </para>
+    <para>
+     To join columns with parent/child relationship, you can use:
+    </para>
+  <variablelist>
+   <varlistentry>
+    <term>
+     <literal>INNER</literal>
+    </term>
+    <listitem>
+
+    <para>
+     Use <literal>INNER JOIN</literal>, so that the parent row
+     is omitted from the output if it does not have any child rows
+     after joining the data returned by <literal>NESTED PATH</literal>.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <literal>OUTER</literal>
+    </term>
+    <listitem>
+
+    <para>
+     Use <literal>LEFT OUTER JOIN</literal>, so that the parent row
+     is always included into the output even if it does not have any child rows
+     after joining the data returned by <literal>NESTED PATH</literal>, with NULL values
+     inserted into the child columns if the corresponding
+     values are missing.
+    </para>
+    <para>
+     This is the default option for joining columns with parent/child relationship.
+    </para>
+    </listitem>
+   </varlistentry>
+  </variablelist>
+     <para>
+     To join sibling columns, you can use:
+    </para>
+
+  <variablelist>
+   <varlistentry>
+    <term>
+     <literal>UNION</literal>
+    </term>
+    <listitem>
+
+    <para>
+     Generate one row for each value produced by each of the sibling
+     columns. The columns from the other siblings are set to null.
+    </para>
+    <para>
+     This is the default option for joining sibling columns.
     </para>
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term>
+     <literal>CROSS</literal>
+    </term>
+    <listitem>
+
+    <para>
+     Generate one row for each combination of values from the sibling columns.
+    </para>
+    </listitem>
+   </varlistentry>
+
+  </variablelist>
+
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <literal>PLAN DEFAULT</literal> ( <literal><replaceable>OUTER | INNER</replaceable> <optional>, <replaceable>UNION | CROSS</replaceable> </optional></literal> )
+    </term>
+    <listitem>
+     <para>
+      The terms can also be specified in reverse order. The
+      <literal>INNER</literal> or <literal>OUTER</literal> option defines the
+      joining plan for parent/child columns, while <literal>UNION</literal> or
+      <literal>CROSS</literal> affects joins of sibling columns. This form
+      of <literal>PLAN</literal> overrides the default plan for
+      all columns at once.
+     </para>
+     <para>
+      <literal>PLAN DEFAULT</literal> is simpler than specifying a complete
+      <literal>PLAN</literal>, and is often all that is required to get the desired
+      output.
+     </para>
+    </listitem>
+   </varlistentry>
+
    <varlistentry>
     <term>
      { <literal>ERROR</literal> | <literal>EMPTY</literal> } <literal>ON ERROR</literal>
@@ -4082,5 +4220,39 @@ COLUMNS (
 </screen>
 
      </para>
+
+     <para>
+      Find a director that has done films in two different genres:
+<screen>
+SELECT
+  director1 AS director, title1, kind1, title2, kind2
+FROM
+  my_films,
+  JSON_TABLE ( js, '$.favorites' AS favs COLUMNS (
+    NESTED PATH '$[*]' AS films1 COLUMNS (
+      kind1 text PATH '$.kind',
+      NESTED PATH '$.films[*]' AS film1 COLUMNS (
+        title1 text PATH '$.title',
+        director1 text PATH '$.director')
+    ),
+    NESTED PATH '$[*]' AS films2 COLUMNS (
+      kind2 text PATH '$.kind',
+      NESTED PATH '$.films[*]' AS film2 COLUMNS (
+        title2 text PATH '$.title',
+        director2 text PATH '$.director'
+      )
+    )
+   )
+   PLAN (favs OUTER ((films1 INNER film1) CROSS (films2 INNER film2)))
+  ) AS jt
+ WHERE kind1 > kind2 AND director1 = director2;
+
+     director     | title1  |  kind1   | title2 | kind2
+------------------+---------+----------+--------+--------
+ Alfred Hitchcock | Vertigo | thriller | Psycho | horror
+(1 row)
+</screen>
+     </para>
+
   </sect2>
  </sect1>
diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt
index 626054cbcef..55f073e2262 100644
--- a/src/backend/catalog/sql_features.txt
+++ b/src/backend/catalog/sql_features.txt
@@ -650,7 +650,7 @@ T814	Colon in JSON_OBJECT or JSON_OBJECTAGG			YES
 T821	Basic SQL/JSON query operators			YES	
 T822	SQL/JSON: IS JSON WITH UNIQUE KEYS predicate			YES	
 T823	SQL/JSON: PASSING clause			YES	
-T824	JSON_TABLE: specific PLAN clause			NO	
+T824	JSON_TABLE: specific PLAN clause			YES	
 T825	SQL/JSON: ON EMPTY and ON ERROR clauses			YES	
 T826	General value expression in ON ERROR or ON EMPTY clauses			YES	
 T827	JSON_TABLE: sibling NESTED COLUMNS clauses			YES	
@@ -664,7 +664,7 @@ T834	SQL/JSON path language: wildcard member accessor			YES
 T835	SQL/JSON path language: filter expressions			YES	
 T836	SQL/JSON path language: starts with predicate			YES	
 T837	SQL/JSON path language: regex_like predicate			YES	
-T838	JSON_TABLE: PLAN DEFAULT clause			NO	
+T838	JSON_TABLE: PLAN DEFAULT clause			YES	
 T839	Formatted cast of datetimes to/from character strings			NO	
 T840	Hex integer literals in SQL/JSON path language			YES	
 T851	SQL/JSON: optional keywords for default syntax			YES	
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 40b09958ac2..cdc02b274ff 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -963,6 +963,60 @@ makeJsonBehavior(JsonBehaviorType btype, Node *expr, int location)
 	return behavior;
 }
 
+/*
+ * makeJsonTableDefaultPlan -
+ *	   creates a JsonTablePlanSpec node to represent a "default" JSON_TABLE plan
+ *	   with given join strategy
+ */
+Node *
+makeJsonTableDefaultPlan(JsonTablePlanJoinType join_type, int location)
+{
+	JsonTablePlanSpec *n = makeNode(JsonTablePlanSpec);
+
+	n->plan_type = JSTP_DEFAULT;
+	n->join_type = join_type;
+	n->location = location;
+
+	return (Node *) n;
+}
+
+/*
+ * makeJsonTableSimplePlan -
+ *	   creates a JsonTablePlanSpec node to represent a "simple" JSON_TABLE plan
+ *	   for given PATH
+ */
+Node *
+makeJsonTableSimplePlan(char *pathname, int location)
+{
+	JsonTablePlanSpec *n = makeNode(JsonTablePlanSpec);
+
+	n->plan_type = JSTP_SIMPLE;
+	n->pathname = pathname;
+	n->location = location;
+
+	return (Node *) n;
+}
+
+/*
+ * makeJsonTableJoinedPlan -
+ *	   creates a JsonTablePlanSpec node to represent join between the given
+ *	   pair of plans
+ */
+Node *
+makeJsonTableJoinedPlan(JsonTablePlanJoinType type, Node *plan1, Node *plan2,
+						int location)
+{
+	JsonTablePlanSpec *n = makeNode(JsonTablePlanSpec);
+
+	n->plan_type = JSTP_JOINED;
+	n->join_type = type;
+	n->plan1 = castNode(JsonTablePlanSpec, plan1);
+	n->plan2 = castNode(JsonTablePlanSpec, plan2);
+	n->location = location;
+
+	return (Node *) n;
+}
+
 /*
  * makeJsonKeyValue -
  *	  creates a JsonKeyValue node
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index ff4e1388c55..9e05a314707 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -672,6 +672,14 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 				json_table
 				json_table_column_definition
 				json_table_column_path_clause_opt
+				json_table_plan_clause_opt
+				json_table_plan
+				json_table_plan_simple
+				json_table_plan_outer
+				json_table_plan_inner
+				json_table_plan_union
+				json_table_plan_cross
+				json_table_plan_primary
 %type <list>	json_name_and_value_list
 				json_value_expr_list
 				json_array_aggregate_order_by_clause_opt
@@ -683,6 +691,9 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %type <ival>	json_behavior_type
 				json_predicate_type_constraint
 				json_quotes_clause_opt
+				json_table_default_plan_choices
+				json_table_default_plan_inner_outer
+				json_table_default_plan_union_cross
 				json_wrapper_behavior
 %type <boolean>	json_key_uniqueness_constraint_opt
 				json_object_constructor_null_clause_opt
@@ -15188,6 +15199,7 @@ json_table:
 				json_value_expr ',' a_expr json_table_path_name_opt
 				json_passing_clause_opt
 				COLUMNS '(' json_table_column_definition_list ')'
+				json_table_plan_clause_opt
 				json_on_error_clause_opt
 			')'
 				{
@@ -15199,13 +15211,15 @@ json_table:
 						castNode(A_Const, $5)->val.node.type != T_String)
 						ereport(ERROR,
 								errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-								errmsg("only string constants are supported in JSON_TABLE path specification"),
+								errmsg("only string constants are supported in"
+									   " JSON_TABLE path specification"),
 								parser_errposition(@5));
 					pathstring = castNode(A_Const, $5)->val.sval.sval;
 					n->pathspec = makeJsonTablePathSpec(pathstring, $6, @5, @6);
 					n->passing = $7;
 					n->columns = $10;
-					n->on_error = (JsonBehavior *) $12;
+					n->planspec = (JsonTablePlanSpec *) $12;
+					n->on_error = (JsonBehavior *) $13;
 					n->location = @1;
 					$$ = (Node *) n;
 				}
@@ -15297,8 +15311,7 @@ json_table_column_definition:
 					JsonTableColumn *n = makeNode(JsonTableColumn);
 
 					n->coltype = JTC_NESTED;
-					n->pathspec = (JsonTablePathSpec *)
-						makeJsonTablePathSpec($3, NULL, @3, -1);
+					n->pathspec = makeJsonTablePathSpec($3, NULL, @3, -1);
 					n->columns = $6;
 					n->location = @1;
 					$$ = (Node *) n;
@@ -15309,8 +15322,7 @@ json_table_column_definition:
 					JsonTableColumn *n = makeNode(JsonTableColumn);
 
 					n->coltype = JTC_NESTED;
-					n->pathspec = (JsonTablePathSpec *)
-						makeJsonTablePathSpec($3, $5, @3, @5);
+					n->pathspec = makeJsonTablePathSpec($3, $5, @3, @5);
 					n->columns = $8;
 					n->location = @1;
 					$$ = (Node *) n;
@@ -15329,6 +15341,83 @@ json_table_column_path_clause_opt:
 				{ $$ = NULL; }
 		;
 
+json_table_plan_clause_opt:
+			PLAN '(' json_table_plan ')'
+				{ $$ = $3; }
+			| PLAN DEFAULT '(' json_table_default_plan_choices ')'
+				{ $$ = makeJsonTableDefaultPlan($4, @1); }
+			| /* EMPTY */
+				{ $$ = NULL; }
+		;
+
+json_table_plan:
+			json_table_plan_simple
+			| json_table_plan_outer
+			| json_table_plan_inner
+			| json_table_plan_union
+			| json_table_plan_cross
+		;
+
+json_table_plan_simple:
+			name
+				{ $$ = makeJsonTableSimplePlan($1, @1); }
+		;
+
+json_table_plan_outer:
+			json_table_plan_simple OUTER_P json_table_plan_primary
+				{ $$ = makeJsonTableJoinedPlan(JSTP_JOIN_OUTER, $1, $3, @1); }
+		;
+
+json_table_plan_inner:
+			json_table_plan_simple INNER_P json_table_plan_primary
+				{ $$ = makeJsonTableJoinedPlan(JSTP_JOIN_INNER, $1, $3, @1); }
+		;
+
+json_table_plan_union:
+			json_table_plan_primary UNION json_table_plan_primary
+				{ $$ = makeJsonTableJoinedPlan(JSTP_JOIN_UNION, $1, $3, @1); }
+			| json_table_plan_union UNION json_table_plan_primary
+				{ $$ = makeJsonTableJoinedPlan(JSTP_JOIN_UNION, $1, $3, @1); }
+		;
+
+json_table_plan_cross:
+			json_table_plan_primary CROSS json_table_plan_primary
+				{ $$ = makeJsonTableJoinedPlan(JSTP_JOIN_CROSS, $1, $3, @1); }
+			| json_table_plan_cross CROSS json_table_plan_primary
+				{ $$ = makeJsonTableJoinedPlan(JSTP_JOIN_CROSS, $1, $3, @1); }
+		;
+
+json_table_plan_primary:
+			json_table_plan_simple
+				{ $$ = $1; }
+			| '(' json_table_plan ')'
+				{
+					castNode(JsonTablePlanSpec, $2)->location = @1;
+					$$ = $2;
+				}
+		;
+
+json_table_default_plan_choices:
+			json_table_default_plan_inner_outer
+				{ $$ = $1 | JSTP_JOIN_UNION; }
+			| json_table_default_plan_union_cross
+				{ $$ = $1 | JSTP_JOIN_OUTER; }
+			| json_table_default_plan_inner_outer ',' json_table_default_plan_union_cross
+				{ $$ = $1 | $3; }
+			| json_table_default_plan_union_cross ',' json_table_default_plan_inner_outer
+				{ $$ = $1 | $3; }
+		;
+
+json_table_default_plan_inner_outer:
+			INNER_P						{ $$ = JSTP_JOIN_INNER; }
+			| OUTER_P					{ $$ = JSTP_JOIN_OUTER; }
+		;
+
+json_table_default_plan_union_cross:
+			UNION						{ $$ = JSTP_JOIN_UNION; }
+			| CROSS						{ $$ = JSTP_JOIN_CROSS; }
+		;
+
 /*****************************************************************************
  *
  *	Type syntax
diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c
index 32a1e8629b2..ef042830f02 100644
--- a/src/backend/parser/parse_jsontable.c
+++ b/src/backend/parser/parse_jsontable.c
@@ -39,17 +39,22 @@ typedef struct JsonTableParseContext
 } JsonTableParseContext;
 
 static JsonTablePlan *transformJsonTableColumns(JsonTableParseContext *cxt,
+												JsonTablePlanSpec *planspec,
 												List *columns,
 												List *passingArgs,
 												JsonTablePathSpec *pathspec);
 static JsonTablePlan *transformJsonTableNestedColumns(JsonTableParseContext *cxt,
+													  JsonTablePlanSpec *plan,
 													  List *passingArgs,
 													  List *columns);
 static JsonFuncExpr *transformJsonTableColumn(JsonTableColumn *jtc,
 											  Node *contextItemExpr,
-											  List *passingArgs);
+											  List *passingArgs,
+											  bool errorOnError);
 static bool isCompositeType(Oid typid);
-static JsonTablePlan *makeJsonTablePathScan(JsonTablePathSpec *pathspec,
+static JsonTablePlan *makeJsonTablePathScan(JsonTableParseContext *cxt,
+											JsonTablePathSpec *pathspec,
+											JsonTablePlanSpec *planspec,
 											bool errorOnError,
 											int colMin, int colMax,
 											JsonTablePlan *childplan);
@@ -57,8 +62,14 @@ static void CheckDuplicateColumnOrPathNames(JsonTableParseContext *cxt,
 											List *columns);
 static bool LookupPathOrColumnName(JsonTableParseContext *cxt, char *name);
 static char *generateJsonTablePathName(JsonTableParseContext *cxt);
-static JsonTablePlan *makeJsonTableSiblingJoin(JsonTablePlan *lplan,
+static void validateJsonTableChildPlan(JsonTableParseContext *cxt,
+									   JsonTablePlanSpec *plan,
+									   List *columns);
+static JsonTablePlan *makeJsonTableSiblingJoin(bool cross,
+											   JsonTablePlan *lplan,
 											   JsonTablePlan *rplan);
+static void
+			appendJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingArgs);
 
 /*
  * transformJsonTable -
@@ -76,6 +87,7 @@ transformJsonTable(ParseState *pstate, JsonTable *jt)
 	TableFunc  *tf;
 	JsonFuncExpr *jfe;
 	JsonExpr   *je;
+	JsonTablePlanSpec *plan = jt->planspec;
 	JsonTablePathSpec *rootPathSpec = jt->pathspec;
 	bool		is_lateral;
 	JsonTableParseContext cxt = {pstate};
@@ -94,8 +106,17 @@ transformJsonTable(ParseState *pstate, JsonTable *jt)
 				parser_errposition(pstate, jt->on_error->location));
 
 	cxt.pathNameId = 0;
+
+	/*
+	 * Generate a name for the row pattern path if it was not given one.  Path
+	 * names are optional for every path, including when a PLAN clause is
+	 * present; a specific PLAN() can only reference named paths, so an
+	 * unnamed path that the plan must mention is caught later as a path name
+	 * mismatch or a path not covered by the plan.
+	 */
 	if (rootPathSpec->name == NULL)
 		rootPathSpec->name = generateJsonTablePathName(&cxt);
+
 	cxt.pathNames = list_make1(rootPathSpec->name);
 	CheckDuplicateColumnOrPathNames(&cxt, jt->columns);
 
@@ -135,7 +156,7 @@ transformJsonTable(ParseState *pstate, JsonTable *jt)
 	 */
 	cxt.jt = jt;
 	cxt.tf = tf;
-	tf->plan = (Node *) transformJsonTableColumns(&cxt, jt->columns,
+	tf->plan = (Node *) transformJsonTableColumns(&cxt, plan, jt->columns,
 												  jt->passing,
 												  rootPathSpec);
 
@@ -246,25 +267,110 @@ generateJsonTablePathName(JsonTableParseContext *cxt)
  * their type/collation information to cxt->tf.
  */
 static JsonTablePlan *
-transformJsonTableColumns(JsonTableParseContext *cxt, List *columns,
+transformJsonTableColumns(JsonTableParseContext *cxt,
+						  JsonTablePlanSpec *planspec,
+						  List *columns,
 						  List *passingArgs,
 						  JsonTablePathSpec *pathspec)
 {
-	ParseState *pstate = cxt->pstate;
 	JsonTable  *jt = cxt->jt;
 	TableFunc  *tf = cxt->tf;
-	ListCell   *col;
-	bool		ordinality_found = false;
+	JsonTablePathScan *scan;
+	JsonTablePlanSpec *childPlanSpec;
+	bool		defaultPlan = planspec == NULL ||
+		planspec->plan_type == JSTP_DEFAULT;
 	bool		errorOnError = jt->on_error &&
 		jt->on_error->btype == JSON_BEHAVIOR_ERROR;
-	Oid			contextItemTypid = exprType(tf->docexpr);
 	int			colMin,
 				colMax;
-	JsonTablePlan *childplan;
+	JsonTablePlan *childplan = NULL;
 
 	/* Start of column range */
 	colMin = list_length(tf->colvalexprs);
 
+	if (defaultPlan)
+		childPlanSpec = planspec;
+	else
+	{
+		/* validate parent and child plans */
+		JsonTablePlanSpec *parentPlanSpec;
+
+		if (planspec->plan_type == JSTP_JOINED)
+		{
+			if (planspec->join_type != JSTP_JOIN_INNER &&
+				planspec->join_type != JSTP_JOIN_OUTER)
+				ereport(ERROR,
+						(errcode(ERRCODE_SYNTAX_ERROR),
+						 errmsg("invalid JSON_TABLE plan clause"),
+						 errdetail("Expected INNER or OUTER."),
+						 parser_errposition(cxt->pstate, planspec->location)));
+
+			parentPlanSpec = planspec->plan1;
+			childPlanSpec = planspec->plan2;
+
+			Assert(parentPlanSpec->plan_type != JSTP_JOINED);
+			Assert(parentPlanSpec->pathname);
+		}
+		else
+		{
+			parentPlanSpec = planspec;
+			childPlanSpec = NULL;
+		}
+
+		if (strcmp(parentPlanSpec->pathname, pathspec->name) != 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_SYNTAX_ERROR),
+					 errmsg("invalid JSON_TABLE plan"),
+					 errdetail("PATH name mismatch: expected %s but %s is given.",
+							   pathspec->name, parentPlanSpec->pathname),
+					 parser_errposition(cxt->pstate, planspec->location)));
+
+		validateJsonTableChildPlan(cxt, childPlanSpec, columns);
+	}
+
+	appendJsonTableColumns(cxt, columns, passingArgs);
+
+	/* End of column range. */
+	if (list_length(tf->colvalexprs) == colMin)
+	{
+		/* No columns in this Scan beside the nested ones. */
+		colMax = colMin = -1;
+	}
+	else
+		colMax = list_length(tf->colvalexprs) - 1;
+
+	if (childPlanSpec || defaultPlan)
+	{
+		/* transform recursively nested columns */
+		childplan = transformJsonTableNestedColumns(cxt, childPlanSpec,
+													columns, passingArgs);
+	}
+
+	/* transform only non-nested columns */
+	scan = (JsonTablePathScan *) makeJsonTablePathScan(cxt, pathspec,
+													   planspec,
+													   errorOnError,
+													   colMin,
+													   colMax,
+													   childplan);
+
+	return (JsonTablePlan *) scan;
+}
+
+/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */
+static void
+appendJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingArgs)
+{
+	ListCell   *col;
+	ParseState *pstate = cxt->pstate;
+	JsonTable  *jt = cxt->jt;
+	TableFunc  *tf = cxt->tf;
+	bool		ordinality_found = false;
+	JsonBehavior *on_error = jt->on_error;
+	bool		errorOnError = on_error &&
+		on_error->btype == JSON_BEHAVIOR_ERROR;
+	Oid			contextItemTypid = exprType(tf->docexpr);
+
 	foreach(col, columns)
 	{
 		JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col));
@@ -273,12 +379,9 @@ transformJsonTableColumns(JsonTableParseContext *cxt, List *columns,
 		Oid			typcoll = InvalidOid;
 		Node	   *colexpr;
 
-		if (rawc->coltype != JTC_NESTED)
-		{
-			Assert(rawc->name);
+		if (rawc->name)
 			tf->colnames = lappend(tf->colnames,
 								   makeString(pstrdup(rawc->name)));
-		}
 
 		/*
 		 * Determine the type and typmod for the new column. FOR ORDINALITY
@@ -324,7 +427,7 @@ transformJsonTableColumns(JsonTableParseContext *cxt, List *columns,
 					param->typeMod = -1;
 
 					jfe = transformJsonTableColumn(rawc, (Node *) param,
-												   passingArgs);
+												   passingArgs, errorOnError);
 
 					colexpr = transformExpr(pstate, (Node *) jfe,
 											EXPR_KIND_FROM_FUNCTION);
@@ -349,22 +452,6 @@ transformJsonTableColumns(JsonTableParseContext *cxt, List *columns,
 		tf->colcollations = lappend_oid(tf->colcollations, typcoll);
 		tf->colvalexprs = lappend(tf->colvalexprs, colexpr);
 	}
-
-	/* End of column range. */
-	if (list_length(tf->colvalexprs) == colMin)
-	{
-		/* No columns in this Scan beside the nested ones. */
-		colMax = colMin = -1;
-	}
-	else
-		colMax = list_length(tf->colvalexprs) - 1;
-
-	/* Recursively transform nested columns */
-	childplan = transformJsonTableNestedColumns(cxt, passingArgs, columns);
-
-	/* Create a "parent" scan responsible for all columns handled above. */
-	return makeJsonTablePathScan(pathspec, errorOnError, colMin, colMax,
-								 childplan);
 }
 
 /*
@@ -395,7 +482,7 @@ isCompositeType(Oid typid)
  */
 static JsonFuncExpr *
 transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr,
-						 List *passingArgs)
+						 List *passingArgs, bool errorOnError)
 {
 	Node	   *pathspec;
 	JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr);
@@ -437,6 +524,8 @@ transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr,
 	jfexpr->output->returning->format = jtc->format;
 	jfexpr->on_empty = jtc->on_empty;
 	jfexpr->on_error = jtc->on_error;
+	if (jfexpr->on_error == NULL && errorOnError)
+		jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL, -1);
 	jfexpr->quotes = jtc->quotes;
 	jfexpr->wrapper = jtc->wrapper;
 	jfexpr->location = jtc->location;
@@ -444,57 +533,137 @@ transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr,
 	return jfexpr;
 }
 
+static JsonTableColumn *
+findNestedJsonTableColumn(List *columns, const char *pathname)
+{
+	ListCell   *lc;
+
+	foreach(lc, columns)
+	{
+		JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc));
+
+		if (jtc->coltype == JTC_NESTED &&
+			jtc->pathspec->name &&
+			!strcmp(jtc->pathspec->name, pathname))
+			return jtc;
+	}
+
+	return NULL;
+}
+
 /*
  * Recursively transform nested columns and create child plan(s) that will be
  * used to evaluate their row patterns.
+ *
+ * Default plan is transformed into a cross/union join of its nested columns.
+ * Simple and outer/inner plans are transformed into a JsonTablePlan by
+ * finding and transforming corresponding nested column.
+ * Sibling plans are recursively transformed into a JsonTableSiblingJoin.
  */
 static JsonTablePlan *
 transformJsonTableNestedColumns(JsonTableParseContext *cxt,
-								List *passingArgs,
-								List *columns)
+								JsonTablePlanSpec *planspec,
+								List *columns,
+								List *passingArgs)
 {
-	JsonTablePlan *plan = NULL;
-	ListCell   *lc;
+	JsonTableColumn *jtc = NULL;
 
-	/*
-	 * If there are multiple NESTED COLUMNS clauses in 'columns', their
-	 * respective plans will be combined using a "sibling join" plan, which
-	 * effectively does a UNION of the sets of rows coming from each nested
-	 * plan.
-	 */
-	foreach(lc, columns)
+	if (!planspec || planspec->plan_type == JSTP_DEFAULT)
 	{
-		JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc));
-		JsonTablePlan *nested;
+		/* unspecified or default plan */
+		JsonTablePlan *plan = NULL;
+		ListCell   *lc;
+		bool		cross = planspec && (planspec->join_type & JSTP_JOIN_CROSS);
 
-		if (jtc->coltype != JTC_NESTED)
-			continue;
+		/*
+		 * If there are multiple NESTED COLUMNS clauses in 'columns', their
+		 * respective plans will be combined using a "sibling join" plan,
+		 * which effectively does a UNION of the sets of rows coming from each
+		 * nested plan.
+		 */
+		foreach(lc, columns)
+		{
+			JsonTableColumn *col = castNode(JsonTableColumn, lfirst(lc));
+			JsonTablePlan *nested;
 
-		if (jtc->pathspec->name == NULL)
-			jtc->pathspec->name = generateJsonTablePathName(cxt);
+			if (col->coltype != JTC_NESTED)
+				continue;
+
+			if (col->pathspec->name == NULL)
+			{
+				col->pathspec->name = generateJsonTablePathName(cxt);
+			}
 
-		nested = transformJsonTableColumns(cxt, jtc->columns, passingArgs,
-										   jtc->pathspec);
+			nested = transformJsonTableColumns(cxt, planspec, col->columns,
+											   passingArgs,
+											   col->pathspec);
 
-		if (plan)
-			plan = makeJsonTableSiblingJoin(plan, nested);
+			/* Join nested plan with previous sibling nested plans. */
+			if (plan)
+				plan = makeJsonTableSiblingJoin(cross, plan, nested);
+			else
+				plan = nested;
+		}
+
+		return plan;
+	}
+	else if (planspec->plan_type == JSTP_SIMPLE)
+	{
+		jtc = findNestedJsonTableColumn(columns, planspec->pathname);
+	}
+	else if (planspec->plan_type == JSTP_JOINED)
+	{
+		if (planspec->join_type == JSTP_JOIN_INNER ||
+			planspec->join_type == JSTP_JOIN_OUTER)
+		{
+			Assert(planspec->plan1->plan_type == JSTP_SIMPLE);
+			jtc = findNestedJsonTableColumn(columns, planspec->plan1->pathname);
+		}
 		else
-			plan = nested;
+		{
+			JsonTablePlan *lplan = transformJsonTableNestedColumns(cxt,
+																   planspec->plan1,
+																   columns,
+																   passingArgs);
+			JsonTablePlan *rplan = transformJsonTableNestedColumns(cxt,
+																   planspec->plan2,
+																   columns,
+																   passingArgs);
+
+			return makeJsonTableSiblingJoin(planspec->join_type == JSTP_JOIN_CROSS,
+											lplan, rplan);
+		}
 	}
+	else
+		elog(ERROR, "invalid JSON_TABLE plan type %d", planspec->plan_type);
 
-	return plan;
+	if (!jtc)
+		ereport(ERROR,
+				(errcode(ERRCODE_SYNTAX_ERROR),
+				 errmsg("invalid JSON_TABLE plan clause"),
+				 errdetail("PATH name was %s not found in nested columns list.",
+						   planspec->pathname),
+				 parser_errposition(cxt->pstate, planspec->location)));
+
+	return transformJsonTableColumns(cxt, planspec, jtc->columns,
+									 passingArgs,
+									 jtc->pathspec);
 }
 
 /*
- * Create a JsonTablePlan for given path and ON ERROR behavior.
+ * Create transformed JSON_TABLE parent plan node by appending all non-nested
+ * columns to the TableFunc node and remembering their indices in the
+ * colvalexprs list.
  *
- * colMin and colMin give the range of columns computed by this scan in the
+ * colMin and colMax give the range of columns computed by this scan in the
  * global flat list of column expressions that will be passed to the
  * JSON_TABLE's TableFunc.  Both are -1 when all of columns are nested and
  * thus computed by 'childplan'.
  */
 static JsonTablePlan *
-makeJsonTablePathScan(JsonTablePathSpec *pathspec, bool errorOnError,
+makeJsonTablePathScan(JsonTableParseContext *cxt, JsonTablePathSpec *pathspec,
+					  JsonTablePlanSpec *planspec,
+					  bool errorOnError,
 					  int colMin, int colMax,
 					  JsonTablePlan *childplan)
 {
@@ -518,6 +687,11 @@ makeJsonTablePathScan(JsonTablePathSpec *pathspec, bool errorOnError,
 	scan->colMin = colMin;
 	scan->colMax = colMax;
 
+	if (scan->child)
+		scan->outerJoin = planspec == NULL ||
+			(planspec->join_type & JSTP_JOIN_OUTER);
+	/* else: default plan case, no children found */
+
 	return (JsonTablePlan *) scan;
 }
 
@@ -529,13 +703,103 @@ makeJsonTablePathScan(JsonTablePathSpec *pathspec, bool errorOnError,
  * sets of rows from 'lplan' and 'rplan'.
  */
 static JsonTablePlan *
-makeJsonTableSiblingJoin(JsonTablePlan *lplan, JsonTablePlan *rplan)
+makeJsonTableSiblingJoin(bool cross, JsonTablePlan *lplan, JsonTablePlan *rplan)
 {
 	JsonTableSiblingJoin *join = makeNode(JsonTableSiblingJoin);
 
 	join->plan.type = T_JsonTableSiblingJoin;
 	join->lplan = lplan;
 	join->rplan = rplan;
+	join->cross = cross;
 
 	return (JsonTablePlan *) join;
 }
+
+/* Collect sibling path names from plan to the specified list. */
+static void
+collectSiblingPathsInJsonTablePlan(JsonTablePlanSpec *plan, List **paths)
+{
+	if (plan->plan_type == JSTP_SIMPLE)
+		*paths = lappend(*paths, plan->pathname);
+	else if (plan->plan_type == JSTP_JOINED)
+	{
+		if (plan->join_type == JSTP_JOIN_INNER ||
+			plan->join_type == JSTP_JOIN_OUTER)
+		{
+			Assert(plan->plan1->plan_type == JSTP_SIMPLE);
+			*paths = lappend(*paths, plan->plan1->pathname);
+		}
+		else if (plan->join_type == JSTP_JOIN_CROSS ||
+				 plan->join_type == JSTP_JOIN_UNION)
+		{
+			collectSiblingPathsInJsonTablePlan(plan->plan1, paths);
+			collectSiblingPathsInJsonTablePlan(plan->plan2, paths);
+		}
+		else
+			elog(ERROR, "invalid JSON_TABLE join type %d",
+				 plan->join_type);
+	}
+}
+
+/*
+ * Validate child JSON_TABLE plan by checking that:
+ *  - all nested columns have path names specified
+ *  - all nested columns have corresponding node in the sibling plan
+ *  - plan does not contain duplicate or extra nodes
+ */
+static void
+validateJsonTableChildPlan(JsonTableParseContext *cxt, JsonTablePlanSpec *plan,
+						   List *columns)
+{
+	ParseState *pstate = cxt->pstate;
+	ListCell   *lc1;
+	List	   *siblings = NIL;
+	int			nchildren = 0;
+
+	if (plan)
+		collectSiblingPathsInJsonTablePlan(plan, &siblings);
+
+	foreach(lc1, columns)
+	{
+		JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1));
+
+		if (jtc->coltype == JTC_NESTED)
+		{
+			ListCell   *lc2;
+			bool		found = false;
+
+			/*
+			 * A NESTED path need not be named; generate a name if it was left
+			 * unnamed.  A generated name cannot be referenced by a specific
+			 * PLAN(), so such a path is reported just below as a nested path
+			 * not covered by the plan.
+			 */
+			if (jtc->pathspec->name == NULL)
+				jtc->pathspec->name = generateJsonTablePathName(cxt);
+
+			/* find nested path name in the list of sibling path names */
+			foreach(lc2, siblings)
+			{
+				if ((found = !strcmp(jtc->pathspec->name, lfirst(lc2))))
+					break;
+			}
+
+			if (!found)
+				ereport(ERROR,
+						errcode(ERRCODE_SYNTAX_ERROR),
+						errmsg("invalid JSON_TABLE specification"),
+						errdetail("PLAN clause for nested path %s was not found.",
+								  jtc->pathspec->name),
+						parser_errposition(pstate, jtc->location));
+
+			nchildren++;
+		}
+	}
+
+	if (list_length(siblings) > nchildren)
+		ereport(ERROR,
+				errcode(ERRCODE_SYNTAX_ERROR),
+				errmsg("invalid JSON_TABLE plan clause"),
+				errdetail("PLAN clause contains some extra or duplicate sibling nodes."),
+				parser_errposition(pstate, plan ? plan->location : -1));
+}
diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c
index 9bf8ecdcd0c..b69c87d8587 100644
--- a/src/backend/utils/adt/jsonpath_exec.c
+++ b/src/backend/utils/adt/jsonpath_exec.c
@@ -240,6 +240,14 @@ typedef struct JsonTablePlanState
 
 	/* Parent plan, if this is a nested plan */
 	struct JsonTablePlanState *parent;
+
+	/* Join type */
+	bool		cross;
+	bool		outerJoin;
+	/* Planning control fields */
+	bool		advanceNested;
+	bool		advanceRight;
+	bool		reset;
 } JsonTablePlanState;
 
 /* Random number to identify JsonTableExecContext for sanity checking */
@@ -388,13 +396,13 @@ static JsonTablePlanState *JsonTableInitPlan(JsonTableExecContext *cxt,
 											 MemoryContext mcxt);
 static void JsonTableSetDocument(TableFuncScanState *state, Datum value);
 static void JsonTableResetRowPattern(JsonTablePlanState *planstate, Datum item);
+static void JsonTableRescan(JsonTablePlanState *planstate);
 static bool JsonTableFetchRow(TableFuncScanState *state);
 static Datum JsonTableGetValue(TableFuncScanState *state, int colnum,
 							   Oid typid, int32 typmod, bool *isnull);
 static void JsonTableDestroyOpaque(TableFuncScanState *state);
 static bool JsonTablePlanScanNextRow(JsonTablePlanState *planstate);
 static void JsonTableResetNestedPlan(JsonTablePlanState *planstate);
-static bool JsonTablePlanJoinNextRow(JsonTablePlanState *planstate);
 static bool JsonTablePlanNextRow(JsonTablePlanState *planstate);
 
 const TableFuncRoutine JsonbTableRoutine =
@@ -4514,6 +4522,7 @@ JsonTableInitPlan(JsonTableExecContext *cxt, JsonTablePlan *plan,
 		JsonTablePathScan *scan = (JsonTablePathScan *) plan;
 		int			i;
 
+		planstate->outerJoin = scan->outerJoin;
 		planstate->path = DatumGetJsonPathP(scan->path->value->constvalue);
 		planstate->args = args;
 		planstate->mcxt = AllocSetContextCreate(mcxt, "JsonTableExecContext",
@@ -4533,6 +4542,8 @@ JsonTableInitPlan(JsonTableExecContext *cxt, JsonTablePlan *plan,
 	{
 		JsonTableSiblingJoin *join = (JsonTableSiblingJoin *) plan;
 
+		planstate->cross = join->cross;
+
 		planstate->left = JsonTableInitPlan(cxt, join->lplan, parentstate,
 											args, mcxt);
 		planstate->right = JsonTableInitPlan(cxt, join->rplan, parentstate,
@@ -4587,11 +4598,7 @@ JsonTableResetRowPattern(JsonTablePlanState *planstate, Datum item)
 		JsonValueListClear(&planstate->found);
 	}
 
-	/* Reset plan iterator to the beginning of the item list */
-	JsonValueListInitIterator(&planstate->found, &planstate->iter);
-	planstate->current.value = PointerGetDatum(NULL);
-	planstate->current.isnull = true;
-	planstate->ordinal = 0;
+	JsonTableRescan(planstate);
 }
 
 /*
@@ -4602,16 +4609,93 @@ JsonTableResetRowPattern(JsonTablePlanState *planstate, Datum item)
 static bool
 JsonTablePlanNextRow(JsonTablePlanState *planstate)
 {
-	if (IsA(planstate->plan, JsonTablePathScan))
-		return JsonTablePlanScanNextRow(planstate);
-	else if (IsA(planstate->plan, JsonTableSiblingJoin))
-		return JsonTablePlanJoinNextRow(planstate);
+	if (IsA(planstate->plan, JsonTableSiblingJoin))
+	{
+		if (planstate->advanceRight)
+		{
+			/* fetch next inner row */
+			if (JsonTablePlanNextRow(planstate->right))
+				return true;
+
+			/* inner rows are exhausted */
+			if (planstate->cross)
+				planstate->advanceRight = false;	/* next outer row */
+			else
+				return false;	/* end of scan */
+		}
+
+		while (!planstate->advanceRight)
+		{
+			/* fetch next outer row */
+			bool		more = JsonTablePlanNextRow(planstate->left);
+
+			if (planstate->cross)
+			{
+				if (!more)
+					return false;	/* end of scan */
+
+				JsonTableRescan(planstate->right);
+
+				if (!JsonTablePlanNextRow(planstate->right))
+					continue;	/* next outer row */
+
+				planstate->advanceRight = true; /* next inner row */
+			}
+			else if (!more)
+			{
+				if (!JsonTablePlanNextRow(planstate->right))
+					return false;	/* end of scan */
+
+				planstate->advanceRight = true; /* next inner row */
+			}
+
+			break;
+		}
+	}
 	else
-		elog(ERROR, "invalid JsonTablePlan %d", (int) planstate->plan->type);
+	{
+		/* reset context item if requested */
+		if (planstate->reset)
+		{
+			JsonTablePlanState *parent = planstate->parent;
+
+			Assert(parent != NULL && !parent->current.isnull);
+			JsonTableResetRowPattern(planstate, parent->current.value);
+			planstate->reset = false;
+		}
+
+		if (planstate->advanceNested)
+		{
+			/* fetch next nested row */
+			planstate->advanceNested = JsonTablePlanNextRow(planstate->nested);
+			if (planstate->advanceNested)
+				return true;
+		}
+
+		for (;;)
+		{
+			if (!JsonTablePlanScanNextRow(planstate))
+				return false;
+
+			if (planstate->nested == NULL)
+				break;
+
+			JsonTableResetNestedPlan(planstate->nested);
+			planstate->advanceNested = JsonTablePlanNextRow(planstate->nested);
+
+			if (!planstate->advanceNested && !planstate->outerJoin)
+				continue;
 
-	Assert(false);
-	/* Appease compiler */
-	return false;
+			/*
+			 * We have a row to return: either the nested plan produced one,
+			 * or this is an outer join and we emit the parent row with the
+			 * nested columns set to NULL.
+			 */
+			break;
+		}
+	}
+
+	return true;
 }
 
 /*
@@ -4697,47 +4781,27 @@ JsonTableResetNestedPlan(JsonTablePlanState *planstate)
 	{
 		JsonTablePlanState *parent = planstate->parent;
 
-		if (!parent->current.isnull)
-			JsonTableResetRowPattern(planstate, parent->current.value);
+		planstate->reset = true;
+		planstate->advanceNested = false;
+
+		if (planstate->nested)
+			JsonTableResetNestedPlan(planstate->nested);
 
 		/*
 		 * If this plan itself has a child nested plan, it will be reset when
 		 * the caller calls JsonTablePlanNextRow() on this plan.
 		 */
+		if (!parent->current.isnull)
+			JsonTableResetRowPattern(planstate, parent->current.value);
 	}
 	else if (IsA(planstate->plan, JsonTableSiblingJoin))
 	{
 		JsonTableResetNestedPlan(planstate->left);
 		JsonTableResetNestedPlan(planstate->right);
+		planstate->advanceRight = false;
 	}
 }
 
-/*
- * Fetch the next row from a JsonTableSiblingJoin.
- *
- * This is essentially a UNION between the rows from left and right siblings.
- */
-static bool
-JsonTablePlanJoinNextRow(JsonTablePlanState *planstate)
-{
-
-	/* Fetch row from left sibling. */
-	if (!JsonTablePlanNextRow(planstate->left))
-	{
-		/*
-		 * Left sibling ran out of rows, so start fetching from the right
-		 * sibling.
-		 */
-		if (!JsonTablePlanNextRow(planstate->right))
-		{
-			/* Right sibling ran out of rows too, so there are no more rows. */
-			return false;
-		}
-	}
-
-	return true;
-}
-
 /*
  * JsonTableFetchRow
  *		Prepare the next "current" row for upcoming GetValue calls.
@@ -4802,3 +4866,26 @@ JsonTableGetValue(TableFuncScanState *state, int colnum,
 
 	return result;
 }
+
+/* Recursively reset planstate and its child nodes */
+static void
+JsonTableRescan(JsonTablePlanState *planstate)
+{
+	if (IsA(planstate->plan, JsonTablePathScan))
+	{
+		/* Reset plan iterator to the beginning of the item list */
+		JsonValueListInitIterator(&planstate->found, &planstate->iter);
+		planstate->current.value = PointerGetDatum(NULL);
+		planstate->current.isnull = true;
+		planstate->ordinal = 0;
+
+		if (planstate->nested)
+			JsonTableRescan(planstate->nested);
+	}
+	else if (IsA(planstate->plan, JsonTableSiblingJoin))
+	{
+		JsonTableRescan(planstate->left);
+		JsonTableRescan(planstate->right);
+		planstate->advanceRight = false;
+	}
+}
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..8b725ed1adb 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -12675,6 +12675,89 @@ get_json_table_nested_columns(TableFunc *tf, JsonTablePlan *plan,
 	}
 }
 
+/*
+ * json_table_plan_is_default - does this plan match the implicit default?
+ *
+ * When no PLAN clause is given, JSON_TABLE builds a default plan that joins
+ * every nested path to its parent with OUTER and every set of sibling paths
+ * with UNION (see transformJsonTableColumns()).  Such a plan is fully implied
+ * by the NESTED COLUMNS structure, so we need not (and, to match the input,
+ * should not) print a PLAN clause for it; we only deparse a PLAN clause when
+ * the plan deviates from the default, i.e. uses an INNER or CROSS join
+ * somewhere.  This follows the usual ruleutils convention of omitting a clause
+ * that merely restates the default (cf. get_json_expr_options() for ON
+ * EMPTY/ON ERROR, or the NULLS FIRST/LAST handling in get_rule_orderby()).
+ */
+static bool
+json_table_plan_is_default(JsonTablePlan *plan)
+{
+	if (IsA(plan, JsonTablePathScan))
+	{
+		JsonTablePathScan *scan = castNode(JsonTablePathScan, plan);
+
+		if (scan->child)
+		{
+			if (!scan->outerJoin)
+				return false;	/* INNER is not the default */
+			return json_table_plan_is_default(scan->child);
+		}
+
+		return true;
+	}
+	else
+	{
+		JsonTableSiblingJoin *join = castNode(JsonTableSiblingJoin, plan);
+
+		if (join->cross)
+			return false;		/* CROSS is not the default */
+		return json_table_plan_is_default(join->lplan) &&
+			json_table_plan_is_default(join->rplan);
+	}
+}
+
+/*
+ * get_json_table_plan - Parse back a JSON_TABLE plan
+ */
+static void
+get_json_table_plan(TableFunc *tf, JsonTablePlan *plan, deparse_context *context,
+					bool parenthesize)
+{
+	if (parenthesize)
+		appendStringInfoChar(context->buf, '(');
+
+	if (IsA(plan, JsonTablePathScan))
+	{
+		JsonTablePathScan *s = castNode(JsonTablePathScan, plan);
+
+		appendStringInfoString(context->buf, quote_identifier(s->path->name));
+
+		if (s->child)
+		{
+			appendStringInfoString(context->buf,
+								   s->outerJoin ? " OUTER " : " INNER ");
+			get_json_table_plan(tf, s->child, context,
+								IsA(s->child, JsonTableSiblingJoin));
+		}
+	}
+	else if (IsA(plan, JsonTableSiblingJoin))
+	{
+		JsonTableSiblingJoin *j = (JsonTableSiblingJoin *) plan;
+
+		get_json_table_plan(tf, j->lplan, context,
+							IsA(j->lplan, JsonTableSiblingJoin) ||
+							castNode(JsonTablePathScan, j->lplan)->child);
+
+		appendStringInfoString(context->buf, j->cross ? " CROSS " : " UNION ");
+
+		get_json_table_plan(tf, j->rplan, context,
+							IsA(j->rplan, JsonTableSiblingJoin) ||
+							castNode(JsonTablePathScan, j->rplan)->child);
+	}
+
+	if (parenthesize)
+		appendStringInfoChar(context->buf, ')');
+}
+
 /*
  * get_json_table_columns - Parse back JSON_TABLE columns
  */
@@ -12684,6 +12767,7 @@ get_json_table_columns(TableFunc *tf, JsonTablePathScan *scan,
 					   bool showimplicit)
 {
 	StringInfo	buf = context->buf;
+	JsonExpr   *jexpr = castNode(JsonExpr, tf->docexpr);
 	ListCell   *lc_colname;
 	ListCell   *lc_coltype;
 	ListCell   *lc_coltypmod;
@@ -12763,6 +12847,9 @@ get_json_table_columns(TableFunc *tf, JsonTablePathScan *scan,
 			default_behavior = JSON_BEHAVIOR_NULL;
 		}
 
+		if (jexpr->on_error->btype == JSON_BEHAVIOR_ERROR)
+			default_behavior = JSON_BEHAVIOR_ERROR;
+
 		appendStringInfoString(buf, " PATH ");
 
 		get_json_path_spec(colexpr->path_spec, context, showimplicit);
@@ -12840,6 +12927,18 @@ get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit)
 	get_json_table_columns(tf, castNode(JsonTablePathScan, tf->plan), context,
 						   showimplicit);
 
+	/*
+	 * Deparse a PLAN clause only for a non-default plan; the default plan is
+	 * implied by the NESTED COLUMNS structure (see
+	 * json_table_plan_is_default).
+	 */
+	if (root->child && !json_table_plan_is_default((JsonTablePlan *) root))
+	{
+		appendStringInfoChar(buf, ' ');
+		appendContextKeyword(context, "PLAN ", 0, 0, 0);
+		get_json_table_plan(tf, (JsonTablePlan *) root, context, true);
+	}
+
 	if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY_ARRAY)
 		get_json_behavior(jexpr->on_error, context, "ERROR");
 
diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h
index bf54d39feb0..4c9d2ec7c09 100644
--- a/src/include/nodes/makefuncs.h
+++ b/src/include/nodes/makefuncs.h
@@ -124,5 +124,10 @@ extern JsonTablePath *makeJsonTablePath(Const *pathvalue, char *pathname);
 extern JsonTablePathSpec *makeJsonTablePathSpec(char *string, char *name,
 												int string_location,
 												int name_location);
+extern Node *makeJsonTableDefaultPlan(JsonTablePlanJoinType join_type,
+									  int location);
+extern Node *makeJsonTableSimplePlan(char *pathname, int location);
+extern Node *makeJsonTableJoinedPlan(JsonTablePlanJoinType type,
+									 Node *plan1, Node *plan2, int location);
 
 #endif							/* MAKEFUNC_H */
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index e03556399ab..77f69f94607 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1982,6 +1982,48 @@ typedef struct JsonTablePathSpec
 	ParseLoc	location;		/* location of 'string' */
 } JsonTablePathSpec;
 
+/*
+ * JsonTablePlanType -
+ *		flags for JSON_TABLE plan node types representation
+ */
+typedef enum JsonTablePlanType
+{
+	JSTP_DEFAULT,
+	JSTP_SIMPLE,
+	JSTP_JOINED,
+} JsonTablePlanType;
+
+/*
+ * JsonTablePlanJoinType -
+ *		JSON_TABLE join types for JSTP_JOINED plans
+ */
+typedef enum JsonTablePlanJoinType
+{
+	JSTP_JOIN_INNER = 0x01,
+	JSTP_JOIN_OUTER = 0x02,
+	JSTP_JOIN_CROSS = 0x04,
+	JSTP_JOIN_UNION = 0x08,
+} JsonTablePlanJoinType;
+
+/*
+ * JsonTablePlanSpec -
+ *		untransformed representation of JSON_TABLE's PLAN clause
+ */
+typedef struct JsonTablePlanSpec
+{
+	NodeTag		type;
+
+	JsonTablePlanType plan_type;	/* plan type */
+	JsonTablePlanJoinType join_type;	/* join type (for joined plan only) */
+	char	   *pathname;		/* path name (for simple plan only) */
+
+	/* For joined plans */
+	struct JsonTablePlanSpec *plan1;	/* first joined plan */
+	struct JsonTablePlanSpec *plan2;	/* second joined plan */
+
+	ParseLoc	location;		/* token location, or -1 if unknown */
+} JsonTablePlanSpec;
+
 /*
  * JsonTable -
  *		untransformed representation of JSON_TABLE
@@ -1993,6 +2035,7 @@ typedef struct JsonTable
 	JsonTablePathSpec *pathspec;	/* JSON path specification */
 	List	   *passing;		/* list of PASSING clause arguments, if any */
 	List	   *columns;		/* list of JsonTableColumn */
+	JsonTablePlanSpec *planspec;	/* join plan, if specified */
 	JsonBehavior *on_error;		/* ON ERROR behavior */
 	Alias	   *alias;			/* table alias in FROM clause */
 	bool		lateral;		/* does it have LATERAL prefix? */
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index bb05aeebee4..cacef7d4151 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1926,6 +1926,7 @@ typedef struct JsonTablePathScan
 
 	/* Plan(s) for nested columns, if any. */
 	JsonTablePlan *child;
+	bool		outerJoin;		/* outer or inner join for nested columns? */
 
 	/*
 	 * 0-based index in TableFunc.colvalexprs of the 1st and the last column
@@ -1947,6 +1948,7 @@ typedef struct JsonTableSiblingJoin
 
 	JsonTablePlan *lplan;
 	JsonTablePlan *rplan;
+	bool		cross;
 } JsonTableSiblingJoin;
 
 /* ----------------
diff --git a/src/test/regress/expected/sqljson_jsontable.out b/src/test/regress/expected/sqljson_jsontable.out
index 458c5aaa5b0..e690d79c688 100644
--- a/src/test/regress/expected/sqljson_jsontable.out
+++ b/src/test/regress/expected/sqljson_jsontable.out
@@ -774,6 +774,244 @@ SELECT * FROM JSON_TABLE(
 ERROR:  duplicate JSON_TABLE column or path name: a
 LINE 10:    NESTED PATH '$' AS a
                                ^
+-- JSON_TABLE: nested paths and plans
+-- Path names are not required on the row pattern or the NESTED paths, even
+-- when a PLAN clause is present; a name is generated for any path left
+-- unnamed.  The next two queries succeed.
+-- PLAN DEFAULT with an unnamed row pattern
+SELECT * FROM JSON_TABLE(
+       jsonb '[]', '$'
+       COLUMNS (
+               foo int PATH '$'
+       )
+       PLAN DEFAULT (UNION)
+) jt;
+ foo 
+-----
+    
+(1 row)
+
+-- PLAN DEFAULT with an unnamed NESTED path
+SELECT * FROM JSON_TABLE(
+       jsonb '[]', '$' AS path1
+       COLUMNS (
+               NESTED PATH '$' COLUMNS (
+                       foo int PATH '$'
+               )
+       )
+       PLAN DEFAULT (UNION)
+) jt;
+ foo 
+-----
+    
+(1 row)
+
+-- An unnamed NESTED path under an explicit PLAN() is also accepted, but the
+-- generated name cannot be referenced by the plan, so it fails as a nested
+-- path not covered by the plan.
+SELECT * FROM JSON_TABLE(
+       jsonb '[]', '$' AS path1
+       COLUMNS (
+               NESTED PATH '$' COLUMNS (
+                       foo int PATH '$'
+               )
+       )
+       PLAN (path1)
+) jt;
+ERROR:  invalid JSON_TABLE specification
+LINE 4:                NESTED PATH '$' COLUMNS (
+                       ^
+DETAIL:  PLAN clause for nested path json_table_path_0 was not found.
+-- JSON_TABLE: plan validation
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p1)
+) jt;
+ERROR:  invalid JSON_TABLE plan
+LINE 12:        PLAN (p1)
+                      ^
+DETAIL:  PATH name mismatch: expected p0 but p1 is given.
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0)
+) jt;
+ERROR:  invalid JSON_TABLE specification
+LINE 4:                NESTED PATH '$' AS p1 COLUMNS (
+                       ^
+DETAIL:  PLAN clause for nested path p1 was not found.
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER p3)
+) jt;
+ERROR:  invalid JSON_TABLE specification
+LINE 4:                NESTED PATH '$' AS p1 COLUMNS (
+                       ^
+DETAIL:  PLAN clause for nested path p1 was not found.
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 UNION p1 UNION p11)
+) jt;
+ERROR:  invalid JSON_TABLE plan clause
+LINE 12:        PLAN (p0 UNION p1 UNION p11)
+                      ^
+DETAIL:  Expected INNER or OUTER.
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER (p1 CROSS p13))
+) jt;
+ERROR:  invalid JSON_TABLE specification
+LINE 8:                NESTED PATH '$' AS p2 COLUMNS (
+                       ^
+DETAIL:  PLAN clause for nested path p2 was not found.
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER (p1 CROSS p2))
+) jt;
+ERROR:  invalid JSON_TABLE specification
+LINE 5:                        NESTED PATH '$' AS p11 COLUMNS ( foo ...
+                               ^
+DETAIL:  PLAN clause for nested path p11 was not found.
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER ((p1 UNION p11) CROSS p2))
+) jt;
+ERROR:  invalid JSON_TABLE plan clause
+LINE 12:        PLAN (p0 OUTER ((p1 UNION p11) CROSS p2))
+                               ^
+DETAIL:  PLAN clause contains some extra or duplicate sibling nodes.
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER ((p1 INNER p11) CROSS p2))
+) jt;
+ERROR:  invalid JSON_TABLE specification
+LINE 6:                        NESTED PATH '$' AS p12 COLUMNS ( bar ...
+                               ^
+DETAIL:  PLAN clause for nested path p12 was not found.
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2))
+) jt;
+ERROR:  invalid JSON_TABLE specification
+LINE 9:                        NESTED PATH '$' AS p21 COLUMNS ( baz ...
+                               ^
+DETAIL:  PLAN clause for nested path p21 was not found.
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', 'strict $[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)))
+) jt;
+ bar | foo | baz 
+-----+-----+-----
+(0 rows)
+
+-- Should fail (top-level plan must join the row pattern using INNER or
+-- OUTER, not a bare sibling CROSS)
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', 'strict $[*]'
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))
+) jt;
+ERROR:  invalid JSON_TABLE plan clause
+LINE 12:        PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)...
+                      ^
+DETAIL:  Expected INNER or OUTER.
 -- JSON_TABLE: plan execution
 CREATE TEMP TABLE jsonb_table_test (js jsonb);
 INSERT INTO jsonb_table_test
@@ -813,6 +1051,325 @@ from
  4 | -1 |    2 | 2 |      |   
 (11 rows)
 
+-- default plan (outer, union)
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan default (outer, union)
+       ) jt;
+ n | a  | b | c  
+---+----+---+----
+ 1 |  1 |   |   
+ 2 |  2 | 1 |   
+ 2 |  2 | 2 |   
+ 2 |  2 | 3 |   
+ 2 |  2 |   | 10
+ 2 |  2 |   |   
+ 2 |  2 |   | 20
+ 3 |  3 | 1 |   
+ 3 |  3 | 2 |   
+ 4 | -1 | 1 |   
+ 4 | -1 | 2 |   
+(11 rows)
+
+-- specific plan (p outer (pb union pc))
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan (p outer (pb union pc))
+       ) jt;
+ n | a  | b | c  
+---+----+---+----
+ 1 |  1 |   |   
+ 2 |  2 | 1 |   
+ 2 |  2 | 2 |   
+ 2 |  2 | 3 |   
+ 2 |  2 |   | 10
+ 2 |  2 |   |   
+ 2 |  2 |   | 20
+ 3 |  3 | 1 |   
+ 3 |  3 | 2 |   
+ 4 | -1 | 1 |   
+ 4 | -1 | 2 |   
+(11 rows)
+
+-- specific plan (p outer (pc union pb))
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan (p outer (pc union pb))
+       ) jt;
+ n | a  | c  | b 
+---+----+----+---
+ 1 |  1 |    |  
+ 2 |  2 | 10 |  
+ 2 |  2 |    |  
+ 2 |  2 | 20 |  
+ 2 |  2 |    | 1
+ 2 |  2 |    | 2
+ 2 |  2 |    | 3
+ 3 |  3 |    | 1
+ 3 |  3 |    | 2
+ 4 | -1 |    | 1
+ 4 | -1 |    | 2
+(11 rows)
+
+-- default plan (inner, union)
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan default (inner)
+       ) jt;
+ n | a  | b | c  
+---+----+---+----
+ 2 |  2 | 1 |   
+ 2 |  2 | 2 |   
+ 2 |  2 | 3 |   
+ 2 |  2 |   | 10
+ 2 |  2 |   |   
+ 2 |  2 |   | 20
+ 3 |  3 | 1 |   
+ 3 |  3 | 2 |   
+ 4 | -1 | 1 |   
+ 4 | -1 | 2 |   
+(10 rows)
+
+-- specific plan (p inner (pb union pc))
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan (p inner (pb union pc))
+       ) jt;
+ n | a  | b | c  
+---+----+---+----
+ 2 |  2 | 1 |   
+ 2 |  2 | 2 |   
+ 2 |  2 | 3 |   
+ 2 |  2 |   | 10
+ 2 |  2 |   |   
+ 2 |  2 |   | 20
+ 3 |  3 | 1 |   
+ 3 |  3 | 2 |   
+ 4 | -1 | 1 |   
+ 4 | -1 | 2 |   
+(10 rows)
+
+-- default plan (inner, cross)
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan default (cross, inner)
+       ) jt;
+ n | a | b | c  
+---+---+---+----
+ 2 | 2 | 1 | 10
+ 2 | 2 | 1 |   
+ 2 | 2 | 1 | 20
+ 2 | 2 | 2 | 10
+ 2 | 2 | 2 |   
+ 2 | 2 | 2 | 20
+ 2 | 2 | 3 | 10
+ 2 | 2 | 3 |   
+ 2 | 2 | 3 | 20
+(9 rows)
+
+-- specific plan (p inner (pb cross pc))
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan (p inner (pb cross pc))
+       ) jt;
+ n | a | b | c  
+---+---+---+----
+ 2 | 2 | 1 | 10
+ 2 | 2 | 1 |   
+ 2 | 2 | 1 | 20
+ 2 | 2 | 2 | 10
+ 2 | 2 | 2 |   
+ 2 | 2 | 2 | 20
+ 2 | 2 | 3 | 10
+ 2 | 2 | 3 |   
+ 2 | 2 | 3 | 20
+(9 rows)
+
+-- default plan (outer, cross)
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan default (outer, cross)
+       ) jt;
+ n | a  | b | c  
+---+----+---+----
+ 1 |  1 |   |   
+ 2 |  2 | 1 | 10
+ 2 |  2 | 1 |   
+ 2 |  2 | 1 | 20
+ 2 |  2 | 2 | 10
+ 2 |  2 | 2 |   
+ 2 |  2 | 2 | 20
+ 2 |  2 | 3 | 10
+ 2 |  2 | 3 |   
+ 2 |  2 | 3 | 20
+ 3 |  3 |   |   
+ 4 | -1 |   |   
+(12 rows)
+
+-- specific plan (p outer (pb cross pc))
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan (p outer (pb cross pc))
+       ) jt;
+ n | a  | b | c  
+---+----+---+----
+ 1 |  1 |   |   
+ 2 |  2 | 1 | 10
+ 2 |  2 | 1 |   
+ 2 |  2 | 1 | 20
+ 2 |  2 | 2 | 10
+ 2 |  2 | 2 |   
+ 2 |  2 | 2 | 20
+ 2 |  2 | 3 | 10
+ 2 |  2 | 3 |   
+ 2 |  2 | 3 | 20
+ 3 |  3 |   |   
+ 4 | -1 |   |   
+(12 rows)
+
+select
+       jt.*, b1 + 100 as b
+from
+       json_table (jsonb
+               '[
+                       {"a":  1,  "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]},
+                       {"a":  2,  "b": [10, 20], "c": [1, null, 2]},
+                       {"x": "3", "b": [11, 22, 33, 44]}
+                ]',
+               '$[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on error,
+                       nested path 'strict $.b[*]' as pb columns (
+                               b text format json path '$',
+                               nested path 'strict $[*]' as pb1 columns (
+                                       b1 int path '$'
+                               )
+                       ),
+                       nested path 'strict $.c[*]' as pc columns (
+                               c text format json path '$',
+                               nested path 'strict $[*]' as pc1 columns (
+                                       c1 int path '$'
+                               )
+                       )
+               )
+               --plan default(outer, cross)
+               plan(p outer ((pb inner pb1) cross (pc outer pc1)))
+       ) jt;
+ n | a |      b       | b1  |  c   | c1 |  b  
+---+---+--------------+-----+------+----+-----
+ 1 | 1 | [1, 10]      |   1 | 1    |    | 101
+ 1 | 1 | [1, 10]      |   1 | null |    | 101
+ 1 | 1 | [1, 10]      |   1 | 2    |    | 101
+ 1 | 1 | [1, 10]      |  10 | 1    |    | 110
+ 1 | 1 | [1, 10]      |  10 | null |    | 110
+ 1 | 1 | [1, 10]      |  10 | 2    |    | 110
+ 1 | 1 | [2]          |   2 | 1    |    | 102
+ 1 | 1 | [2]          |   2 | null |    | 102
+ 1 | 1 | [2]          |   2 | 2    |    | 102
+ 1 | 1 | [3, 30, 300] |   3 | 1    |    | 103
+ 1 | 1 | [3, 30, 300] |   3 | null |    | 103
+ 1 | 1 | [3, 30, 300] |   3 | 2    |    | 103
+ 1 | 1 | [3, 30, 300] |  30 | 1    |    | 130
+ 1 | 1 | [3, 30, 300] |  30 | null |    | 130
+ 1 | 1 | [3, 30, 300] |  30 | 2    |    | 130
+ 1 | 1 | [3, 30, 300] | 300 | 1    |    | 400
+ 1 | 1 | [3, 30, 300] | 300 | null |    | 400
+ 1 | 1 | [3, 30, 300] | 300 | 2    |    | 400
+ 2 | 2 |              |     |      |    |    
+ 3 |   |              |     |      |    |    
+(20 rows)
+
 -- PASSING arguments are passed to nested paths and their columns' paths
 SELECT *
 FROM
@@ -876,6 +1433,7 @@ SELECT * FROM
 			)
 		)
 	);
+CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo');
 \sv jsonb_table_view_nested
 CREATE OR REPLACE VIEW public.jsonb_table_view_nested AS
  SELECT id,
@@ -913,7 +1471,102 @@ CREATE OR REPLACE VIEW public.jsonb_table_view_nested AS
                 )
             )
         )
+CREATE OR REPLACE VIEW public.jsonb_table_view AS
+ SELECT id,
+    "int",
+    text,
+    "char(4)",
+    bool,
+    "numeric",
+    domain,
+    js,
+    jb,
+    jst,
+    jsc,
+    jsv,
+    jsb,
+    jsbq,
+    aaa,
+    aaa1,
+    exists1,
+    exists2,
+    exists3,
+    js2,
+    jsb2w,
+    jsb2q,
+    ia,
+    ta,
+    jba,
+    a1,
+    b1,
+    a11,
+    a21,
+    a22
+   FROM JSON_TABLE(
+            'null'::jsonb, '$[*]' AS json_table_path_1
+            PASSING
+                1 + 2 AS a,
+                '"foo"'::json AS "b c"
+            COLUMNS (
+                id FOR ORDINALITY,
+                "int" integer PATH '$',
+                text text PATH '$',
+                "char(4)" character(4) PATH '$',
+                bool boolean PATH '$',
+                "numeric" numeric PATH '$',
+                domain jsonb_test_domain PATH '$',
+                js json PATH '$',
+                jb jsonb PATH '$',
+                jst text FORMAT JSON PATH '$',
+                jsc character(4) FORMAT JSON PATH '$',
+                jsv character varying(4) FORMAT JSON PATH '$',
+                jsb jsonb PATH '$',
+                jsbq jsonb PATH '$' OMIT QUOTES,
+                aaa integer PATH '$."aaa"',
+                aaa1 integer PATH '$."aaa"',
+                exists1 boolean EXISTS PATH '$."aaa"',
+                exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR,
+                exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR,
+                js2 json PATH '$',
+                jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER,
+                jsb2q jsonb PATH '$' OMIT QUOTES,
+                ia integer[] PATH '$',
+                ta text[] PATH '$',
+                jba jsonb[] PATH '$',
+                NESTED PATH '$[1]' AS p1
+                COLUMNS (
+                    a1 integer PATH '$."a1"',
+                    b1 text PATH '$."b1"',
+                    NESTED PATH '$[*]' AS "p1 1"
+                    COLUMNS (
+                        a11 text PATH '$."a11"'
+                    )
+                ),
+                NESTED PATH '$[2]' AS p2
+                COLUMNS (
+                    NESTED PATH '$[*]' AS "p2:1"
+                    COLUMNS (
+                        a21 text PATH '$."a21"'
+                    ),
+                    NESTED PATH '$[*]' AS p22
+                    COLUMNS (
+                        a22 text PATH '$."a22"'
+                    )
+                )
+            )
+            PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))
+        );
+EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view;
+                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      QUERY PLAN                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      
+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Table Function Scan on "json_table"
+   Output: "json_table".id, "json_table"."int", "json_table".text, "json_table"."char(4)", "json_table".bool, "json_table"."numeric", "json_table".domain, "json_table".js, "json_table".jb, "json_table".jst, "json_table".jsc, "json_table".jsv, "json_table".jsb, "json_table".jsbq, "json_table".aaa, "json_table".aaa1, "json_table".exists1, "json_table".exists2, "json_table".exists3, "json_table".js2, "json_table".jsb2w, "json_table".jsb2q, "json_table".ia, "json_table".ta, "json_table".jba, "json_table".a1, "json_table".b1, "json_table".a11, "json_table".a21, "json_table".a22
+   Table Function Call: JSON_TABLE('null'::jsonb, '$[*]' AS json_table_path_1 PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, "int" integer PATH '$', text text PATH '$', "char(4)" character(4) PATH '$', bool boolean PATH '$', "numeric" numeric PATH '$', domain jsonb_test_domain PATH '$', js json PATH '$' WITHOUT WRAPPER KEEP QUOTES, jb jsonb PATH '$' WITHOUT WRAPPER KEEP QUOTES, jst text FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsc character(4) FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsv character varying(4) FORMAT JSON PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsb jsonb PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsbq jsonb PATH '$' WITHOUT WRAPPER OMIT QUOTES, aaa integer PATH '$."aaa"', aaa1 integer PATH '$."aaa"', exists1 boolean EXISTS PATH '$."aaa"', exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR, exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR, js2 json PATH '$' WITHOUT WRAPPER KEEP QUOTES, jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER KEEP QUOTES, jsb2q jsonb PATH '$' WITHOUT WRAPPER OMIT QUOTES, ia integer[] PATH '$' WITHOUT WRAPPER KEEP QUOTES, ta text[] PATH '$' WITHOUT WRAPPER KEEP QUOTES, jba jsonb[] PATH '$' WITHOUT WRAPPER KEEP QUOTES, NESTED PATH '$[1]' AS p1 COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' AS "p1 1" COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' AS p2 COLUMNS ( NESTED PATH '$[*]' AS "p2:1" COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' AS p22 COLUMNS (a22 text PATH '$."a22"'))))
+(3 rows)
+
+DROP VIEW jsonb_table_view;
 DROP VIEW jsonb_table_view_nested;
+DROP DOMAIN jsonb_test_domain;
 CREATE TABLE s (js jsonb);
 INSERT INTO s VALUES
 	('{"a":{"za":[{"z1": [11,2222]},{"z21": [22, 234,2345]},{"z22": [32, 204,145]}]},"c": 3}'),
@@ -1152,7 +1805,7 @@ CREATE OR REPLACE VIEW public.json_table_view9 AS
    FROM JSON_TABLE(
             '"a"'::text, '$' AS json_table_path_0
             COLUMNS (
-                a text PATH '$'
+                a text PATH '$' NULL ON EMPTY
             ) ERROR ON ERROR
         )
 DROP VIEW json_table_view8, json_table_view9;
diff --git a/src/test/regress/sql/sqljson_jsontable.sql b/src/test/regress/sql/sqljson_jsontable.sql
index 154eea79c76..14b75c8cc19 100644
--- a/src/test/regress/sql/sqljson_jsontable.sql
+++ b/src/test/regress/sql/sqljson_jsontable.sql
@@ -376,6 +376,187 @@ SELECT * FROM JSON_TABLE(
 	)
 ) jt;
 
+-- JSON_TABLE: nested paths and plans
+-- Path names are not required on the row pattern or the NESTED paths, even
+-- when a PLAN clause is present; a name is generated for any path left
+-- unnamed.  The next two queries succeed.
+-- PLAN DEFAULT with an unnamed row pattern
+SELECT * FROM JSON_TABLE(
+       jsonb '[]', '$'
+       COLUMNS (
+               foo int PATH '$'
+       )
+       PLAN DEFAULT (UNION)
+) jt;
+-- PLAN DEFAULT with an unnamed NESTED path
+SELECT * FROM JSON_TABLE(
+       jsonb '[]', '$' AS path1
+       COLUMNS (
+               NESTED PATH '$' COLUMNS (
+                       foo int PATH '$'
+               )
+       )
+       PLAN DEFAULT (UNION)
+) jt;
+-- An unnamed NESTED path under an explicit PLAN() is also accepted, but the
+-- generated name cannot be referenced by the plan, so it fails as a nested
+-- path not covered by the plan.
+SELECT * FROM JSON_TABLE(
+       jsonb '[]', '$' AS path1
+       COLUMNS (
+               NESTED PATH '$' COLUMNS (
+                       foo int PATH '$'
+               )
+       )
+       PLAN (path1)
+) jt;
+
+-- JSON_TABLE: plan validation
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p1)
+) jt;
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0)
+) jt;
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER p3)
+) jt;
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 UNION p1 UNION p11)
+) jt;
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER (p1 CROSS p13))
+) jt;
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER (p1 CROSS p2))
+) jt;
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER ((p1 UNION p11) CROSS p2))
+) jt;
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER ((p1 INNER p11) CROSS p2))
+) jt;
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', '$[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS p2))
+) jt;
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', 'strict $[*]' AS p0
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN (p0 OUTER ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21)))
+) jt;
+-- Should fail (top-level plan must join the row pattern using INNER or
+-- OUTER, not a bare sibling CROSS)
+SELECT * FROM JSON_TABLE(
+       jsonb 'null', 'strict $[*]'
+       COLUMNS (
+               NESTED PATH '$' AS p1 COLUMNS (
+                       NESTED PATH '$' AS p11 COLUMNS ( foo int ),
+                       NESTED PATH '$' AS p12 COLUMNS ( bar int )
+               ),
+               NESTED PATH '$' AS p2 COLUMNS (
+                       NESTED PATH '$' AS p21 COLUMNS ( baz int )
+               )
+       )
+       PLAN ((p1 INNER (p12 CROSS p11)) CROSS (p2 INNER p21))
+) jt;
 
 -- JSON_TABLE: plan execution
 
@@ -405,6 +586,170 @@ from
 		)
 	) jt;
 
+-- default plan (outer, union)
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan default (outer, union)
+       ) jt;
+-- specific plan (p outer (pb union pc))
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan (p outer (pb union pc))
+       ) jt;
+-- specific plan (p outer (pc union pb))
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan (p outer (pc union pb))
+       ) jt;
+-- default plan (inner, union)
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan default (inner)
+       ) jt;
+-- specific plan (p inner (pb union pc))
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan (p inner (pb union pc))
+       ) jt;
+-- default plan (inner, cross)
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan default (cross, inner)
+       ) jt;
+-- specific plan (p inner (pb cross pc))
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan (p inner (pb cross pc))
+       ) jt;
+-- default plan (outer, cross)
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan default (outer, cross)
+       ) jt;
+-- specific plan (p outer (pb cross pc))
+select
+       jt.*
+from
+       jsonb_table_test jtt,
+       json_table (
+               jtt.js,'strict $[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on empty,
+                       nested path 'strict $.b[*]' as pb columns ( b int path '$' ),
+                       nested path 'strict $.c[*]' as pc columns ( c int path '$' )
+               )
+               plan (p outer (pb cross pc))
+       ) jt;
+select
+       jt.*, b1 + 100 as b
+from
+       json_table (jsonb
+               '[
+                       {"a":  1,  "b": [[1, 10], [2], [3, 30, 300]], "c": [1, null, 2]},
+                       {"a":  2,  "b": [10, 20], "c": [1, null, 2]},
+                       {"x": "3", "b": [11, 22, 33, 44]}
+                ]',
+               '$[*]' as p
+               columns (
+                       n for ordinality,
+                       a int path 'lax $.a' default -1 on error,
+                       nested path 'strict $.b[*]' as pb columns (
+                               b text format json path '$',
+                               nested path 'strict $[*]' as pb1 columns (
+                                       b1 int path '$'
+                               )
+                       ),
+                       nested path 'strict $.c[*]' as pc columns (
+                               c text format json path '$',
+                               nested path 'strict $[*]' as pc1 columns (
+                                       c1 int path '$'
+                               )
+                       )
+               )
+               --plan default(outer, cross)
+               plan(p outer ((pb inner pb1) cross (pc outer pc1)))
+       ) jt;
 
 -- PASSING arguments are passed to nested paths and their columns' paths
 SELECT *
@@ -450,8 +795,97 @@ SELECT * FROM
 		)
 	);
 
+CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo');
 \sv jsonb_table_view_nested
+CREATE OR REPLACE VIEW public.jsonb_table_view AS
+ SELECT id,
+    "int",
+    text,
+    "char(4)",
+    bool,
+    "numeric",
+    domain,
+    js,
+    jb,
+    jst,
+    jsc,
+    jsv,
+    jsb,
+    jsbq,
+    aaa,
+    aaa1,
+    exists1,
+    exists2,
+    exists3,
+    js2,
+    jsb2w,
+    jsb2q,
+    ia,
+    ta,
+    jba,
+    a1,
+    b1,
+    a11,
+    a21,
+    a22
+   FROM JSON_TABLE(
+            'null'::jsonb, '$[*]' AS json_table_path_1
+            PASSING
+                1 + 2 AS a,
+                '"foo"'::json AS "b c"
+            COLUMNS (
+                id FOR ORDINALITY,
+                "int" integer PATH '$',
+                text text PATH '$',
+                "char(4)" character(4) PATH '$',
+                bool boolean PATH '$',
+                "numeric" numeric PATH '$',
+                domain jsonb_test_domain PATH '$',
+                js json PATH '$',
+                jb jsonb PATH '$',
+                jst text FORMAT JSON PATH '$',
+                jsc character(4) FORMAT JSON PATH '$',
+                jsv character varying(4) FORMAT JSON PATH '$',
+                jsb jsonb PATH '$',
+                jsbq jsonb PATH '$' OMIT QUOTES,
+                aaa integer PATH '$."aaa"',
+                aaa1 integer PATH '$."aaa"',
+                exists1 boolean EXISTS PATH '$."aaa"',
+                exists2 integer EXISTS PATH '$."aaa"' TRUE ON ERROR,
+                exists3 text EXISTS PATH 'strict $."aaa"' UNKNOWN ON ERROR,
+                js2 json PATH '$',
+                jsb2w jsonb PATH '$' WITH UNCONDITIONAL WRAPPER,
+                jsb2q jsonb PATH '$' OMIT QUOTES,
+                ia integer[] PATH '$',
+                ta text[] PATH '$',
+                jba jsonb[] PATH '$',
+                NESTED PATH '$[1]' AS p1
+                COLUMNS (
+                    a1 integer PATH '$."a1"',
+                    b1 text PATH '$."b1"',
+                    NESTED PATH '$[*]' AS "p1 1"
+                    COLUMNS (
+                        a11 text PATH '$."a11"'
+                    )
+                ),
+                NESTED PATH '$[2]' AS p2
+                COLUMNS (
+                    NESTED PATH '$[*]' AS "p2:1"
+                    COLUMNS (
+                        a21 text PATH '$."a21"'
+                    ),
+                    NESTED PATH '$[*]' AS p22
+                    COLUMNS (
+                        a22 text PATH '$."a22"'
+                    )
+                )
+            )
+            PLAN (json_table_path_1 OUTER ((p1 OUTER "p1 1") UNION (p2 OUTER ("p2:1" UNION p22))))
+        );
+EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view;
+DROP VIEW jsonb_table_view;
 DROP VIEW jsonb_table_view_nested;
+DROP DOMAIN jsonb_test_domain;
 
 CREATE TABLE s (js jsonb);
 INSERT INTO s VALUES
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 117e7379f10..887a61fa7f3 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1508,7 +1508,10 @@ JsonTablePathScan
 JsonTablePathSpec
 JsonTablePlan
 JsonTablePlanRowSource
+JsonTablePlanSpec
 JsonTablePlanState
+JsonTablePlanJoinType
+JsonTablePlanType
 JsonTableSiblingJoin
 JsonTokenType
 JsonTransformStringValuesAction
-- 
2.50.1 (Apple Git-155)



view thread (9+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected], [email protected]
  Subject: Re: SQL/JSON json_table plan clause
  In-Reply-To: <CAPpHfdtJB2APBjD7Npa58Cg4P0KqFjNKq_mT4tpNWN2y+5zKoA@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox