From 074417afc8be747a235729d7f3732c101f520baf Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <andrew@dunslane.net>
Date: Mon, 13 Sep 2021 18:03:14 -0400
Subject: [PATCH 3/4] JSON_TABLE PLAN DEFAULT clause

---
 doc/src/sgml/func.sgml                      | 118 ++++++++++++++++---
 src/backend/nodes/copyfuncs.c               |   4 +
 src/backend/nodes/equalfuncs.c              |   3 +
 src/backend/nodes/outfuncs.c                |   3 +
 src/backend/nodes/readfuncs.c               |   3 +
 src/backend/parser/gram.y                   |  41 ++++++-
 src/backend/parser/parse_jsontable.c        |  13 ++-
 src/backend/utils/adt/jsonpath_exec.c       | 120 +++++++++++++++-----
 src/backend/utils/adt/ruleutils.c           |   9 ++
 src/include/nodes/parsenodes.h              |  13 +++
 src/include/nodes/primnodes.h               |   3 +
 src/include/parser/kwlist.h                 |   1 +
 src/test/regress/expected/jsonb_sqljson.out | 118 +++++++++++++++++++
 src/test/regress/sql/jsonb_sqljson.sql      |  65 +++++++++++
 src/tools/pgindent/typedefs.list            |   1 +
 15 files changed, 462 insertions(+), 53 deletions(-)

diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index e86c8992c3..746f24501a 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -19207,6 +19207,10 @@ FROM
 JSON_TABLE (
   <replaceable>context_item</replaceable>, <replaceable>path_expression</replaceable> <optional> PASSING { <replaceable>value</replaceable> AS <replaceable>varname</replaceable> } <optional>, ...</optional> </optional>
   COLUMNS ( <replaceable class="parameter">json_table_column</replaceable> <optional>, ...</optional> )
+  <optional>
+    PLAN DEFAULT ( { INNER | OUTER } <optional> , { CROSS | UNION } </optional>
+                 | { CROSS | UNION } <optional> , { INNER | OUTER } </optional> )
+  </optional>
 )
 <phrase>
 where <replaceable class="parameter">json_table_column</replaceable> is:
@@ -19268,7 +19272,8 @@ where <replaceable class="parameter">json_table_column</replaceable> is:
       The rows produced by <function>JSON_TABLE</function> are laterally
       joined to the row that generated them, so you do not have to explicitly join
       the constructed view with the original table holding <acronym>JSON</acronym>
-      data.
+      data. Optionally, you can specify how to join the columns returned
+      by <literal>NESTED PATH</literal> using the <literal>PLAN DEFAULT</literal> clause.
      </para>
 
      <para>
@@ -19281,22 +19286,6 @@ where <replaceable class="parameter">json_table_column</replaceable> is:
       the resulting rows are joined to the parent row.
      </para>
 
-     <para>
-      Columns with parent/child relationship are joined using
-      <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>
-      Sibling columns are joined using
-      <literal>FULL OUTER JOIN ON FALSE</literal>, so that both parent and child
-      rows are included into the output, with NULL values inserted
-      into both child and parrent columns for all missing values.
-     </para>
-
     </sect5>
     <sect5>
      <title>Parameters</title>
@@ -19468,6 +19457,10 @@ where <replaceable class="parameter">json_table_column</replaceable> is:
      <function>JSON_TABLE</function> expressions in an SQL statement.
     </para>
 
+    <para>
+     You can use the <literal>PLAN DEFAULT</literal> clause to define how
+     to join the columns returned by <replaceable>NESTED PATH</replaceable> clauses.
+    </para>
     </listitem>
    </varlistentry>
 
@@ -19490,8 +19483,99 @@ where <replaceable class="parameter">json_table_column</replaceable> is:
     </listitem>
    </varlistentry>
 
