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 ( context_item, path_expression AS json_path_name PASSING { value AS varname } , ... COLUMNS ( json_table_column , ... ) { ERROR | EMPTY ARRAY} ON ERROR + + PLAN ( json_table_plan ) | + PLAN DEFAULT ( { INNER | OUTER } , { CROSS | UNION } + | { CROSS | UNION } , { INNER | OUTER } ) + ) @@ -3679,6 +3684,16 @@ where json_table_column is: | name type EXISTS PATH path_expression { ERROR | TRUE | FALSE | UNKNOWN } ON ERROR | NESTED PATH path_expression AS json_path_name COLUMNS ( json_table_column , ... ) + +json_table_plan is: + + json_path_name { OUTER | INNER } json_table_plan_primary + | json_table_plan_primary { UNION json_table_plan_primary } ... + | json_table_plan_primary { CROSS json_table_plan_primary } ... + +json_table_plan_primary is: + + json_path_name | ( json_table_plan ) @@ -3842,6 +3857,11 @@ where json_table_column is: in a single function invocation rather than chaining several JSON_TABLE expressions in an SQL statement. + + + You can use the PLAN clause to define how + to join the columns returned by NESTED PATH clauses. + @@ -3866,9 +3886,127 @@ where json_table_column is: The optional json_path_name serves as an - identifier of the provided path_expression. - The name must be unique and distinct from the column names. + identifier of the provided json_path_specification. + The path name must be unique and distinct from the column names. Each + path name can appear in the PLAN clause only once. + + The SQL/JSON standard requires an explicit path name for every + NESTED PATH when a PLAN or + PLAN DEFAULT clause is present, while the row pattern + path name always remains optional. As an extension, + PostgreSQL does not require any path name: a + name is generated for any path left unnamed. Note, however, that a + specific PLAN (json_table_plan) + can only refer to paths by name, so every path that such a plan must + mention has to be given a name explicitly. + + + + + + + PLAN ( json_table_plan ) + + + + + Defines how to join the data returned by NESTED PATH + clauses to the constructed view. + + + To join columns with parent/child relationship, you can use: + + + + + INNER + + + + + Use INNER JOIN, so that the parent row + is omitted from the output if it does not have any child rows + after joining the data returned by NESTED PATH. + + + + + + + OUTER + + + + + Use LEFT OUTER JOIN, 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 NESTED PATH, with NULL values + inserted into the child columns if the corresponding + values are missing. + + + This is the default option for joining columns with parent/child relationship. + + + + + + To join sibling columns, you can use: + + + + + + UNION + + + + + Generate one row for each value produced by each of the sibling + columns. The columns from the other siblings are set to null. + + + This is the default option for joining sibling columns. + + + + + + + CROSS + + + + + Generate one row for each combination of values from the sibling columns. + + + + + + + + + + + + PLAN DEFAULT ( OUTER | INNER , UNION | CROSS ) + + + + The terms can also be specified in reverse order. The + INNER or OUTER option defines the + joining plan for parent/child columns, while UNION or + CROSS affects joins of sibling columns. This form + of PLAN overrides the default plan for + all columns at once. + + + PLAN DEFAULT is simpler than specifying a complete + PLAN, and is often all that is required to get the desired + output. + @@ -4082,5 +4220,39 @@ COLUMNS ( + + + Find a director that has done films in two different genres: + +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) + + + 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 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 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 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; } -/* - * Recursively transform nested columns and create child plan(s) that will be - * used to evaluate their row patterns. - */ -static JsonTablePlan * -transformJsonTableNestedColumns(JsonTableParseContext *cxt, - List *passingArgs, - List *columns) +static JsonTableColumn * +findNestedJsonTableColumn(List *columns, const char *pathname) { - JsonTablePlan *plan = NULL; ListCell *lc; - /* - * 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 *jtc = castNode(JsonTableColumn, lfirst(lc)); - JsonTablePlan *nested; - if (jtc->coltype != JTC_NESTED) - continue; - - if (jtc->pathspec->name == NULL) - jtc->pathspec->name = generateJsonTablePathName(cxt); - - nested = transformJsonTableColumns(cxt, jtc->columns, passingArgs, - jtc->pathspec); - - if (plan) - plan = makeJsonTableSiblingJoin(plan, nested); - else - plan = nested; + if (jtc->coltype == JTC_NESTED && + jtc->pathspec->name && + !strcmp(jtc->pathspec->name, pathname)) + return jtc; } - return plan; + return NULL; } /* - * Create a JsonTablePlan for given path and ON ERROR behavior. + * Recursively transform nested columns and create child plan(s) that will be + * used to evaluate their row patterns. * - * colMin and colMin give the range of columns computed by this scan in the + * 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, + JsonTablePlanSpec *planspec, + List *columns, + List *passingArgs) +{ + JsonTableColumn *jtc = NULL; + + if (!planspec || planspec->plan_type == JSTP_DEFAULT) + { + /* unspecified or default plan */ + JsonTablePlan *plan = NULL; + ListCell *lc; + bool cross = planspec && (planspec->join_type & JSTP_JOIN_CROSS); + + /* + * 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 (col->coltype != JTC_NESTED) + continue; + + if (col->pathspec->name == NULL) + { + col->pathspec->name = generateJsonTablePathName(cxt); + } + + nested = transformJsonTableColumns(cxt, planspec, col->columns, + passingArgs, + col->pathspec); + + /* 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 + { + 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); + + 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 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 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); - else - elog(ERROR, "invalid JsonTablePlan %d", (int) planstate->plan->type); + if (IsA(planstate->plan, JsonTableSiblingJoin)) + { + if (planstate->advanceRight) + { + /* fetch next inner row */ + if (JsonTablePlanNextRow(planstate->right)) + return true; - Assert(false); - /* Appease compiler */ - return false; + /* 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 + { + /* 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; + + /* + * 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 819631781c0..b3aef797e7d 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -12707,6 +12707,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 */ @@ -12716,6 +12799,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; @@ -12795,6 +12879,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); @@ -12872,6 +12959,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 4d500e7de2d..1530ef8afe0 100644 --- a/src/test/regress/expected/sqljson_jsontable.out +++ b/src/test/regress/expected/sqljson_jsontable.out @@ -790,6 +790,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 @@ -829,6 +1067,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 @@ -892,6 +1449,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, @@ -929,7 +1487,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}'), @@ -1168,7 +1821,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 41824094b96..d71d57e99b2 100644 --- a/src/test/regress/sql/sqljson_jsontable.sql +++ b/src/test/regress/sql/sqljson_jsontable.sql @@ -385,6 +385,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 @@ -414,6 +595,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 * @@ -459,8 +804,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 ffb413ab612..f6e87d04b0e 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