+   <varlistentry>
+    <term>
+     <literal>PLAN DEFAULT ( <replaceable>option</replaceable> <optional>, ... </optional> )</literal>
+    </term>
+    <listitem>
+     <para>
+      Defines how to join the data returned by <replaceable>NESTED PATH</replaceable>
+      clauses to the constructed view. The <literal>INNER</literal> and
+      <literal>OUTER</literal> options define the joining plan for parent/child
+      columns, while <literal>UNION</literal> and <literal>CROSS</literal>
+      affect the sibling columns. You can override the default plans for all
+      columns at once.
+     </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>
+     Use <literal>FULL OUTER JOIN ON FALSE</literal>, so that both parent and child
+     rows are included into the output, with NULL values inserted
+     into both child and parrent columns for all missing values.
+    </para>
+    <para>
+     This is the default option for joining sibling columns.
+    </para>
+    </listitem>
+   </varlistentry>
+
+   <varlistentry>
+    <term>
+     <literal>CROSS</literal>
+    </term>
+    <listitem>
+
+    <para>
+     Use <literal>CROSS JOIN</literal>, so that the output includes 
+     a row for every possible combination of rows from the left-hand
+     and the right-hand columns.
+    </para>
+    </listitem>
+   </varlistentry>
+
   </variablelist>
 
+    </listitem>
+   </varlistentry>
+  </variablelist>
     </sect5>
 
     <sect5>
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 5e35e57697..ffa8f7e39b 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2677,6 +2677,7 @@ _copyJsonTable(const JsonTable *from)
 	COPY_NODE_FIELD(columns);
 	COPY_NODE_FIELD(on_error);
 	COPY_NODE_FIELD(alias);
+	COPY_SCALAR_FIELD(join_type);
 	COPY_SCALAR_FIELD(location);
 
 	return newnode;
@@ -2715,6 +2716,8 @@ _copyJsonTableParentNode(const JsonTableParentNode *from)
 
 	COPY_NODE_FIELD(path);
 	COPY_NODE_FIELD(child);
+	COPY_SCALAR_FIELD(outerJoin);
+	COPY_SCALAR_FIELD(unionJoin);
 	COPY_SCALAR_FIELD(colMin);
 	COPY_SCALAR_FIELD(colMax);
 
@@ -2731,6 +2734,7 @@ _copyJsonTableSiblingNode(const JsonTableSiblingNode *from)
 
 	COPY_NODE_FIELD(larg);
 	COPY_NODE_FIELD(rarg);
+	COPY_SCALAR_FIELD(cross);
 
 	return newnode;
 }
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index adcb608756..5351be8bb4 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -152,6 +152,8 @@ _equalJsonTableParentNode(const JsonTableParentNode *a, const JsonTableParentNod
 {
 	COMPARE_NODE_FIELD(path);
 	COMPARE_NODE_FIELD(child);
+	COMPARE_SCALAR_FIELD(outerJoin);
+	COMPARE_SCALAR_FIELD(unionJoin);
 	COMPARE_SCALAR_FIELD(colMin);
 	COMPARE_SCALAR_FIELD(colMax);
 
@@ -163,6 +165,7 @@ _equalJsonTableSiblingNode(const JsonTableSiblingNode *a, const JsonTableSibling
 {
 	COMPARE_NODE_FIELD(larg);
 	COMPARE_NODE_FIELD(rarg);
+	COMPARE_SCALAR_FIELD(cross);
 
 	return true;
 }
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 80bb16e504..2a18d6ad66 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -1872,6 +1872,8 @@ _outJsonTableParentNode(StringInfo str, const JsonTableParentNode *node)
 
 	WRITE_NODE_FIELD(path);
 	WRITE_NODE_FIELD(child);
+	WRITE_BOOL_FIELD(outerJoin);
+	WRITE_BOOL_FIELD(unionJoin);
 	WRITE_INT_FIELD(colMin);
 	WRITE_INT_FIELD(colMax);
 }
@@ -1883,6 +1885,7 @@ _outJsonTableSiblingNode(StringInfo str, const JsonTableSiblingNode *node)
 
 	WRITE_NODE_FIELD(larg);
 	WRITE_NODE_FIELD(rarg);
+	WRITE_BOOL_FIELD(cross);
 }
 
 /*****************************************************************************
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index cc173a38ec..b9f12205b3 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -1506,6 +1506,8 @@ _readJsonTableParentNode(void)
 
 	READ_NODE_FIELD(path);
 	READ_NODE_FIELD(child);
+	READ_BOOL_FIELD(outerJoin);
+	READ_BOOL_FIELD(unionJoin);
 	READ_INT_FIELD(colMin);
 	READ_INT_FIELD(colMax);
 
@@ -1519,6 +1521,7 @@ _readJsonTableSiblingNode(void)
 
 	READ_NODE_FIELD(larg);
 	READ_NODE_FIELD(rarg);
+	READ_BOOL_FIELD(cross);
 
 	READ_DONE();
 }
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 01804dab46..f9ab89bf99 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -667,6 +667,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 
 %type <ival>		json_encoding
 					json_encoding_clause_opt
+					json_table_plan_clause_opt
+					json_table_default_plan
+					json_table_default_plan_choices
+					json_table_default_plan_inner_outer
+					json_table_default_plan_union_cross
 					json_wrapper_clause_opt
 					json_wrapper_behavior
 					json_conditional_or_unconditional_opt
@@ -780,7 +785,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ORDER ORDINALITY OTHERS OUT_P OUTER_P
 	OVER OVERLAPS OVERLAY OVERRIDING OWNED OWNER
 
-	PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PATH PLACING PLANS POLICY
+	PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PATH PLACING PLAN PLANS POLICY
 	POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY
 	PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION
 
@@ -15515,13 +15520,15 @@ json_table:
 			JSON_TABLE '('
 				json_api_common_syntax
 				json_table_columns_clause
+				json_table_plan_clause_opt
 				json_table_error_clause_opt
 			')'
 				{
 					JsonTable *n = makeNode(JsonTable);
 					n->common = (JsonCommon *) $3;
 					n->columns = $4;
-					n->on_error = $5;
+					n->join_type = $5;
+					n->on_error = $6;
 					n->location = @1;
 					$$ = (Node *) n;
 				}
@@ -15658,6 +15665,34 @@ path_opt:
 			| /* EMPTY */							{ }
 		;
 
+json_table_plan_clause_opt:
+			json_table_default_plan					{ $$ = $1; }
+			| /* EMPTY */							{ $$ = JSTPJ_OUTER | JSTPJ_UNION; }
+		;
+
+json_table_default_plan:
+			PLAN DEFAULT '(' json_table_default_plan_choices ')' { $$ = $4; }
+		;
+
+json_table_default_plan_choices:
+			json_table_default_plan_inner_outer			{ $$ = $1 | JSTPJ_UNION; }
+			| json_table_default_plan_inner_outer ','
+			  json_table_default_plan_union_cross		{ $$ = $1 | $3; }
+			| json_table_default_plan_union_cross		{ $$ = $1 | JSTPJ_OUTER; }
+			| json_table_default_plan_union_cross ','
+			  json_table_default_plan_inner_outer		{ $$ = $1 | $3; }
+		;
+
+json_table_default_plan_inner_outer:
+			INNER_P										{ $$ = JSTPJ_INNER; }
+			| OUTER_P									{ $$ = JSTPJ_OUTER; }
+		;
+
+json_table_default_plan_union_cross:
+			UNION										{ $$ = JSTPJ_UNION; }
+			| CROSS										{ $$ = JSTPJ_CROSS; }
+		;
+
 json_returning_clause_opt:
 			RETURNING Typename
 				{
@@ -16557,6 +16592,7 @@ unreserved_keyword:
 			| PASSING
 			| PASSWORD
 			| PATH
+			| PLAN
 			| PLANS
 			| POLICY
 			| PRECEDING
@@ -17172,6 +17208,7 @@ bare_label_keyword:
 			| PASSWORD
 			| PATH
 			| PLACING
+			| PLAN
 			| PLANS
 			| POLICY
 			| POSITION
diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c
index 94c96606c1..41fe7659de 100644
--- a/src/backend/parser/parse_jsontable.c
+++ b/src/backend/parser/parse_jsontable.c
@@ -174,12 +174,13 @@ transformNestedJsonTableColumn(JsonTableContext *cxt, JsonTableColumn *jtc)
 }
 
 static Node *
-makeJsonTableSiblingJoin(Node *lnode, Node *rnode)
+makeJsonTableSiblingJoin(bool cross, Node *lnode, Node *rnode)
 {
 	JsonTableSiblingNode *join = makeNode(JsonTableSiblingNode);
 
 	join->larg = lnode;
 	join->rarg = rnode;
+	join->cross = cross;
 
 	return (Node *) join;
 }
@@ -187,7 +188,7 @@ makeJsonTableSiblingJoin(Node *lnode, Node *rnode)
 /*
  * Recursively transform child (nested) JSON_TABLE columns.
  *
- * Child columns are transformed into a binary tree of union-joined
+ * Child columns are transformed into a binary tree of union/cross-joined
  * JsonTableSiblingNodes.
  */
 static Node *
@@ -195,8 +196,9 @@ transformJsonTableChildColumns(JsonTableContext *cxt, List *columns)
 {
 	Node	   *res = NULL;
 	ListCell   *lc;
+	bool		cross = cxt->table->join_type & JSTPJ_CROSS;
 
-	/* transform all nested columns into union join */
+	/* transform all nested columns into union/cros join */
 	foreach(lc, columns)
 	{
 		JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc));
@@ -208,7 +210,7 @@ transformJsonTableChildColumns(JsonTableContext *cxt, List *columns)
 		node = transformNestedJsonTableColumn(cxt, jtc);
 
 		/* join transformed node with previous sibling nodes */
-		res = res ? makeJsonTableSiblingJoin(res, node) : node;
+		res = res ? makeJsonTableSiblingJoin(cross, res, node) : node;
 	}
 
 	return res;
@@ -386,6 +388,9 @@ transformJsonTableColumns(JsonTableContext *cxt, List *columns, char *pathSpec,
 	/* transform recursively nested columns */
 	node->child = transformJsonTableChildColumns(cxt, columns);
 
+	node->outerJoin = cxt->table->join_type & JSTPJ_OUTER;
+	node->unionJoin = cxt->table->join_type & JSTPJ_UNION;
+
 	return node;
 }
 
diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c
index ebfa226bf3..4512c529e7 100644
--- a/src/backend/utils/adt/jsonpath_exec.c
+++ b/src/backend/utils/adt/jsonpath_exec.c
@@ -175,6 +175,7 @@ struct JsonTableScanState
 	Datum		current;
 	int			ordinal;
 	bool		currentIsNull;
+	bool		outerJoin;
 	bool		errorOnError;
 	bool		advanceNested;
 	bool		reset;
@@ -188,6 +189,7 @@ struct JsonTableJoinState
 		{
 			JsonTableJoinState *left;
 			JsonTableJoinState *right;
+			bool		cross;
 			bool		advanceRight;
 		}			join;
 		JsonTableScanState scan;
@@ -3166,6 +3168,7 @@ JsonTableInitScanState(JsonTableContext *cxt, JsonTableScanState *scan,
 	int			i;
 
 	scan->parent = parent;
+	scan->outerJoin = node->outerJoin;
 	scan->errorOnError = node->errorOnError;
 	scan->path = DatumGetJsonPathP(node->path->constvalue);
 	scan->args = args;
@@ -3192,6 +3195,7 @@ JsonTableInitPlanState(JsonTableContext *cxt, Node *plan,
 		JsonTableSiblingNode *join = castNode(JsonTableSiblingNode, plan);
 
 		state->is_join = true;
+		state->u.join.cross = join->cross;
 		state->u.join.left = JsonTableInitPlanState(cxt, join->larg, parent);
 		state->u.join.right = JsonTableInitPlanState(cxt, join->rarg, parent);
 	}
@@ -3328,8 +3332,26 @@ JsonTableSetDocument(TableFuncScanState *state, Datum value)
 	JsonTableResetContextItem(&cxt->root, value);
 }
 
+/* Recursively reset scan and its child nodes */
+static void
+JsonTableRescanRecursive(JsonTableJoinState *state)
+{
+	if (state->is_join)
+	{
+		JsonTableRescanRecursive(state->u.join.left);
+		JsonTableRescanRecursive(state->u.join.right);
+		state->u.join.advanceRight = false;
+	}
+	else
+	{
+		JsonTableRescan(&state->u.scan);
+		if (state->u.scan.nested)
+			JsonTableRescanRecursive(state->u.scan.nested);
+	}
+}
+
 /*
- * Fetch next row from a union joined scan.
+ * Fetch next row from a cross/union joined scan.
  *
  * Returned false at the end of a scan, true otherwise.
  */
@@ -3339,17 +3361,48 @@ JsonTableNextJoinRow(JsonTableJoinState *state)
 	if (!state->is_join)
 		return JsonTableNextRow(&state->u.scan);
 
-	if (!state->u.join.advanceRight)
+	if (state->u.join.advanceRight)
 	{
-		/* fetch next outer row */
-		if (JsonTableNextJoinRow(state->u.join.left))
+		/* fetch next inner row */
+		if (JsonTableNextJoinRow(state->u.join.right))
 			return true;
 
-		state->u.join.advanceRight = true;	/* next inner row */
+		/* inner rows are exhausted */
+		if (state->u.join.cross)
+			state->u.join.advanceRight = false;	/* next outer row */
+		else
+			return false;	/* end of scan */
 	}
 
-	/* fetch next inner row */
-	return JsonTableNextJoinRow(state->u.join.right);
+	while (!state->u.join.advanceRight)
+	{
+		/* fetch next outer row */
+		bool		left = JsonTableNextJoinRow(state->u.join.left);
+
+		if (state->u.join.cross)
+		{
+			if (!left)
+				return false;	/* end of scan */
+
+			JsonTableRescanRecursive(state->u.join.right);
+
+			if (!JsonTableNextJoinRow(state->u.join.right))
+				continue;	/* next outer row */
+
+			state->u.join.advanceRight = true;	/* next inner row */
+		}
+		else if (!left)
+		{
+			if (!JsonTableNextJoinRow(state->u.join.right))
+				return false;	/* end of scan */
+
+			state->u.join.advanceRight = true;	/* next inner row */
+		}
+
+		break;
+	}
+
+	return true;
 }
 
 /* Recursively set 'reset' flag of scan and its child nodes */
@@ -3373,16 +3426,13 @@ JsonTableJoinReset(JsonTableJoinState *state)
 }
 
 /*
- * Fetch next row from a simple scan with outer joined nested subscans.
+ * Fetch next row from a simple scan with outer/inner joined nested subscans.
  *
  * Returned false at the end of a scan, true otherwise.
  */
 static bool
 JsonTableNextRow(JsonTableScanState *scan)
 {
-	JsonbValue *jbv;
-	MemoryContext oldcxt;
-
 	/* reset context item if requested */
 	if (scan->reset)
 	{
@@ -3394,34 +3444,44 @@ JsonTableNextRow(JsonTableScanState *scan)
 	if (scan->advanceNested)
 	{
 		/* fetch next nested row */
-		if (JsonTableNextJoinRow(scan->nested))
-			return true;
+		scan->advanceNested = JsonTableNextJoinRow(scan->nested);
 
-		scan->advanceNested = false;
+		if (scan->advanceNested)
+			return true;
 	}
 
-	/* fetch next row */
-	jbv = JsonValueListNext(&scan->found, &scan->iter);
-
-	if (!jbv)
+	for (;;)
 	{
-		scan->current = PointerGetDatum(NULL);
-		scan->currentIsNull = true;
-		return false;	/* end of scan */
-	}
+		/* fetch next row */
+		JsonbValue *jbv = JsonValueListNext(&scan->found, &scan->iter);
+		MemoryContext oldcxt;
 
-	/* set current row item */
-	oldcxt = MemoryContextSwitchTo(scan->mcxt);
-	scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv));
-	scan->currentIsNull = false;
-	MemoryContextSwitchTo(oldcxt);
+		if (!jbv)
+		{
+			scan->current = PointerGetDatum(NULL);
+			scan->currentIsNull = true;
+			return false;	/* end of scan */
+		}
 
-	scan->ordinal++;
+		/* set current row item */
+		oldcxt = MemoryContextSwitchTo(scan->mcxt);
+		scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv));
+		scan->currentIsNull = false;
+		MemoryContextSwitchTo(oldcxt);
+
+		scan->ordinal++;
+
+		if (!scan->nested)
+			break;
 
-	if (scan->nested)
-	{
 		JsonTableJoinReset(scan->nested);
+
 		scan->advanceNested = JsonTableNextJoinRow(scan->nested);
+
+		if (scan->advanceNested || scan->outerJoin)
+			break;
+
+		/* state->ordinal--; */	/* skip current outer row, reset counter */
 	}
 
 	return true;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index a52337b586..780f6520c6 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -11182,6 +11182,15 @@ get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit)
 
 	get_json_table_columns(tf, root, context, showimplicit);
 
+	if (!root->outerJoin || !root->unionJoin)
+	{
+		appendStringInfoChar(buf, ' ');
+		appendContextKeyword(context, "PLAN DEFAULT", 0, 0, 0);
+		appendStringInfo(buf, "(%s, %s)",
+						 root->outerJoin ? "OUTER" : "INNER",
+						 root->unionJoin ? "UNION" : "CROSS");
+	}
+
 	if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY)
 		get_json_behavior(jexpr->on_error, context, "ERROR");
 
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 55dbdd8107..0de78dd6ad 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -1655,6 +1655,18 @@ typedef struct JsonTableColumn
 	int			location;			/* token location, or -1 if unknown */
 } JsonTableColumn;
 
+/*
+ * JsonTablePlanJoinType -
+ *		flags for JSON_TABLE join types representation
+ */
+typedef enum JsonTablePlanJoinType
+{
+	JSTPJ_INNER = 0x01,
+	JSTPJ_OUTER = 0x02,
+	JSTPJ_CROSS = 0x04,
+	JSTPJ_UNION = 0x08,
+} JsonTablePlanJoinType;
+
 /*
  * JsonTable -
  *		untransformed representation of JSON_TABLE
@@ -1664,6 +1676,7 @@ typedef struct JsonTable
 	NodeTag		type;
 	JsonCommon *common;					/* common JSON path syntax fields */
 	List	   *columns;				/* list of JsonTableColumn */
+	JsonTablePlanJoinType join_type;	/* DEFAULT PLAN join type */
 	JsonBehavior *on_error;				/* ON ERROR behavior, if specified */
 	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 57420a572f..1aa3e01048 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1476,6 +1476,8 @@ typedef struct JsonTableParentNode
 	NodeTag		type;
 	Const	   *path;		/* jsonpath constant */
 	Node	   *child;		/* nested columns, if any */
+	bool		outerJoin;	/* outer or inner join for nested columns? */
+	bool		unionJoin;	/* union or cross join for nested columns? */
 	int			colMin;		/* min column index in the resulting column list */
 	int			colMax;		/* max column index in the resulting column list */
 	bool		errorOnError; /* ERROR/EMPTY ON ERROR behavior */
@@ -1490,6 +1492,7 @@ typedef struct JsonTableSiblingNode
 	NodeTag		type;
 	Node	   *larg;		/* left join node */
 	Node	   *rarg;		/* right join node */
+	bool		cross;		/* cross or union join? */
 } JsonTableSiblingNode;
 
 /* ----------------
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 15f586d455..6065bbae88 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -334,6 +334,7 @@ PG_KEYWORD("passing", PASSING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("password", PASSWORD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("path", PATH, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("placing", PLACING, RESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("plan", PLAN, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("position", POSITION, COL_NAME_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out
index d89c3bef44..0cd8f73c66 100644
--- a/src/test/regress/expected/jsonb_sqljson.out
+++ b/src/test/regress/expected/jsonb_sqljson.out
@@ -1420,6 +1420,124 @@ from
  4 | -1 | 2 |   
 (11 rows)
 
+-- default plan (outer, union)
+select
+	jt.*
+from
+	jsonb_table_test jtt,
+	json_table (
+		jtt.js,'strict $[*]'
+		columns (
+			n for ordinality,
+			a int path 'lax $.a' default -1 on empty,
+			nested path 'strict $.b[*]' columns ( b int path '$' ),
+			nested path 'strict $.c[*]' 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)
+
+-- default plan (inner, union)
+select
+	jt.*
+from
+	jsonb_table_test jtt,
+	json_table (
+		jtt.js,'strict $[*]'
+		columns (
+			n for ordinality,
+			a int path 'lax $.a' default -1 on empty,
+			nested path 'strict $.b[*]' columns ( b int path '$' ),
+			nested path 'strict $.c[*]' 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)
+
+-- default plan (inner, cross)
+select
+	jt.*
+from
+	jsonb_table_test jtt,
+	json_table (
+		jtt.js,'strict $[*]'
+		columns (
+			n for ordinality,
+			a int path 'lax $.a' default -1 on empty,
+			nested path 'strict $.b[*]' columns ( b int path '$' ),
+			nested path 'strict $.c[*]' 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)
+
+-- default plan (outer, cross)
+select
+	jt.*
+from
+	jsonb_table_test jtt,
+	json_table (
+		jtt.js,'strict $[*]'
+		columns (
+			n for ordinality,
+			a int path 'lax $.a' default -1 on empty,
+			nested path 'strict $.b[*]' columns ( b int path '$' ),
+			nested path 'strict $.c[*]' 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)
+
 -- Should succeed (JSON arguments are passed to root and nested paths)
 SELECT *
 FROM
diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql
index 6614dd6e45..cd97ff7f46 100644
--- a/src/test/regress/sql/jsonb_sqljson.sql
+++ b/src/test/regress/sql/jsonb_sqljson.sql
@@ -547,6 +547,71 @@ from
 		)
 	) jt;
 
+-- default plan (outer, union)
+select
+	jt.*
+from
+	jsonb_table_test jtt,
+	json_table (
+		jtt.js,'strict $[*]'
+		columns (
+			n for ordinality,
+			a int path 'lax $.a' default -1 on empty,
+			nested path 'strict $.b[*]' columns ( b int path '$' ),
+			nested path 'strict $.c[*]' columns ( c int path '$' )
+		)
+		plan default (outer, union)
+	) jt;
+
+-- default plan (inner, union)
+select
+	jt.*
+from
+	jsonb_table_test jtt,
+	json_table (
+		jtt.js,'strict $[*]'
+		columns (
+			n for ordinality,
+			a int path 'lax $.a' default -1 on empty,
+			nested path 'strict $.b[*]' columns ( b int path '$' ),
+			nested path 'strict $.c[*]' columns ( c int path '$' )
+		)
+		plan default (inner)
+	) jt;
+
+-- default plan (inner, cross)
+select
+	jt.*
+from
+	jsonb_table_test jtt,
+	json_table (
+		jtt.js,'strict $[*]'
+		columns (
+			n for ordinality,
+			a int path 'lax $.a' default -1 on empty,
+			nested path 'strict $.b[*]' columns ( b int path '$' ),
+			nested path 'strict $.c[*]' columns ( c int path '$' )
+		)
+		plan default (cross, inner)
+	) jt;
+
+-- default plan (outer, cross)
+select
+	jt.*
+from
+	jsonb_table_test jtt,
+	json_table (
+		jtt.js,'strict $[*]'
+		columns (
+			n for ordinality,
+			a int path 'lax $.a' default -1 on empty,
+			nested path 'strict $.b[*]' columns ( b int path '$' ),
+			nested path 'strict $.c[*]' columns ( c int path '$' )
+		)
+		plan default (outer, cross)
+	) jt;
+
+
 -- Should succeed (JSON arguments are passed to root and nested paths)
 SELECT *
 FROM
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 063d639f65..27cf806043 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1237,6 +1237,7 @@ JsonTableColumnType
 JsonTableContext
 JsonTableJoinState
 JsonTableParentNode
+JsonTablePlanJoinType
 JsonTableScanState
 JsonTableSiblingNode
 JsonTokenType
-- 
2.25.4

