diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index cb13227db1..1e5701b8eb 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -3796,7 +3796,13 @@ ExplainTargetRel(Plan *plan, Index rti, ExplainState *es) break; case T_TableFuncScan: Assert(rte->rtekind == RTE_TABLEFUNC); - objectname = "xmltable"; + if (rte->tablefunc) + if (rte->tablefunc->functype == TFT_XMLTABLE) + objectname = "xmltable"; + else /* Must be TFT_JSON_TABLE */ + objectname = "json_table"; + else + objectname = NULL; objecttag = "Table Function Name"; break; case T_ValuesScan: diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index d4d3850ec7..38b94c0276 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -2635,6 +2635,7 @@ ExecInitExprRec(Expr *node, ExprState *state, var->typmod = exprTypmod((Node *) argexpr); var->estate = ExecInitExpr(argexpr, state->parent); var->econtext = NULL; + var->mcxt = NULL; var->evaluated = false; var->value = (Datum) 0; var->isnull = true; diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 7d4253d970..7094e7e3f6 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -4602,6 +4602,7 @@ ExecEvalJsonBehavior(ExprContext *econtext, JsonBehavior *behavior, case JSON_BEHAVIOR_NULL: case JSON_BEHAVIOR_UNKNOWN: + case JSON_BEHAVIOR_EMPTY: *is_null = true; return (Datum) 0; @@ -4694,8 +4695,14 @@ EvalJsonPathVar(void *cxt, char *varName, int varNameLen, if (!var->evaluated) { + MemoryContext oldcxt = var->mcxt ? + MemoryContextSwitchTo(var->mcxt) : NULL; + var->value = ExecEvalExpr(var->estate, var->econtext, &var->isnull); var->evaluated = true; + + if (oldcxt) + MemoryContextSwitchTo(oldcxt); } if (var->isnull) @@ -4843,6 +4850,7 @@ ExecEvalJsonExprSubtrans(JsonFunc func, ExprEvalStep *op, PG_CATCH(); { ErrorData *edata; + int ecategory; /* Save error info in oldcontext */ MemoryContextSwitchTo(oldcontext); @@ -4854,8 +4862,10 @@ ExecEvalJsonExprSubtrans(JsonFunc func, ExprEvalStep *op, MemoryContextSwitchTo(oldcontext); CurrentResourceOwner = oldowner; - if (ERRCODE_TO_CATEGORY(edata->sqlerrcode) != - ERRCODE_DATA_EXCEPTION) + ecategory = ERRCODE_TO_CATEGORY(edata->sqlerrcode); + + if (ecategory != ERRCODE_DATA_EXCEPTION && /* jsonpath and other data errors */ + ecategory != ERRCODE_INTEGRITY_CONSTRAINT_VIOLATION) /* domain errors */ ReThrowError(edata); res = (Datum) 0; @@ -4981,6 +4991,10 @@ ExecEvalJsonExpr(ExprEvalStep *op, ExprContext *econtext, break; } + case JSON_TABLE_OP: + *resnull = false; + return item; + default: elog(ERROR, "unrecognized SQL/JSON expression op %d", jexpr->op); return (Datum) 0; diff --git a/src/backend/executor/nodeTableFuncscan.c b/src/backend/executor/nodeTableFuncscan.c index 0db4ed0c2f..691c3e28ce 100644 --- a/src/backend/executor/nodeTableFuncscan.c +++ b/src/backend/executor/nodeTableFuncscan.c @@ -28,6 +28,7 @@ #include "miscadmin.h" #include "nodes/execnodes.h" #include "utils/builtins.h" +#include "utils/jsonpath.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/xml.h" @@ -161,8 +162,9 @@ ExecInitTableFuncScan(TableFuncScan *node, EState *estate, int eflags) scanstate->ss.ps.qual = ExecInitQual(node->scan.plan.qual, &scanstate->ss.ps); - /* Only XMLTABLE is supported currently */ - scanstate->routine = &XmlTableRoutine; + /* Only XMLTABLE and JSON_TABLE are supported currently */ + scanstate->routine = + tf->functype == TFT_XMLTABLE ? &XmlTableRoutine : &JsonbTableRoutine; scanstate->perTableCxt = AllocSetContextCreate(CurrentMemoryContext, @@ -381,14 +383,17 @@ tfuncInitialize(TableFuncScanState *tstate, ExprContext *econtext, Datum doc) routine->SetNamespace(tstate, ns_name, ns_uri); } - /* Install the row filter expression into the table builder context */ - value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); - if (isnull) - ereport(ERROR, - (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), - errmsg("row filter expression must not be null"))); + if (routine->SetRowFilter) + { + /* Install the row filter expression into the table builder context */ + value = ExecEvalExpr(tstate->rowexpr, econtext, &isnull); + if (isnull) + ereport(ERROR, + (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("row filter expression must not be null"))); - routine->SetRowFilter(tstate, TextDatumGetCString(value)); + routine->SetRowFilter(tstate, TextDatumGetCString(value)); + } /* * Install the column filter expressions into the table builder context. diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c index 11c016495e..1a74122f13 100644 --- a/src/backend/nodes/copyfuncs.c +++ b/src/backend/nodes/copyfuncs.c @@ -1394,6 +1394,7 @@ _copyTableFunc(const TableFunc *from) { TableFunc *newnode = makeNode(TableFunc); + COPY_SCALAR_FIELD(functype); COPY_NODE_FIELD(ns_uris); COPY_NODE_FIELD(ns_names); COPY_NODE_FIELD(docexpr); @@ -1404,7 +1405,9 @@ _copyTableFunc(const TableFunc *from) COPY_NODE_FIELD(colcollations); COPY_NODE_FIELD(colexprs); COPY_NODE_FIELD(coldefexprs); + COPY_NODE_FIELD(colvalexprs); COPY_BITMAPSET_FIELD(notnulls); + COPY_NODE_FIELD(plan); COPY_SCALAR_FIELD(ordinalitycol); COPY_LOCATION_FIELD(location); @@ -2683,6 +2686,76 @@ _copyJsonArgument(const JsonArgument *from) return newnode; } +/* + * _copyJsonTable + */ +static JsonTable * +_copyJsonTable(const JsonTable *from) +{ + JsonTable *newnode = makeNode(JsonTable); + + COPY_NODE_FIELD(common); + COPY_NODE_FIELD(columns); + COPY_NODE_FIELD(on_error); + COPY_NODE_FIELD(alias); + COPY_SCALAR_FIELD(location); + + return newnode; +} + +/* + * _copyJsonTableColumn + */ +static JsonTableColumn * +_copyJsonTableColumn(const JsonTableColumn *from) +{ + JsonTableColumn *newnode = makeNode(JsonTableColumn); + + COPY_SCALAR_FIELD(coltype); + COPY_STRING_FIELD(name); + COPY_NODE_FIELD(typeName); + COPY_STRING_FIELD(pathspec); + COPY_SCALAR_FIELD(format); + COPY_SCALAR_FIELD(wrapper); + COPY_SCALAR_FIELD(omit_quotes); + COPY_NODE_FIELD(columns); + COPY_NODE_FIELD(on_empty); + COPY_NODE_FIELD(on_error); + COPY_SCALAR_FIELD(location); + + return newnode; +} + +/* + * _copyJsonTableParent + */ +static JsonTableParent * +_copyJsonTableParent(const JsonTableParent *from) +{ + JsonTableParent *newnode = makeNode(JsonTableParent); + + COPY_NODE_FIELD(path); + COPY_NODE_FIELD(child); + COPY_SCALAR_FIELD(colMin); + COPY_SCALAR_FIELD(colMax); + + return newnode; +} + +/* + * _copyJsonTableSibling + */ +static JsonTableSibling * +_copyJsonTableSibling(const JsonTableSibling *from) +{ + JsonTableSibling *newnode = makeNode(JsonTableSibling); + + COPY_NODE_FIELD(larg); + COPY_NODE_FIELD(rarg); + + return newnode; +} + /* **************************************************************** * pathnodes.h copy functions * @@ -5850,6 +5923,18 @@ copyObjectImpl(const void *from) case T_JsonItemCoercions: retval = _copyJsonItemCoercions(from); break; + case T_JsonTable: + retval = _copyJsonTable(from); + break; + case T_JsonTableColumn: + retval = _copyJsonTableColumn(from); + break; + case T_JsonTableParent: + retval = _copyJsonTableParent(from); + break; + case T_JsonTableSibling: + retval = _copyJsonTableSibling(from); + break; /* * RELATION NODES diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c index 722dbe6a0d..5c21850c97 100644 --- a/src/backend/nodes/equalfuncs.c +++ b/src/backend/nodes/equalfuncs.c @@ -127,6 +127,7 @@ _equalRangeVar(const RangeVar *a, const RangeVar *b) static bool _equalTableFunc(const TableFunc *a, const TableFunc *b) { + COMPARE_SCALAR_FIELD(functype); COMPARE_NODE_FIELD(ns_uris); COMPARE_NODE_FIELD(ns_names); COMPARE_NODE_FIELD(docexpr); @@ -137,13 +138,65 @@ _equalTableFunc(const TableFunc *a, const TableFunc *b) COMPARE_NODE_FIELD(colcollations); COMPARE_NODE_FIELD(colexprs); COMPARE_NODE_FIELD(coldefexprs); + COMPARE_NODE_FIELD(colvalexprs); COMPARE_BITMAPSET_FIELD(notnulls); + COMPARE_NODE_FIELD(plan); COMPARE_SCALAR_FIELD(ordinalitycol); COMPARE_LOCATION_FIELD(location); return true; } +static bool +_equalJsonTable(const JsonTable *a, const JsonTable *b) +{ + COMPARE_NODE_FIELD(common); + COMPARE_NODE_FIELD(columns); + COMPARE_NODE_FIELD(on_error); + COMPARE_NODE_FIELD(alias); + COMPARE_SCALAR_FIELD(location); + + return true; +} + +static bool +_equalJsonTableColumn(const JsonTableColumn *a, const JsonTableColumn *b) +{ + COMPARE_SCALAR_FIELD(coltype); + COMPARE_STRING_FIELD(name); + COMPARE_NODE_FIELD(typeName); + COMPARE_STRING_FIELD(pathspec); + COMPARE_SCALAR_FIELD(format); + COMPARE_SCALAR_FIELD(wrapper); + COMPARE_SCALAR_FIELD(omit_quotes); + COMPARE_NODE_FIELD(columns); + COMPARE_NODE_FIELD(on_empty); + COMPARE_NODE_FIELD(on_error); + COMPARE_SCALAR_FIELD(location); + + return true; +} + +static bool +_equalJsonTableParent(const JsonTableParent *a, const JsonTableParent *b) +{ + COMPARE_NODE_FIELD(path); + COMPARE_NODE_FIELD(child); + COMPARE_SCALAR_FIELD(colMin); + COMPARE_SCALAR_FIELD(colMax); + + return true; +} + +static bool +_equalJsonTableSibling(const JsonTableSibling *a, const JsonTableSibling *b) +{ + COMPARE_NODE_FIELD(larg); + COMPARE_NODE_FIELD(rarg); + + return true; +} + static bool _equalIntoClause(const IntoClause *a, const IntoClause *b) { @@ -3719,6 +3772,12 @@ equal(const void *a, const void *b) case T_JsonItemCoercions: retval = _equalJsonItemCoercions(a, b); break; + case T_JsonTableParent: + retval = _equalJsonTableParent(a, b); + break; + case T_JsonTableSibling: + retval = _equalJsonTableSibling(a, b); + break; /* * RELATION NODES @@ -4341,6 +4400,12 @@ equal(const void *a, const void *b) case T_JsonArgument: retval = _equalJsonArgument(a, b); break; + case T_JsonTable: + retval = _equalJsonTable(a, b); + break; + case T_JsonTableColumn: + retval = _equalJsonTableColumn(a, b); + break; default: elog(ERROR, "unrecognized node type: %d", diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index a094317bfc..4ae5e5d4dd 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2466,6 +2466,8 @@ expression_tree_walker(Node *node, return true; if (walker(tf->coldefexprs, context)) return true; + if (walker(tf->colvalexprs, context)) + return true; } break; case T_JsonValueExpr: @@ -3513,6 +3515,7 @@ expression_tree_mutator(Node *node, MUTATE(newnode->rowexpr, tf->rowexpr, Node *); MUTATE(newnode->colexprs, tf->colexprs, List *); MUTATE(newnode->coldefexprs, tf->coldefexprs, List *); + MUTATE(newnode->colvalexprs, tf->colvalexprs, List *); return (Node *) newnode; } break; @@ -4530,6 +4533,30 @@ raw_expression_tree_walker(Node *node, return true; } break; + case T_JsonTable: + { + JsonTable *jt = (JsonTable *) node; + + if (walker(jt->common, context)) + return true; + if (walker(jt->columns, context)) + return true; + } + break; + case T_JsonTableColumn: + { + JsonTableColumn *jtc = (JsonTableColumn *) node; + + if (walker(jtc->typeName, context)) + return true; + if (walker(jtc->on_empty, context)) + return true; + if (walker(jtc->on_error, context)) + return true; + if (jtc->coltype == JTC_NESTED && walker(jtc->columns, context)) + return true; + } + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(node)); diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c index 6e39590730..213396f999 100644 --- a/src/backend/nodes/outfuncs.c +++ b/src/backend/nodes/outfuncs.c @@ -1092,6 +1092,7 @@ _outTableFunc(StringInfo str, const TableFunc *node) { WRITE_NODE_TYPE("TABLEFUNC"); + WRITE_ENUM_FIELD(functype, TableFuncType); WRITE_NODE_FIELD(ns_uris); WRITE_NODE_FIELD(ns_names); WRITE_NODE_FIELD(docexpr); @@ -1102,7 +1103,9 @@ _outTableFunc(StringInfo str, const TableFunc *node) WRITE_NODE_FIELD(colcollations); WRITE_NODE_FIELD(colexprs); WRITE_NODE_FIELD(coldefexprs); + WRITE_NODE_FIELD(colvalexprs); WRITE_BITMAPSET_FIELD(notnulls); + WRITE_NODE_FIELD(plan); WRITE_INT_FIELD(ordinalitycol); WRITE_LOCATION_FIELD(location); } @@ -1866,6 +1869,26 @@ _outJsonItemCoercions(StringInfo str, const JsonItemCoercions *node) WRITE_NODE_FIELD(composite); } +static void +_outJsonTableParent(StringInfo str, const JsonTableParent *node) +{ + WRITE_NODE_TYPE("JSONTABPNODE"); + + WRITE_NODE_FIELD(path); + WRITE_NODE_FIELD(child); + WRITE_INT_FIELD(colMin); + WRITE_INT_FIELD(colMax); +} + +static void +_outJsonTableSibling(StringInfo str, const JsonTableSibling *node) +{ + WRITE_NODE_TYPE("JSONTABSNODE"); + + WRITE_NODE_FIELD(larg); + WRITE_NODE_FIELD(rarg); +} + /***************************************************************************** * * Stuff from pathnodes.h. @@ -4714,6 +4737,12 @@ outNode(StringInfo str, const void *obj) case T_JsonItemCoercions: _outJsonItemCoercions(str, obj); break; + case T_JsonTableParent: + _outJsonTableParent(str, obj); + break; + case T_JsonTableSibling: + _outJsonTableSibling(str, obj); + break; default: diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c index c94b2561f0..19e257684c 100644 --- a/src/backend/nodes/readfuncs.c +++ b/src/backend/nodes/readfuncs.c @@ -571,6 +571,7 @@ _readTableFunc(void) { READ_LOCALS(TableFunc); + READ_ENUM_FIELD(functype, TableFuncType); READ_NODE_FIELD(ns_uris); READ_NODE_FIELD(ns_names); READ_NODE_FIELD(docexpr); @@ -581,7 +582,9 @@ _readTableFunc(void) READ_NODE_FIELD(colcollations); READ_NODE_FIELD(colexprs); READ_NODE_FIELD(coldefexprs); + READ_NODE_FIELD(colvalexprs); READ_BITMAPSET_FIELD(notnulls); + READ_NODE_FIELD(plan); READ_INT_FIELD(ordinalitycol); READ_LOCATION_FIELD(location); @@ -1532,6 +1535,30 @@ _readJsonExpr(void) READ_DONE(); } +static JsonTableParent * +_readJsonTableParent(void) +{ + READ_LOCALS(JsonTableParent); + + READ_NODE_FIELD(path); + READ_NODE_FIELD(child); + READ_INT_FIELD(colMin); + READ_INT_FIELD(colMax); + + READ_DONE(); +} + +static JsonTableSibling * +_readJsonTableSibling(void) +{ + READ_LOCALS(JsonTableSibling); + + READ_NODE_FIELD(larg); + READ_NODE_FIELD(rarg); + + READ_DONE(); +} + /* * _readJsonCoercion */ @@ -3194,6 +3221,10 @@ parseNodeString(void) return_value = _readJsonCoercion(); else if (MATCH("JSONITEMCOERCIONS", 17)) return_value = _readJsonItemCoercions(); + else if (MATCH("JSONTABPNODE", 12)) + return_value = _readJsonTableParent(); + else if (MATCH("JSONTABSNODE", 12)) + return_value = _readJsonTableSibling(); else { elog(ERROR, "badly formatted node string \"%.32s\"...", token); diff --git a/src/backend/parser/Makefile b/src/backend/parser/Makefile index 9f1c4022bb..f4c0cc7f10 100644 --- a/src/backend/parser/Makefile +++ b/src/backend/parser/Makefile @@ -23,6 +23,7 @@ OBJS = \ parse_enr.o \ parse_expr.o \ parse_func.o \ + parse_jsontable.o \ parse_merge.o \ parse_node.o \ parse_oper.o \ diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index e5a3c528aa..13fa5bea87 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -676,15 +676,25 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_object_aggregate_constructor json_array_aggregate_constructor json_path_specification + json_table + json_table_column_definition + json_table_ordinality_column_definition + json_table_regular_column_definition + json_table_formatted_column_definition + json_table_exists_column_definition + json_table_nested_columns %type json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt json_arguments json_passing_clause_opt + json_table_columns_clause + json_table_column_definition_list %type json_table_path_name json_as_path_name_clause_opt + json_table_column_path_specification_clause_opt %type json_encoding json_encoding_clause_opt @@ -698,6 +708,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_behavior_true json_behavior_false json_behavior_unknown + json_behavior_empty json_behavior_empty_array json_behavior_empty_object json_behavior_default @@ -705,6 +716,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_query_behavior json_exists_error_behavior json_exists_error_clause_opt + json_table_error_behavior + json_table_error_clause_opt %type json_value_on_behavior_clause_opt json_query_on_behavior_clause_opt @@ -779,7 +792,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG - JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE KEY KEYS KEEP @@ -790,8 +803,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); MAPPING MATCH MATCHED MATERIALIZED MAXVALUE MERGE METHOD MINUTE_P MINVALUE MODE MONTH_P MOVE - NAME_P NAMES NATIONAL NATURAL NCHAR NEW NEXT NFC NFD NFKC NFKD NO NONE - NORMALIZE NORMALIZED + NAME_P NAMES NATIONAL NATURAL NCHAR NESTED NEW NEXT NFC NFD NFKC NFKD NO + NONE NORMALIZE NORMALIZED NOT NOTHING NOTIFY NOTNULL NOWAIT NULL_P NULLIF NULLS_P NUMERIC @@ -799,7 +812,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 PLACING PLANS POLICY + PARALLEL PARSER PARTIAL PARTITION PASSING PASSWORD PATH PLACING PLANS POLICY POSITION PRECEDING PRECISION PRESERVE PREPARE PREPARED PRIMARY PRIOR PRIVILEGES PROCEDURAL PROCEDURE PROCEDURES PROGRAM PUBLICATION @@ -901,7 +914,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %nonassoc UNBOUNDED /* ideally would have same precedence as IDENT */ %nonassoc ERROR_P EMPTY_P DEFAULT ABSENT /* JSON error/empty behavior */ -%nonassoc FALSE_P KEEP OMIT PASSING TRUE_P UNKNOWN UNIQUE JSON +%nonassoc FALSE_P KEEP OMIT PASSING TRUE_P UNKNOWN UNIQUE JSON COLUMNS %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP %left Op OPERATOR /* multi-character ops and user-defined operators */ %left '+' '-' @@ -926,6 +939,10 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); */ %left JOIN CROSS LEFT FULL RIGHT INNER_P NATURAL +%nonassoc json_table_column +%nonassoc NESTED +%left PATH + %nonassoc empty_json_unique %left WITHOUT WITH_LA_UNIQUE @@ -12697,6 +12714,19 @@ table_ref: relation_expr opt_alias_clause $2->alias = $4; $$ = (Node *) $2; } + | json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $1); + jt->alias = $2; + $$ = (Node *) jt; + } + | LATERAL_P json_table opt_alias_clause + { + JsonTable *jt = castNode(JsonTable, $2); + jt->alias = $3; + jt->lateral = true; + $$ = (Node *) jt; + } ; @@ -13248,6 +13278,8 @@ xmltable_column_option_el: { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(true), @1); } | NULL_P { $$ = makeDefElem("is_not_null", (Node *) makeBoolean(false), @1); } + | PATH b_expr + { $$ = makeDefElem("path", $2, @1); } ; xml_namespace_list: @@ -15774,6 +15806,10 @@ json_behavior_unknown: UNKNOWN { $$ = makeJsonBehavior(JSON_BEHAVIOR_UNKNOWN, NULL); } ; +json_behavior_empty: + EMPTY_P { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_OBJECT, NULL); } + ; + json_behavior_empty_array: EMPTY_P ARRAY { $$ = makeJsonBehavior(JSON_BEHAVIOR_EMPTY_ARRAY, NULL); } /* non-standard, for Oracle compatibility only */ @@ -15888,6 +15924,153 @@ json_query_on_behavior_clause_opt: { $$.on_empty = NULL; $$.on_error = NULL; } ; +json_table: + JSON_TABLE '(' + json_api_common_syntax + json_table_columns_clause + json_table_error_clause_opt + ')' + { + JsonTable *n = makeNode(JsonTable); + n->common = (JsonCommon *) $3; + n->columns = $4; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_columns_clause: + COLUMNS '(' json_table_column_definition_list ')' { $$ = $3; } + ; + +json_table_column_definition_list: + json_table_column_definition + { $$ = list_make1($1); } + | json_table_column_definition_list ',' json_table_column_definition + { $$ = lappend($1, $3); } + ; + +json_table_column_definition: + json_table_ordinality_column_definition %prec json_table_column + | json_table_regular_column_definition %prec json_table_column + | json_table_formatted_column_definition %prec json_table_column + | json_table_exists_column_definition %prec json_table_column + | json_table_nested_columns + ; + +json_table_ordinality_column_definition: + ColId FOR ORDINALITY + { + JsonTableColumn *n = makeNode(JsonTableColumn); + n->coltype = JTC_FOR_ORDINALITY; + n->name = $1; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_regular_column_definition: + ColId Typename + json_table_column_path_specification_clause_opt + json_wrapper_clause_opt + json_quotes_clause_opt + json_value_on_behavior_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + n->coltype = JTC_REGULAR; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = $4; /* JSW_NONE */ + n->omit_quotes = $5; /* false */ + n->pathspec = $3; + n->on_empty = $6.on_empty; + n->on_error = $6.on_error; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_exists_column_definition: + ColId Typename + EXISTS json_table_column_path_specification_clause_opt + json_exists_error_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + n->coltype = JTC_EXISTS; + n->name = $1; + n->typeName = $2; + n->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + n->wrapper = JSW_NONE; + n->omit_quotes = false; + n->pathspec = $4; + n->on_empty = NULL; + n->on_error = $5; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_error_behavior: + json_behavior_error + | json_behavior_empty + ; + +json_table_error_clause_opt: + json_table_error_behavior ON ERROR_P { $$ = $1; } + | /* EMPTY */ { $$ = NULL; } + ; + +json_table_column_path_specification_clause_opt: + PATH Sconst { $$ = $2; } + | /* EMPTY */ %prec json_table_column { $$ = NULL; } + ; + +json_table_formatted_column_definition: + ColId Typename FORMAT json_representation + json_table_column_path_specification_clause_opt + json_wrapper_clause_opt + json_quotes_clause_opt + json_query_on_behavior_clause_opt + { + JsonTableColumn *n = makeNode(JsonTableColumn); + n->coltype = JTC_FORMATTED; + n->name = $1; + n->typeName = $2; + n->format = castNode(JsonFormat, $4); + n->pathspec = $5; + n->wrapper = $6; + if (n->wrapper != JSW_NONE && $7 != JS_QUOTES_UNSPEC) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("SQL/JSON QUOTES behavior must not be specified when WITH WRAPPER is used"), + parser_errposition(@7))); + n->omit_quotes = $7 == JS_QUOTES_OMIT; + n->on_empty = $8.on_empty; + n->on_error = $8.on_error; + n->location = @1; + $$ = (Node *) n; + } + ; + +json_table_nested_columns: + NESTED path_opt Sconst json_table_columns_clause + { + JsonTableColumn *n = makeNode(JsonTableColumn); + n->coltype = JTC_NESTED; + n->pathspec = $3; + n->columns = $4; + n->location = @1; + $$ = (Node *) n; + } + ; + +path_opt: + PATH { } + | /* EMPTY */ { } + ; + json_returning_clause_opt: RETURNING Typename { @@ -16733,6 +16916,7 @@ unreserved_keyword: | MOVE | NAME_P | NAMES + | NESTED | NEW | NEXT | NFC @@ -16766,6 +16950,7 @@ unreserved_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLANS | POLICY | PRECEDING @@ -16929,6 +17114,7 @@ col_name_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | LEAST | NATIONAL @@ -17296,6 +17482,7 @@ bare_label_keyword: | JSON_QUERY | JSON_SCALAR | JSON_SERIALIZE + | JSON_TABLE | JSON_VALUE | KEEP | KEY @@ -17335,6 +17522,7 @@ bare_label_keyword: | NATIONAL | NATURAL | NCHAR + | NESTED | NEW | NEXT | NFC @@ -17378,6 +17566,7 @@ bare_label_keyword: | PARTITION | PASSING | PASSWORD + | PATH | PLACING | PLANS | POLICY diff --git a/src/backend/parser/parse_clause.c b/src/backend/parser/parse_clause.c index d8b14ba7cd..dafde68b20 100644 --- a/src/backend/parser/parse_clause.c +++ b/src/backend/parser/parse_clause.c @@ -696,7 +696,9 @@ transformRangeTableFunc(ParseState *pstate, RangeTableFunc *rtf) char **names; int colno; - /* Currently only XMLTABLE is supported */ + /* Currently only XMLTABLE and JSON_TABLE are supported */ + + tf->functype = TFT_XMLTABLE; constructName = "XMLTABLE"; docType = XMLOID; @@ -1100,13 +1102,17 @@ transformFromClauseItem(ParseState *pstate, Node *n, rtr->rtindex = nsitem->p_rtindex; return (Node *) rtr; } - else if (IsA(n, RangeTableFunc)) + else if (IsA(n, RangeTableFunc) || IsA(n, JsonTable)) { /* table function is like a plain relation */ RangeTblRef *rtr; ParseNamespaceItem *nsitem; - nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + if (IsA(n, RangeTableFunc)) + nsitem = transformRangeTableFunc(pstate, (RangeTableFunc *) n); + else + nsitem = transformJsonTable(pstate, (JsonTable *) n); + *top_nsitem = nsitem; *namespace = list_make1(nsitem); rtr = makeNode(RangeTblRef); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 911f355460..b6a2482f23 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4093,7 +4093,7 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) Node *pathspec; JsonFormatType format; - if (func->common->pathname) + if (func->common->pathname && func->op != JSON_TABLE_OP) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("JSON_TABLE path name is not allowed here"), @@ -4131,14 +4131,19 @@ transformJsonExprCommon(ParseState *pstate, JsonFuncExpr *func) transformJsonPassingArgs(pstate, format, func->common->passing, &jsexpr->passing_values, &jsexpr->passing_names); - if (func->op != JSON_EXISTS_OP) + if (func->op != JSON_EXISTS_OP && func->op != JSON_TABLE_OP) jsexpr->on_empty = transformJsonBehavior(pstate, func->on_empty, JSON_BEHAVIOR_NULL); - jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, - func->op == JSON_EXISTS_OP ? - JSON_BEHAVIOR_FALSE : - JSON_BEHAVIOR_NULL); + if (func->op == JSON_EXISTS_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_FALSE); + else if (func->op == JSON_TABLE_OP) + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_EMPTY); + else + jsexpr->on_error = transformJsonBehavior(pstate, func->on_error, + JSON_BEHAVIOR_NULL); return jsexpr; } @@ -4439,6 +4444,21 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->result_coercion->expr = NULL; } break; + + case JSON_TABLE_OP: + jsexpr->returning = makeNode(JsonReturning); + jsexpr->returning->format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + jsexpr->returning->typid = exprType(contextItemExpr); + jsexpr->returning->typmod = -1; + + if (jsexpr->returning->typid != JSONBOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TABLE() is not yet implemented for json type"), + errhint("Try casting the argument to jsonb"), + parser_errposition(pstate, func->location))); + + break; } if (exprType(contextItemExpr) != JSONBOID) diff --git a/src/backend/parser/parse_jsontable.c b/src/backend/parser/parse_jsontable.c new file mode 100644 index 0000000000..dd75a40bf6 --- /dev/null +++ b/src/backend/parser/parse_jsontable.c @@ -0,0 +1,466 @@ +/*------------------------------------------------------------------------- + * + * parse_jsontable.c + * parsing of JSON_TABLE + * + * Portions Copyright (c) 1996-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/parser/parse_jsontable.c + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "miscadmin.h" +#include "nodes/makefuncs.h" +#include "nodes/nodeFuncs.h" +#include "optimizer/optimizer.h" +#include "parser/parse_clause.h" +#include "parser/parse_collate.h" +#include "parser/parse_expr.h" +#include "parser/parse_relation.h" +#include "parser/parse_type.h" +#include "utils/builtins.h" +#include "utils/json.h" +#include "utils/lsyscache.h" + +/* Context for JSON_TABLE transformation */ +typedef struct JsonTableContext +{ + ParseState *pstate; /* parsing state */ + JsonTable *table; /* untransformed node */ + TableFunc *tablefunc; /* transformed node */ + List *pathNames; /* list of all path and columns names */ + Oid contextItemTypid; /* type oid of context item (json/jsonb) */ +} JsonTableContext; + +static JsonTableParent * transformJsonTableColumns(JsonTableContext *cxt, + List *columns, + char *pathSpec, + int location); + +static Node * +makeStringConst(char *str, int location) +{ + A_Const *n = makeNode(A_Const); + + n->val.node.type = T_String; + n->val.sval.sval = str; + n->location = location; + + return (Node *)n; +} + +/* + * Transform JSON_TABLE column + * - regular column into JSON_VALUE() + * - FORMAT JSON column into JSON_QUERY() + * - EXISTS column into JSON_EXISTS() + */ +static Node * +transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, + List *passingArgs, bool errorOnError) +{ + JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); + JsonCommon *common = makeNode(JsonCommon); + JsonOutput *output = makeNode(JsonOutput); + JsonPathSpec pathspec; + JsonFormat *default_format; + + jfexpr->op = + jtc->coltype == JTC_REGULAR ? JSON_VALUE_OP : + jtc->coltype == JTC_EXISTS ? JSON_EXISTS_OP : JSON_QUERY_OP; + jfexpr->common = common; + jfexpr->output = output; + jfexpr->on_empty = jtc->on_empty; + jfexpr->on_error = jtc->on_error; + if (!jfexpr->on_error && errorOnError) + jfexpr->on_error = makeJsonBehavior(JSON_BEHAVIOR_ERROR, NULL); + jfexpr->omit_quotes = jtc->omit_quotes; + jfexpr->wrapper = jtc->wrapper; + jfexpr->location = jtc->location; + + output->typeName = jtc->typeName; + output->returning = makeNode(JsonReturning); + output->returning->format = jtc->format; + + default_format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1); + + common->pathname = NULL; + common->expr = makeJsonValueExpr((Expr *) contextItemExpr, default_format); + common->passing = passingArgs; + + if (jtc->pathspec) + pathspec = jtc->pathspec; + else + { + /* Construct default path as '$."column_name"' */ + StringInfoData path; + + initStringInfo(&path); + + appendStringInfoString(&path, "$."); + escape_json(&path, jtc->name); + + pathspec = path.data; + } + + common->pathspec = makeStringConst(pathspec, -1); + + return (Node *) jfexpr; +} + +static bool +isJsonTablePathNameDuplicate(JsonTableContext *cxt, const char *pathname) +{ + ListCell *lc; + + foreach(lc, cxt->pathNames) + { + if (!strcmp(pathname, (const char *) lfirst(lc))) + return true; + } + + return false; +} + +/* Register the column name in the path name list. */ +static void +registerJsonTableColumn(JsonTableContext *cxt, char *colname) +{ + if (isJsonTablePathNameDuplicate(cxt, colname)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_ALIAS), + errmsg("duplicate JSON_TABLE column name: %s", colname), + errhint("JSON_TABLE column names must be distinct from one another"))); + + cxt->pathNames = lappend(cxt->pathNames, colname); +} + +/* Recursively register all nested column names in the path name list. */ +static void +registerAllJsonTableColumns(JsonTableContext *cxt, List *columns) +{ + ListCell *lc; + + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + + if (jtc->coltype == JTC_NESTED) + registerAllJsonTableColumns(cxt, jtc->columns); + else + registerJsonTableColumn(cxt, jtc->name); + } +} + +static Node * +transformNestedJsonTableColumn(JsonTableContext *cxt, JsonTableColumn *jtc) +{ + JsonTableParent *node; + + node = transformJsonTableColumns(cxt, jtc->columns, jtc->pathspec, + jtc->location); + + return (Node *) node; +} + +static Node * +makeJsonTableSiblingJoin(Node *lnode, Node *rnode) +{ + JsonTableSibling *join = makeNode(JsonTableSibling); + + join->larg = lnode; + join->rarg = rnode; + + return (Node *) join; +} + +/* + * Recursively transform child (nested) JSON_TABLE columns. + * + * Child columns are transformed into a binary tree of union-joined + * JsonTableSiblings. + */ +static Node * +transformJsonTableChildColumns(JsonTableContext *cxt, List *columns) +{ + Node *res = NULL; + ListCell *lc; + + /* transform all nested columns into union join */ + foreach(lc, columns) + { + JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); + Node *node; + + if (jtc->coltype != JTC_NESTED) + continue; + + node = transformNestedJsonTableColumn(cxt, jtc); + + /* join transformed node with previous sibling nodes */ + res = res ? makeJsonTableSiblingJoin(res, node) : node; + } + + return res; +} + +/* Check whether type is json/jsonb, array, or record. */ +static bool +typeIsComposite(Oid typid) +{ + char typtype; + + if (typid == JSONOID || + typid == JSONBOID || + typid == RECORDOID || + type_is_array(typid)) + return true; + + typtype = get_typtype(typid); + + if (typtype == TYPTYPE_COMPOSITE) + return true; + + if (typtype == TYPTYPE_DOMAIN) + return typeIsComposite(getBaseType(typid)); + + return false; +} + +/* Append transformed non-nested JSON_TABLE columns to the TableFunc node */ +static void +appendJsonTableColumns(JsonTableContext *cxt, List *columns) +{ + ListCell *col; + ParseState *pstate = cxt->pstate; + JsonTable *jt = cxt->table; + TableFunc *tf = cxt->tablefunc; + bool errorOnError = jt->on_error && + jt->on_error->btype == JSON_BEHAVIOR_ERROR; + + foreach(col, columns) + { + JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); + Oid typid; + int32 typmod; + Node *colexpr; + + if (rawc->name) + { + /* make sure column names are unique */ + ListCell *colname; + + foreach(colname, tf->colnames) + if (!strcmp((const char *) colname, rawc->name)) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("column name \"%s\" is not unique", + rawc->name), + parser_errposition(pstate, rawc->location))); + + tf->colnames = lappend(tf->colnames, + makeString(pstrdup(rawc->name))); + } + + /* + * Determine the type and typmod for the new column. FOR + * ORDINALITY columns are INTEGER by standard; the others are + * user-specified. + */ + switch (rawc->coltype) + { + case JTC_FOR_ORDINALITY: + colexpr = NULL; + typid = INT4OID; + typmod = -1; + break; + + case JTC_REGULAR: + typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); + + /* + * Use implicit FORMAT JSON for composite types (arrays and + * records) + */ + if (typeIsComposite(typid)) + rawc->coltype = JTC_FORMATTED; + else if (rawc->wrapper != JSW_NONE) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use WITH WRAPPER clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + else if (rawc->omit_quotes) + ereport(ERROR, + (errcode(ERRCODE_SYNTAX_ERROR), + errmsg("cannot use OMIT QUOTES clause with scalar columns"), + parser_errposition(pstate, rawc->location))); + + /* FALLTHROUGH */ + case JTC_EXISTS: + case JTC_FORMATTED: + { + Node *je; + CaseTestExpr *param = makeNode(CaseTestExpr); + + param->collation = InvalidOid; + param->typeId = cxt->contextItemTypid; + param->typeMod = -1; + + je = transformJsonTableColumn(rawc, (Node *) param, + NIL, errorOnError); + + colexpr = transformExpr(pstate, je, EXPR_KIND_FROM_FUNCTION); + assign_expr_collations(pstate, colexpr); + + typid = exprType(colexpr); + typmod = exprTypmod(colexpr); + break; + } + + case JTC_NESTED: + continue; + + default: + elog(ERROR, "unknown JSON_TABLE column type: %d", rawc->coltype); + break; + } + + tf->coltypes = lappend_oid(tf->coltypes, typid); + tf->coltypmods = lappend_int(tf->coltypmods, typmod); + tf->colcollations = lappend_oid(tf->colcollations, + type_is_collatable(typid) + ? DEFAULT_COLLATION_OID + : InvalidOid); + tf->colvalexprs = lappend(tf->colvalexprs, colexpr); + } +} + +/* + * 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. + */ +static JsonTableParent * +makeParentJsonTableNode(JsonTableContext *cxt, char *pathSpec, List *columns) +{ + JsonTableParent *node = makeNode(JsonTableParent); + + node->path = makeConst(JSONPATHOID, -1, InvalidOid, -1, + DirectFunctionCall1(jsonpath_in, + CStringGetDatum(pathSpec)), + false, false); + + /* save start of column range */ + node->colMin = list_length(cxt->tablefunc->colvalexprs); + + appendJsonTableColumns(cxt, columns); + + /* save end of column range */ + node->colMax = list_length(cxt->tablefunc->colvalexprs) - 1; + + node->errorOnError = + cxt->table->on_error && + cxt->table->on_error->btype == JSON_BEHAVIOR_ERROR; + + return node; +} + +static JsonTableParent * +transformJsonTableColumns(JsonTableContext *cxt, List *columns, char *pathSpec, + int location) +{ + JsonTableParent *node; + + /* transform only non-nested columns */ + node = makeParentJsonTableNode(cxt, pathSpec, columns); + + /* transform recursively nested columns */ + node->child = transformJsonTableChildColumns(cxt, columns); + + return node; +} + +/* + * transformJsonTable - + * Transform a raw JsonTable into TableFunc. + * + * Transform the document-generating expression, the row-generating expression, + * the column-generating expressions, and the default value expressions. + */ +ParseNamespaceItem * +transformJsonTable(ParseState *pstate, JsonTable *jt) +{ + JsonTableContext cxt; + TableFunc *tf = makeNode(TableFunc); + JsonFuncExpr *jfe = makeNode(JsonFuncExpr); + JsonCommon *jscommon; + char *rootPath; + bool is_lateral; + + cxt.pstate = pstate; + cxt.table = jt; + cxt.tablefunc = tf; + cxt.pathNames = NIL; + + registerAllJsonTableColumns(&cxt, jt->columns); + + jscommon = copyObject(jt->common); + jscommon->pathspec = makeStringConst(pstrdup("$"), -1); + + jfe->op = JSON_TABLE_OP; + jfe->common = jscommon; + jfe->on_error = jt->on_error; + jfe->location = jt->common->location; + + /* + * We make lateral_only names of this level visible, whether or not the + * RangeTableFunc is explicitly marked LATERAL. This is needed for SQL + * spec compliance and seems useful on convenience grounds for all + * functions in FROM. + * + * (LATERAL can't nest within a single pstate level, so we don't need + * save/restore logic here.) + */ + Assert(!pstate->p_lateral_active); + pstate->p_lateral_active = true; + + tf->functype = TFT_JSON_TABLE; + tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); + + cxt.contextItemTypid = exprType(tf->docexpr); + + if (!IsA(jt->common->pathspec, A_Const) || + castNode(A_Const, jt->common->pathspec)->val.node.type != T_String) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("only string constants supported in JSON_TABLE path specification"), + parser_errposition(pstate, + exprLocation(jt->common->pathspec)))); + + rootPath = castNode(A_Const, jt->common->pathspec)->val.sval.sval; + + tf->plan = (Node *) transformJsonTableColumns(&cxt, jt->columns, rootPath, + jt->common->location); + + tf->ordinalitycol = -1; /* undefine ordinality column number */ + tf->location = jt->location; + + pstate->p_lateral_active = false; + + /* + * Mark the RTE as LATERAL if the user said LATERAL explicitly, or if + * there are any lateral cross-references in it. + */ + is_lateral = jt->lateral || contain_vars_of_level((Node *) tf, 0); + + return addRangeTableEntryForTableFunc(pstate, + tf, jt->alias, is_lateral, true); +} diff --git a/src/backend/parser/parse_relation.c b/src/backend/parser/parse_relation.c index 7efa5f15d7..5448cb01fa 100644 --- a/src/backend/parser/parse_relation.c +++ b/src/backend/parser/parse_relation.c @@ -1989,7 +1989,8 @@ addRangeTableEntryForTableFunc(ParseState *pstate, bool inFromCl) { RangeTblEntry *rte = makeNode(RangeTblEntry); - char *refname = alias ? alias->aliasname : pstrdup("xmltable"); + char *refname = alias ? alias->aliasname : + pstrdup(tf->functype == TFT_XMLTABLE ? "xmltable" : "json_table"); Alias *eref; int numaliases; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 31c576cfec..2a1d44b813 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -1993,6 +1993,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_EXISTS_OP: *name = "json_exists"; return 2; + case JSON_TABLE_OP: + *name = "json_table"; + return 2; } break; default: diff --git a/src/backend/utils/adt/jsonpath_exec.c b/src/backend/utils/adt/jsonpath_exec.c index 7811fa31e0..c55b3aae02 100644 --- a/src/backend/utils/adt/jsonpath_exec.c +++ b/src/backend/utils/adt/jsonpath_exec.c @@ -61,9 +61,11 @@ #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "executor/execExpr.h" #include "funcapi.h" #include "lib/stringinfo.h" #include "miscadmin.h" +#include "nodes/nodeFuncs.h" #include "regex/regex.h" #include "utils/builtins.h" #include "utils/date.h" @@ -74,6 +76,8 @@ #include "utils/guc.h" #include "utils/json.h" #include "utils/jsonpath.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" #include "utils/timestamp.h" #include "utils/varlena.h" @@ -155,6 +159,57 @@ typedef struct JsonValueListIterator ListCell *next; } JsonValueListIterator; +/* Structures for JSON_TABLE execution */ +typedef struct JsonTableScanState JsonTableScanState; +typedef struct JsonTableJoinState JsonTableJoinState; + +struct JsonTableScanState +{ + JsonTableScanState *parent; + JsonTableJoinState *nested; + MemoryContext mcxt; + JsonPath *path; + List *args; + JsonValueList found; + JsonValueListIterator iter; + Datum current; + int ordinal; + bool currentIsNull; + bool errorOnError; + bool advanceNested; + bool reset; +}; + +struct JsonTableJoinState +{ + union + { + struct + { + JsonTableJoinState *left; + JsonTableJoinState *right; + bool advanceRight; + } join; + JsonTableScanState scan; + } u; + bool is_join; +}; + +/* random number to identify JsonTableContext */ +#define JSON_TABLE_CONTEXT_MAGIC 418352867 + +typedef struct JsonTableContext +{ + int magic; + struct + { + ExprState *expr; + JsonTableScanState *scan; + } *colexprs; + JsonTableScanState root; + bool empty; +} JsonTableContext; + /* strict/lax flags is decomposed into four [un]wrap/error flags */ #define jspStrictAbsenseOfErrors(cxt) (!(cxt)->laxMode) #define jspAutoUnwrap(cxt) ((cxt)->laxMode) @@ -245,6 +300,7 @@ static JsonPathExecResult getArrayIndex(JsonPathExecContext *cxt, JsonPathItem *jsp, JsonbValue *jb, int32 *index); static JsonBaseObjectInfo setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id); +static void JsonValueListClear(JsonValueList *jvl); static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv); static int JsonValueListLength(const JsonValueList *jvl); static bool JsonValueListIsEmpty(JsonValueList *jvl); @@ -262,6 +318,12 @@ static JsonbValue *wrapItemsInArray(const JsonValueList *items); static int compareDatetime(Datum val1, Oid typid1, Datum val2, Oid typid2, bool useTz, bool *have_error); + +static JsonTableJoinState *JsonTableInitPlanState(JsonTableContext *cxt, + Node *plan, JsonTableScanState *parent); +static bool JsonTableNextRow(JsonTableScanState *scan); + + /****************** User interface to JsonPath executor ********************/ /* @@ -2458,6 +2520,13 @@ setBaseObject(JsonPathExecContext *cxt, JsonbValue *jbv, int32 id) return baseObject; } +static void +JsonValueListClear(JsonValueList *jvl) +{ + jvl->singleton = NULL; + jvl->list = NULL; +} + static void JsonValueListAppend(JsonValueList *jvl, JsonbValue *jbv) { @@ -3067,3 +3136,370 @@ JsonItemFromDatum(Datum val, Oid typid, int32 typmod, JsonbValue *res) "casted to supported jsonpath types."))); } } + +/************************ JSON_TABLE functions ***************************/ + +/* + * Returns private data from executor state. Ensure validity by check with + * MAGIC number. + */ +static inline JsonTableContext * +GetJsonTableContext(TableFuncScanState *state, const char *fname) +{ + JsonTableContext *result; + + if (!IsA(state, TableFuncScanState)) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + result = (JsonTableContext *) state->opaque; + if (result->magic != JSON_TABLE_CONTEXT_MAGIC) + elog(ERROR, "%s called with invalid TableFuncScanState", fname); + + return result; +} + +/* Recursively initialize JSON_TABLE scan state */ +static void +JsonTableInitScanState(JsonTableContext *cxt, JsonTableScanState *scan, + JsonTableParent *node, JsonTableScanState *parent, + List *args, MemoryContext mcxt) +{ + int i; + + scan->parent = parent; + scan->errorOnError = node->errorOnError; + scan->path = DatumGetJsonPathP(node->path->constvalue); + scan->args = args; + scan->mcxt = AllocSetContextCreate(mcxt, "JsonTableContext", + ALLOCSET_DEFAULT_SIZES); + scan->nested = node->child ? + JsonTableInitPlanState(cxt, node->child, scan) : NULL; + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + + for (i = node->colMin; i <= node->colMax; i++) + cxt->colexprs[i].scan = scan; +} + +/* Recursively initialize JSON_TABLE scan state */ +static JsonTableJoinState * +JsonTableInitPlanState(JsonTableContext *cxt, Node *plan, + JsonTableScanState *parent) +{ + JsonTableJoinState *state = palloc0(sizeof(*state)); + + if (IsA(plan, JsonTableSibling)) + { + JsonTableSibling *join = castNode(JsonTableSibling, plan); + + state->is_join = true; + state->u.join.left = JsonTableInitPlanState(cxt, join->larg, parent); + state->u.join.right = JsonTableInitPlanState(cxt, join->rarg, parent); + } + else + { + JsonTableParent *node = castNode(JsonTableParent, plan); + + state->is_join = false; + + JsonTableInitScanState(cxt, &state->u.scan, node, parent, + parent->args, parent->mcxt); + } + + return state; +} + +/* + * JsonTableInitOpaque + * Fill in TableFuncScanState->opaque for JsonTable processor + */ +static void +JsonTableInitOpaque(TableFuncScanState *state, int natts) +{ + JsonTableContext *cxt; + PlanState *ps = &state->ss.ps; + TableFuncScan *tfs = castNode(TableFuncScan, ps->plan); + TableFunc *tf = tfs->tablefunc; + JsonExpr *ci = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + List *args = NIL; + ListCell *lc; + int i; + + cxt = palloc0(sizeof(JsonTableContext)); + cxt->magic = JSON_TABLE_CONTEXT_MAGIC; + + if (ci->passing_values) + { + ListCell *exprlc; + ListCell *namelc; + + forboth(exprlc, ci->passing_values, + namelc, ci->passing_names) + { + Expr *expr = (Expr *) lfirst(exprlc); + String *name = lfirst_node(String, namelc); + JsonPathVariableEvalContext *var = palloc(sizeof(*var)); + + var->name = pstrdup(name->sval); + var->typid = exprType((Node *) expr); + var->typmod = exprTypmod((Node *) expr); + var->estate = ExecInitExpr(expr, ps); + var->econtext = ps->ps_ExprContext; + var->mcxt = CurrentMemoryContext; + var->evaluated = false; + var->value = (Datum) 0; + var->isnull = true; + + args = lappend(args, var); + } + } + + cxt->colexprs = palloc(sizeof(*cxt->colexprs) * + list_length(tf->colvalexprs)); + + JsonTableInitScanState(cxt, &cxt->root, root, NULL, args, + CurrentMemoryContext); + + i = 0; + + foreach(lc, tf->colvalexprs) + { + Expr *expr = lfirst(lc); + + cxt->colexprs[i].expr = + ExecInitExprWithCaseValue(expr, ps, + &cxt->colexprs[i].scan->current, + &cxt->colexprs[i].scan->currentIsNull); + + i++; + } + + state->opaque = cxt; +} + +/* Reset scan iterator to the beginning of the item list */ +static void +JsonTableRescan(JsonTableScanState *scan) +{ + JsonValueListInitIterator(&scan->found, &scan->iter); + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + scan->advanceNested = false; + scan->ordinal = 0; +} + +/* Reset context item of a scan, execute JSON path and reset a scan */ +static void +JsonTableResetContextItem(JsonTableScanState *scan, Datum item) +{ + MemoryContext oldcxt; + JsonPathExecResult res; + Jsonb *js = (Jsonb *) DatumGetJsonbP(item); + + JsonValueListClear(&scan->found); + + MemoryContextResetOnly(scan->mcxt); + + oldcxt = MemoryContextSwitchTo(scan->mcxt); + + res = executeJsonPath(scan->path, scan->args, EvalJsonPathVar, js, + scan->errorOnError, &scan->found, false /* FIXME */); + + MemoryContextSwitchTo(oldcxt); + + if (jperIsError(res)) + { + Assert(!scan->errorOnError); + JsonValueListClear(&scan->found); /* EMPTY ON ERROR case */ + } + + JsonTableRescan(scan); +} + +/* + * JsonTableSetDocument + * Install the input document + */ +static void +JsonTableSetDocument(TableFuncScanState *state, Datum value) +{ + JsonTableContext *cxt = GetJsonTableContext(state, "JsonTableSetDocument"); + + JsonTableResetContextItem(&cxt->root, value); +} + +/* + * Fetch next row from a union joined scan. + * + * Returns false at the end of a scan, true otherwise. + */ +static bool +JsonTableNextJoinRow(JsonTableJoinState *state) +{ + if (!state->is_join) + return JsonTableNextRow(&state->u.scan); + + if (!state->u.join.advanceRight) + { + /* fetch next outer row */ + if (JsonTableNextJoinRow(state->u.join.left)) + return true; + + state->u.join.advanceRight = true; /* next inner row */ + } + + /* fetch next inner row */ + return JsonTableNextJoinRow(state->u.join.right); +} + +/* Recursively set 'reset' flag of scan and its child nodes */ +static void +JsonTableJoinReset(JsonTableJoinState *state) +{ + if (state->is_join) + { + JsonTableJoinReset(state->u.join.left); + JsonTableJoinReset(state->u.join.right); + state->u.join.advanceRight = false; + } + else + { + state->u.scan.reset = true; + state->u.scan.advanceNested = false; + + if (state->u.scan.nested) + JsonTableJoinReset(state->u.scan.nested); + } +} + +/* + * Fetch next row from a simple scan with outer joined nested subscans. + * + * Returns 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) + { + Assert(!scan->parent->currentIsNull); + JsonTableResetContextItem(scan, scan->parent->current); + scan->reset = false; + } + + if (scan->advanceNested) + { + /* fetch next nested row */ + if (JsonTableNextJoinRow(scan->nested)) + return true; + + scan->advanceNested = false; + } + + /* fetch next row */ + jbv = JsonValueListNext(&scan->found, &scan->iter); + + if (!jbv) + { + scan->current = PointerGetDatum(NULL); + scan->currentIsNull = true; + return false; /* end of scan */ + } + + /* set current row item */ + oldcxt = MemoryContextSwitchTo(scan->mcxt); + scan->current = JsonbPGetDatum(JsonbValueToJsonb(jbv)); + scan->currentIsNull = false; + MemoryContextSwitchTo(oldcxt); + + scan->ordinal++; + + if (scan->nested) + { + JsonTableJoinReset(scan->nested); + scan->advanceNested = JsonTableNextJoinRow(scan->nested); + } + + return true; +} + +/* + * JsonTableFetchRow + * Prepare the next "current" tuple for upcoming GetValue calls. + * Returns FALSE if the row-filter expression returned no more rows. + */ +static bool +JsonTableFetchRow(TableFuncScanState *state) +{ + JsonTableContext *cxt = GetJsonTableContext(state, "JsonTableFetchRow"); + + if (cxt->empty) + return false; + + return JsonTableNextRow(&cxt->root); +} + +/* + * JsonTableGetValue + * Return the value for column number 'colnum' for the current row. + * + * This leaks memory, so be sure to reset often the context in which it's + * called. + */ +static Datum +JsonTableGetValue(TableFuncScanState *state, int colnum, + Oid typid, int32 typmod, bool *isnull) +{ + JsonTableContext *cxt = GetJsonTableContext(state, "JsonTableGetValue"); + ExprContext *econtext = state->ss.ps.ps_ExprContext; + ExprState *estate = cxt->colexprs[colnum].expr; + JsonTableScanState *scan = cxt->colexprs[colnum].scan; + Datum result; + + if (scan->currentIsNull) /* NULL from outer/union join */ + { + result = (Datum) 0; + *isnull = true; + } + else if (estate) /* regular column */ + { + result = ExecEvalExpr(estate, econtext, isnull); + } + else + { + result = Int32GetDatum(scan->ordinal); /* ordinality column */ + *isnull = false; + } + + return result; +} + +/* + * JsonTableDestroyOpaque + */ +static void +JsonTableDestroyOpaque(TableFuncScanState *state) +{ + JsonTableContext *cxt = GetJsonTableContext(state, "JsonTableDestroyOpaque"); + + /* not valid anymore */ + cxt->magic = 0; + + state->opaque = NULL; +} + +const TableFuncRoutine JsonbTableRoutine = +{ + JsonTableInitOpaque, + JsonTableSetDocument, + NULL, + NULL, + NULL, + JsonTableFetchRow, + JsonTableGetValue, + JsonTableDestroyOpaque +}; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 4458d2ff90..e6173a9db4 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -503,6 +503,8 @@ static char *flatten_reloptions(Oid relid); static void get_reloptions(StringInfo buf, Datum reloptions); static void get_json_path_spec(Node *path_spec, deparse_context *context, bool showimplicit); +static void get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit); #define only_marker(rte) ((rte)->inh ? "" : "ONLY ") @@ -8516,7 +8518,8 @@ get_json_behavior(JsonBehavior *behavior, deparse_context *context, /* * get_json_expr_options * - * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS. + * Parse back common options for JSON_QUERY, JSON_VALUE, JSON_EXISTS and + * JSON_TABLE columns. */ static void get_json_expr_options(JsonExpr *jsexpr, deparse_context *context, @@ -9763,6 +9766,9 @@ get_rule_expr(Node *node, deparse_context *context, case JSON_EXISTS_OP: appendStringInfoString(buf, "JSON_EXISTS("); break; + default: + elog(ERROR, "unexpected JsonExpr type: %d", jexpr->op); + break; } get_rule_expr(jexpr->formatted_expr, context, showimplicit); @@ -11039,16 +11045,14 @@ get_sublink_expr(SubLink *sublink, deparse_context *context) /* ---------- - * get_tablefunc - Parse back a table function + * get_xmltable - Parse back a XMLTABLE function * ---------- */ static void -get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +get_xmltable(TableFunc *tf, deparse_context *context, bool showimplicit) { StringInfo buf = context->buf; - /* XMLTABLE is the only existing implementation. */ - appendStringInfoString(buf, "XMLTABLE("); if (tf->ns_uris != NIL) @@ -11139,6 +11143,220 @@ get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) appendStringInfoChar(buf, ')'); } +/* + * get_json_nested_columns - Parse back nested JSON_TABLE columns + */ +static void +get_json_table_nested_columns(TableFunc *tf, Node *node, + deparse_context *context, bool showimplicit, + bool needcomma) +{ + if (IsA(node, JsonTableSibling)) + { + JsonTableSibling *n = (JsonTableSibling *) node; + + get_json_table_nested_columns(tf, n->larg, context, showimplicit, + needcomma); + get_json_table_nested_columns(tf, n->rarg, context, showimplicit, true); + } + else + { + JsonTableParent *n = castNode(JsonTableParent, node); + + if (needcomma) + appendStringInfoChar(context->buf, ','); + + appendStringInfoChar(context->buf, ' '); + appendContextKeyword(context, "NESTED PATH ", 0, 0, 0); + get_const_expr(n->path, context, -1); + get_json_table_columns(tf, n, context, showimplicit); + } +} + +/* + * get_json_table_columns - Parse back JSON_TABLE columns + */ +static void +get_json_table_columns(TableFunc *tf, JsonTableParent *node, + deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + ListCell *lc_colname; + ListCell *lc_coltype; + ListCell *lc_coltypmod; + ListCell *lc_colvarexpr; + int colnum = 0; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "COLUMNS (", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forfour(lc_colname, tf->colnames, + lc_coltype, tf->coltypes, + lc_coltypmod, tf->coltypmods, + lc_colvarexpr, tf->colvalexprs) + { + char *colname = strVal(lfirst(lc_colname)); + JsonExpr *colexpr; + Oid typid; + int32 typmod; + bool ordinality; + JsonBehaviorType default_behavior; + + typid = lfirst_oid(lc_coltype); + typmod = lfirst_int(lc_coltypmod); + colexpr = castNode(JsonExpr, lfirst(lc_colvarexpr)); + + if (colnum < node->colMin) + { + colnum++; + continue; + } + + if (colnum > node->colMax) + break; + + if (colnum > node->colMin) + appendStringInfoString(buf, ", "); + + colnum++; + + ordinality = !colexpr; + + appendContextKeyword(context, "", 0, 0, 0); + + appendStringInfo(buf, "%s %s", quote_identifier(colname), + ordinality ? "FOR ORDINALITY" : + format_type_with_typemod(typid, typmod)); + if (ordinality) + continue; + + if (colexpr->op == JSON_EXISTS_OP) + { + appendStringInfoString(buf, " EXISTS"); + default_behavior = JSON_BEHAVIOR_FALSE; + } + else + { + if (colexpr->op == JSON_QUERY_OP) + { + char typcategory; + bool typispreferred; + + get_type_category_preferred(typid, &typcategory, &typispreferred); + + if (typcategory == TYPCATEGORY_STRING) + appendStringInfoString(buf, + colexpr->format->format_type == JS_FORMAT_JSONB ? + " FORMAT JSONB" : " FORMAT JSON"); + } + + 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); + + get_json_expr_options(colexpr, context, default_behavior); + } + + if (node->child) + get_json_table_nested_columns(tf, node->child, context, showimplicit, + node->colMax >= node->colMin); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_json_table - Parse back a JSON_TABLE function + * ---------- + */ +static void +get_json_table(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + StringInfo buf = context->buf; + JsonExpr *jexpr = castNode(JsonExpr, tf->docexpr); + JsonTableParent *root = castNode(JsonTableParent, tf->plan); + + appendStringInfoString(buf, "JSON_TABLE("); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr(jexpr->formatted_expr, context, showimplicit); + + appendStringInfoString(buf, ", "); + + get_const_expr(root->path, context, -1); + + if (jexpr->passing_values) + { + ListCell *lc1, *lc2; + bool needcomma = false; + + appendStringInfoChar(buf, ' '); + appendContextKeyword(context, "PASSING ", 0, 0, 0); + + if (PRETTY_INDENT(context)) + context->indentLevel += PRETTYINDENT_VAR; + + forboth(lc1, jexpr->passing_names, + lc2, jexpr->passing_values) + { + if (needcomma) + appendStringInfoString(buf, ", "); + needcomma = true; + + appendContextKeyword(context, "", 0, 0, 0); + + get_rule_expr((Node *) lfirst(lc2), context, false); + appendStringInfo(buf, " AS %s", + quote_identifier((lfirst_node(String, lc1))->sval) + ); + } + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + } + + get_json_table_columns(tf, root, context, showimplicit); + + if (jexpr->on_error->btype != JSON_BEHAVIOR_EMPTY) + get_json_behavior(jexpr->on_error, context, "ERROR"); + + if (PRETTY_INDENT(context)) + context->indentLevel -= PRETTYINDENT_VAR; + + appendContextKeyword(context, ")", 0, 0, 0); +} + +/* ---------- + * get_tablefunc - Parse back a table function + * ---------- + */ +static void +get_tablefunc(TableFunc *tf, deparse_context *context, bool showimplicit) +{ + /* XMLTABLE and JSON_TABLE are the only existing implementations. */ + + if (tf->functype == TFT_XMLTABLE) + get_xmltable(tf, context, showimplicit); + else if (tf->functype == TFT_JSON_TABLE) + get_json_table(tf, context, showimplicit); +} + /* ---------- * get_from_clause - Parse back a FROM clause * diff --git a/src/backend/utils/misc/queryjumble.c b/src/backend/utils/misc/queryjumble.c index 7120836c70..2ffa014618 100644 --- a/src/backend/utils/misc/queryjumble.c +++ b/src/backend/utils/misc/queryjumble.c @@ -878,9 +878,11 @@ JumbleExpr(JumbleState *jstate, Node *node) { TableFunc *tablefunc = (TableFunc *) node; + APP_JUMB(tablefunc->functype); JumbleExpr(jstate, tablefunc->docexpr); JumbleExpr(jstate, tablefunc->rowexpr); JumbleExpr(jstate, (Node *) tablefunc->colexprs); + JumbleExpr(jstate, (Node *) tablefunc->colvalexprs); } break; case T_TableSampleClause: diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index 9ce8df17e5..9df70e6f06 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -850,6 +850,10 @@ extern Datum ExecPrepareJsonItemCoercion(struct JsonbValue *item, struct JsonCoercionState **pjcstate); extern bool ExecEvalJsonNeedsSubTransaction(JsonExpr *jsexpr, struct JsonCoercionsState *); +extern Datum ExecEvalExprPassingCaseValue(ExprState *estate, + ExprContext *econtext, bool *isnull, + Datum caseval_datum, + bool caseval_isnull); extern void ExecAggInitGroup(AggState *aggstate, AggStatePerTrans pertrans, AggStatePerGroup pergroup, ExprContext *aggcontext); diff --git a/src/include/nodes/nodes.h b/src/include/nodes/nodes.h index 53f6b05a3f..aefce33e28 100644 --- a/src/include/nodes/nodes.h +++ b/src/include/nodes/nodes.h @@ -212,6 +212,8 @@ typedef enum NodeTag T_JsonExpr, T_JsonCoercion, T_JsonItemCoercions, + T_JsonTableParent, + T_JsonTableSibling, /* * TAGS FOR EXPRESSION STATE NODES (execnodes.h) @@ -514,6 +516,8 @@ typedef enum NodeTag T_JsonArrayAgg, T_JsonFuncExpr, T_JsonIsPredicate, + T_JsonTable, + T_JsonTableColumn, T_JsonCommon, T_JsonArgument, T_JsonKeyValue, diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 8a9ccf6221..e58211eac1 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1605,6 +1605,19 @@ typedef enum JsonQuotes JS_QUOTES_OMIT /* OMIT QUOTES */ } JsonQuotes; +/* + * JsonTableColumnType - + * enumeration of JSON_TABLE column types + */ +typedef enum +{ + JTC_FOR_ORDINALITY, + JTC_REGULAR, + JTC_EXISTS, + JTC_FORMATTED, + JTC_NESTED, +} JsonTableColumnType; + /* * JsonPathSpec - * representation of JSON path constant @@ -1664,6 +1677,41 @@ typedef struct JsonFuncExpr int location; /* token location, or -1 if unknown */ } JsonFuncExpr; +/* + * JsonTableColumn - + * untransformed representation of JSON_TABLE column + */ +typedef struct JsonTableColumn +{ + NodeTag type; + JsonTableColumnType coltype; /* column type */ + char *name; /* column name */ + TypeName *typeName; /* column type name */ + JsonPathSpec pathspec; /* path specification, if any */ + JsonFormat *format; /* JSON format clause, if specified */ + JsonWrapper wrapper; /* WRAPPER behavior for formatted columns */ + bool omit_quotes; /* omit or keep quotes on scalar strings? */ + List *columns; /* nested columns */ + JsonBehavior *on_empty; /* ON EMPTY behavior */ + JsonBehavior *on_error; /* ON ERROR behavior */ + int location; /* token location, or -1 if unknown */ +} JsonTableColumn; + +/* + * JsonTable - + * untransformed representation of JSON_TABLE + */ +typedef struct JsonTable +{ + NodeTag type; + JsonCommon *common; /* common JSON path syntax fields */ + List *columns; /* list of JsonTableColumn */ + JsonBehavior *on_error; /* ON ERROR behavior, if specified */ + Alias *alias; /* table alias in FROM clause */ + bool lateral; /* does it have LATERAL prefix? */ + int location; /* token location, or -1 if unknown */ +} JsonTable; + /* * JsonKeyValue - * untransformed representation of JSON object key-value pair for diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 439a16aa62..290898cfd7 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -73,8 +73,14 @@ typedef struct RangeVar int location; /* token location, or -1 if unknown */ } RangeVar; +typedef enum TableFuncType +{ + TFT_XMLTABLE, + TFT_JSON_TABLE +} TableFuncType; + /* - * TableFunc - node for a table function, such as XMLTABLE. + * TableFunc - node for a table function, such as XMLTABLE or JSON_TABLE. * * Entries in the ns_names list are either String nodes containing * literal namespace names, or NULL pointers to represent DEFAULT. @@ -82,6 +88,7 @@ typedef struct RangeVar typedef struct TableFunc { NodeTag type; + TableFuncType functype; /* XMLTABLE or JSON_TABLE */ List *ns_uris; /* list of namespace URI expressions */ List *ns_names; /* list of namespace names or NULL */ Node *docexpr; /* input document expression */ @@ -92,7 +99,9 @@ typedef struct TableFunc List *colcollations; /* OID list of column collation OIDs */ List *colexprs; /* list of column filter expressions */ List *coldefexprs; /* list of column default expressions */ + List *colvalexprs; /* list of column value expressions */ Bitmapset *notnulls; /* nullability flag for each output column */ + Node *plan; /* JSON_TABLE plan */ int ordinalitycol; /* counts from 0; -1 if none specified */ int location; /* token location, or -1 if unknown */ } TableFunc; @@ -1241,7 +1250,8 @@ typedef enum JsonExprOp { JSON_VALUE_OP, /* JSON_VALUE() */ JSON_QUERY_OP, /* JSON_QUERY() */ - JSON_EXISTS_OP /* JSON_EXISTS() */ + JSON_EXISTS_OP, /* JSON_EXISTS() */ + JSON_TABLE_OP /* JSON_TABLE() */ } JsonExprOp; /* @@ -1455,6 +1465,31 @@ typedef struct JsonExpr int location; /* token location, or -1 if unknown */ } JsonExpr; +/* + * JsonTableParent - + * transformed representation of parent JSON_TABLE plan node + */ +typedef struct JsonTableParent +{ + NodeTag type; + Const *path; /* jsonpath constant */ + Node *child; /* nested columns, if any */ + 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 */ +} JsonTableParent; + +/* + * JsonTableSibling - + * transformed representation of joined sibling JSON_TABLE plan node + */ +typedef struct JsonTableSibling +{ + NodeTag type; + Node *larg; /* left join node */ + Node *rarg; /* right join node */ +} JsonTableSibling; + /* ---------------- * NullTest * diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index a73032b319..9097ce7b26 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -241,6 +241,7 @@ PG_KEYWORD("json_objectagg", JSON_OBJECTAGG, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_query", JSON_QUERY, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_scalar", JSON_SCALAR, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_serialize", JSON_SERIALIZE, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("json_table", JSON_TABLE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("json_value", JSON_VALUE, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("keep", KEEP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("key", KEY, UNRESERVED_KEYWORD, BARE_LABEL) @@ -283,6 +284,7 @@ PG_KEYWORD("names", NAMES, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("national", NATIONAL, COL_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("natural", NATURAL, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL) PG_KEYWORD("nchar", NCHAR, COL_NAME_KEYWORD, BARE_LABEL) +PG_KEYWORD("nested", NESTED, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("new", NEW, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("next", NEXT, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("nfc", NFC, UNRESERVED_KEYWORD, BARE_LABEL) @@ -332,6 +334,7 @@ PG_KEYWORD("partial", PARTIAL, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("partition", PARTITION, UNRESERVED_KEYWORD, BARE_LABEL) 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("plans", PLANS, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("policy", POLICY, UNRESERVED_KEYWORD, BARE_LABEL) diff --git a/src/include/parser/parse_clause.h b/src/include/parser/parse_clause.h index 2495c30034..e86b002392 100644 --- a/src/include/parser/parse_clause.h +++ b/src/include/parser/parse_clause.h @@ -51,4 +51,7 @@ extern List *addTargetToSortList(ParseState *pstate, TargetEntry *tle, extern Index assignSortGroupRef(TargetEntry *tle, List *tlist); extern bool targetIsInSortList(TargetEntry *tle, Oid sortop, List *sortList); +/* functions in parse_jsontable.c */ +extern ParseNamespaceItem *transformJsonTable(ParseState *pstate, JsonTable *jt); + #endif /* PARSE_CLAUSE_H */ diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 98a61d7f72..1897ddffa6 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -15,6 +15,7 @@ #define JSONPATH_H #include "fmgr.h" +#include "executor/tablefunc.h" #include "nodes/pg_list.h" #include "nodes/primnodes.h" #include "utils/jsonb.h" @@ -263,6 +264,7 @@ typedef struct JsonPathVariableEvalContext int32 typmod; struct ExprContext *econtext; struct ExprState *estate; + MemoryContext mcxt; /* memory context for cached value */ Datum value; bool isnull; bool evaluated; @@ -281,4 +283,6 @@ extern JsonbValue *JsonPathValue(Datum jb, JsonPath *jp, bool *empty, extern int EvalJsonPathVar(void *vars, char *varName, int varNameLen, JsonbValue *val, JsonbValue *baseObject); +extern const TableFuncRoutine JsonbTableRoutine; + #endif diff --git a/src/test/regress/expected/json_sqljson.out b/src/test/regress/expected/json_sqljson.out index bb62634314..5c4dfa5f7c 100644 --- a/src/test/regress/expected/json_sqljson.out +++ b/src/test/regress/expected/json_sqljson.out @@ -13,3 +13,9 @@ SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ERROR: JSON_QUERY() is not yet implemented for json type LINE 1: SELECT JSON_QUERY(NULL FORMAT JSON, '$'); ^ +-- JSON_TABLE +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); +ERROR: JSON_TABLE() is not yet implemented for json type +LINE 1: SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo ... + ^ +HINT: Try casting the argument to jsonb diff --git a/src/test/regress/expected/jsonb_sqljson.out b/src/test/regress/expected/jsonb_sqljson.out index 3661b7a810..144cc0c557 100644 --- a/src/test/regress/expected/jsonb_sqljson.out +++ b/src/test/regress/expected/jsonb_sqljson.out @@ -1020,3 +1020,565 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.dateti ERROR: functions in index expression must be marked IMMUTABLE CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; +-- JSON_TABLE +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); +ERROR: syntax error at or near "(" +LINE 1: SELECT JSON_TABLE('[]', '$'); + ^ +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); +ERROR: syntax error at or near ")" +LINE 1: SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + ^ +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + foo +----- +(0 rows) + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + item | foo +------+----- + 123 | +(1 row) + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + js | id | id2 | int | text | char(4) | bool | numeric | domain | js | jb | jst | jsc | jsv | jsb | jsbq | aaa | aaa1 | exists1 | exists2 | exists3 | exists4 | js2 | jsb2w | jsb2q | ia | ta | jba +---------------------------------------------------------------------------------------+----+-----+-----+---------+---------+------+---------+---------+--------------+--------------+--------------+------+------+--------------+--------------+-----+------+---------+---------+---------+---------+--------------+----------------+--------------+----+----+----- + 1 | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [] | | | | | | | | | | | | | | | | | | | | | | | | | | | + {} | 1 | 1 | | | | | | | {} | {} | {} | {} | {} | {} | {} | | | f | 0 | | false | {} | [{}] | {} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 1 | 1 | 1 | 1 | 1 | | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | | f | 0 | | false | 1 | [1] | 1 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 2 | 2 | 1 | 1.23 | 1.23 | | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | 1.23 | | | f | 0 | | false | 1.23 | [1.23] | 1.23 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 3 | 3 | 2 | 2 | 2 | | 2 | 2 | "2" | "2" | "2" | "2" | "2" | "2" | 2 | | | f | 0 | | false | "2" | ["2"] | 2 | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 4 | 4 | | aaaaaaa | aaaa | | | aaaaaaa | "aaaaaaa" | "aaaaaaa" | "aaaaaaa" | "aaa | "aaa | "aaaaaaa" | | | | f | 0 | | false | "aaaaaaa" | ["aaaaaaa"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 5 | 5 | | foo | foo | | | | "foo" | "foo" | "foo" | "foo | "foo | "foo" | | | | f | 0 | | false | "foo" | ["foo"] | | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 6 | 6 | | | | | | | null | null | null | null | null | null | null | | | f | 0 | | false | null | [null] | null | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 7 | 7 | 0 | false | fals | f | | false | false | false | false | fals | fals | false | false | | | f | 0 | | false | false | [false] | false | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 8 | 8 | 1 | true | true | t | | true | true | true | true | true | true | true | true | | | f | 0 | | false | true | [true] | true | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 9 | 9 | | | | | | | {"aaa": 123} | {"aaa": 123} | {"aaa": 123} | {"aa | {"aa | {"aaa": 123} | {"aaa": 123} | 123 | 123 | t | 1 | 1 | true | {"aaa": 123} | [{"aaa": 123}] | {"aaa": 123} | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 10 | 10 | | [1,2] | [1,2 | | | [1,2] | "[1,2]" | "[1,2]" | "[1,2]" | "[1, | "[1, | "[1,2]" | [1, 2] | | | f | 0 | | false | "[1,2]" | ["[1,2]"] | [1, 2] | | | + [1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""] | 11 | 11 | | "str" | "str | | | "str" | "\"str\"" | "\"str\"" | "\"str\"" | "\"s | "\"s | "\"str\"" | "str" | | | f | 0 | | false | "\"str\"" | ["\"str\""] | "str" | | | +(14 rows) + +-- JSON_TABLE: Test backward parsing +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + NESTED PATH '$[1]' COLUMNS ( + a1 int, + NESTED PATH '$[*]' COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' COLUMNS ( + NESTED PATH '$[*]' COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' COLUMNS ( + a22 text + ) + ) + ) + ); +\sv jsonb_table_view +CREATE OR REPLACE VIEW public.jsonb_table_view AS + SELECT "json_table".id, + "json_table".id2, + "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 + FROM JSON_TABLE( + 'null'::jsonb, '$[*]' + PASSING + 1 + 2 AS a, + '"foo"'::json AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 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]' + COLUMNS ( + a1 integer PATH '$."a1"', + b1 text PATH '$."b1"', + NESTED PATH '$[*]' + COLUMNS ( + a11 text PATH '$."a11"' + ) + ), + NESTED PATH '$[2]' + COLUMNS ( + NESTED PATH '$[*]' + COLUMNS ( + a21 text PATH '$."a21"' + ), + NESTED PATH '$[*]' + COLUMNS ( + a22 text PATH '$."a22"' + ) + ) + ) + ) +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Table Function Scan on "json_table" + Output: "json_table".id, "json_table".id2, "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, '$[*]' PASSING 3 AS a, '"foo"'::jsonb AS "b c" COLUMNS (id FOR ORDINALITY, id2 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]' COLUMNS (a1 integer PATH '$."a1"', b1 text PATH '$."b1"', NESTED PATH '$[*]' COLUMNS (a11 text PATH '$."a11"')), NESTED PATH '$[2]' COLUMNS ( NESTED PATH '$[*]' COLUMNS (a21 text PATH '$."a21"'), NESTED PATH '$[*]' COLUMNS (a22 text PATH '$."a22"')))) +(3 rows) + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + js | a +-------+--- + 1 | 1 + "err" | +(2 rows) + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; +ERROR: invalid input syntax for type integer: "err" +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; + a +--- + +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: jsonpath member accessor can only be applied to an object +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +ERROR: no SQL/JSON item +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 2 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + a +--- + 1 +(1 row) + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); + a +--- + 0 +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to smallint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to bigint +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to real +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 E... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); + a +----- + fal +(1 row) + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to json +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXI... + ^ +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); +ERROR: cannot cast type boolean to jsonb +LINE 1: ...ELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EX... + ^ +-- JSON_TABLE: nested paths and plans +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + a int, + b text, + a jsonb + ) +) jt; +ERROR: duplicate JSON_TABLE column name: a +HINT: JSON_TABLE column names must be distinct from one another +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' + COLUMNS ( + c int, + b text + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' + COLUMNS ( + c int, + b text + ) + ) + ) +) jt; +ERROR: duplicate JSON_TABLE column name: b +HINT: JSON_TABLE column names must be distinct from one another +-- JSON_TABLE: plan execution +CREATE TEMP TABLE jsonb_table_test (js jsonb); +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); +-- unspecified 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 '$' ) + ) + ) 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) + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + x | y | y | z +---+---+--------------+--- + 2 | 1 | [1, 2, 3] | 1 + 2 | 1 | [1, 2, 3] | 2 + 2 | 1 | [1, 2, 3] | 3 + 3 | 1 | [1, 2, 3] | 1 + 3 | 1 | [1, 2, 3] | 2 + 3 | 1 | [1, 2, 3] | 3 + 3 | 1 | [2, 3, 4, 5] | 2 + 3 | 1 | [2, 3, 4, 5] | 3 + 3 | 1 | [2, 3, 4, 5] | 4 + 3 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [1, 2, 3] | 1 + 4 | 1 | [1, 2, 3] | 2 + 4 | 1 | [1, 2, 3] | 3 + 4 | 1 | [2, 3, 4, 5] | 2 + 4 | 1 | [2, 3, 4, 5] | 3 + 4 | 1 | [2, 3, 4, 5] | 4 + 4 | 1 | [2, 3, 4, 5] | 5 + 4 | 1 | [3, 4, 5, 6] | 3 + 4 | 1 | [3, 4, 5, 6] | 4 + 4 | 1 | [3, 4, 5, 6] | 5 + 4 | 1 | [3, 4, 5, 6] | 6 + 2 | 2 | [1, 2, 3] | 2 + 2 | 2 | [1, 2, 3] | 3 + 3 | 2 | [1, 2, 3] | 2 + 3 | 2 | [1, 2, 3] | 3 + 3 | 2 | [2, 3, 4, 5] | 2 + 3 | 2 | [2, 3, 4, 5] | 3 + 3 | 2 | [2, 3, 4, 5] | 4 + 3 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [1, 2, 3] | 2 + 4 | 2 | [1, 2, 3] | 3 + 4 | 2 | [2, 3, 4, 5] | 2 + 4 | 2 | [2, 3, 4, 5] | 3 + 4 | 2 | [2, 3, 4, 5] | 4 + 4 | 2 | [2, 3, 4, 5] | 5 + 4 | 2 | [3, 4, 5, 6] | 3 + 4 | 2 | [3, 4, 5, 6] | 4 + 4 | 2 | [3, 4, 5, 6] | 5 + 4 | 2 | [3, 4, 5, 6] | 6 + 2 | 3 | [1, 2, 3] | 3 + 3 | 3 | [1, 2, 3] | 3 + 3 | 3 | [2, 3, 4, 5] | 3 + 3 | 3 | [2, 3, 4, 5] | 4 + 3 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [1, 2, 3] | 3 + 4 | 3 | [2, 3, 4, 5] | 3 + 4 | 3 | [2, 3, 4, 5] | 4 + 4 | 3 | [2, 3, 4, 5] | 5 + 4 | 3 | [3, 4, 5, 6] | 3 + 4 | 3 | [3, 4, 5, 6] | 4 + 4 | 3 | [3, 4, 5, 6] | 5 + 4 | 3 | [3, 4, 5, 6] | 6 +(52 rows) + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; +ERROR: could not find jsonpath variable "x" +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_exists +------------- + t +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_value +------------ + 123 +(1 row) + +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); + json_value +------------ + foo +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); + json_query +------------ + 123 +(1 row) + +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); + json_query +------------ + [123] +(1 row) + +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +ERROR: syntax error, unexpected IDENT_P at or near " " of jsonpath input +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); +ERROR: only string constants supported in JSON_TABLE path specification +LINE 1: SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || '... + ^ +-- Test parallel JSON_VALUE() +CREATE TABLE test_parallel_jsonb_value AS +SELECT i::text::jsonb AS js +FROM generate_series(1, 1000000) i; +-- Should be non-parallel due to subtransactions +EXPLAIN (COSTS OFF) +SELECT sum(JSON_VALUE(js, '$' RETURNING numeric)) FROM test_parallel_jsonb_value; + QUERY PLAN +--------------------------------------------- + Aggregate + -> Seq Scan on test_parallel_jsonb_value +(2 rows) + +SELECT sum(JSON_VALUE(js, '$' RETURNING numeric)) FROM test_parallel_jsonb_value; + sum +-------------- + 500000500000 +(1 row) + +-- Should be parallel +EXPLAIN (COSTS OFF) +SELECT sum(JSON_VALUE(js, '$' RETURNING numeric ERROR ON ERROR)) FROM test_parallel_jsonb_value; + QUERY PLAN +------------------------------------------------------------------ + Finalize Aggregate + -> Gather + Workers Planned: 2 + -> Partial Aggregate + -> Parallel Seq Scan on test_parallel_jsonb_value +(5 rows) + +SELECT sum(JSON_VALUE(js, '$' RETURNING numeric ERROR ON ERROR)) FROM test_parallel_jsonb_value; + sum +-------------- + 500000500000 +(1 row) + diff --git a/src/test/regress/sql/json_sqljson.sql b/src/test/regress/sql/json_sqljson.sql index 4f30fa46b9..df4a430d88 100644 --- a/src/test/regress/sql/json_sqljson.sql +++ b/src/test/regress/sql/json_sqljson.sql @@ -9,3 +9,7 @@ SELECT JSON_VALUE(NULL FORMAT JSON, '$'); -- JSON_QUERY SELECT JSON_QUERY(NULL FORMAT JSON, '$'); + +-- JSON_TABLE + +SELECT * FROM JSON_TABLE(NULL FORMAT JSON, '$' COLUMNS (foo text)); diff --git a/src/test/regress/sql/jsonb_sqljson.sql b/src/test/regress/sql/jsonb_sqljson.sql index 697b8ed126..62236c9fb1 100644 --- a/src/test/regress/sql/jsonb_sqljson.sql +++ b/src/test/regress/sql/jsonb_sqljson.sql @@ -319,3 +319,287 @@ CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime() CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, 0 to $.a ? (@.datetime() == $x)]' PASSING '12:34'::time AS x)); CREATE INDEX ON test_jsonb_mutability (JSON_QUERY(js, '$[1, $.a ? (@.datetime("HH:MI") == $x)]' PASSING '12:34'::time AS x)); DROP TABLE test_jsonb_mutability; + +-- JSON_TABLE + +-- Should fail (JSON_TABLE can be used only in FROM clause) +SELECT JSON_TABLE('[]', '$'); + +-- Should fail (no columns) +SELECT * FROM JSON_TABLE(NULL, '$' COLUMNS ()); + +-- NULL => empty table +SELECT * FROM JSON_TABLE(NULL::jsonb, '$' COLUMNS (foo int)) bar; + +-- +SELECT * FROM JSON_TABLE(jsonb '123', '$' + COLUMNS (item int PATH '$', foo int)) bar; + +-- JSON_TABLE: basic functionality +CREATE DOMAIN jsonb_test_domain AS text CHECK (value <> 'foo'); + +SELECT * +FROM + (VALUES + ('1'), + ('[]'), + ('{}'), + ('[1, 1.23, "2", "aaaaaaa", "foo", null, false, true, {"aaa": 123}, "[1,2]", "\"str\""]') + ) vals(js) + LEFT OUTER JOIN +-- JSON_TABLE is implicitly lateral + JSON_TABLE( + vals.js::jsonb, 'lax $[*]' + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa', + exists3 int EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + exists4 text EXISTS PATH 'strict $.aaa' FALSE ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$' + ) + ) jt + ON true; + +-- JSON_TABLE: Test backward parsing + +CREATE VIEW jsonb_table_view AS +SELECT * FROM + JSON_TABLE( + jsonb 'null', 'lax $[*]' PASSING 1 + 2 AS a, json '"foo"' AS "b c" + COLUMNS ( + id FOR ORDINALITY, + id2 FOR ORDINALITY, -- allowed additional ordinality columns + "int" int PATH '$', + "text" text PATH '$', + "char(4)" char(4) PATH '$', + "bool" bool PATH '$', + "numeric" numeric PATH '$', + "domain" jsonb_test_domain PATH '$', + js json PATH '$', + jb jsonb PATH '$', + jst text FORMAT JSON PATH '$', + jsc char(4) FORMAT JSON PATH '$', + jsv varchar(4) FORMAT JSON PATH '$', + jsb jsonb FORMAT JSON PATH '$', + jsbq jsonb FORMAT JSON PATH '$' OMIT QUOTES, + aaa int, -- implicit path '$."aaa"', + aaa1 int PATH '$.aaa', + exists1 bool EXISTS PATH '$.aaa', + exists2 int EXISTS PATH '$.aaa' TRUE ON ERROR, + exists3 text EXISTS PATH 'strict $.aaa' UNKNOWN ON ERROR, + + js2 json PATH '$', + jsb2w jsonb PATH '$' WITH WRAPPER, + jsb2q jsonb PATH '$' OMIT QUOTES, + ia int[] PATH '$', + ta text[] PATH '$', + jba jsonb[] PATH '$', + + NESTED PATH '$[1]' COLUMNS ( + a1 int, + NESTED PATH '$[*]' COLUMNS ( + a11 text + ), + b1 text + ), + NESTED PATH '$[2]' COLUMNS ( + NESTED PATH '$[*]' COLUMNS ( + a21 text + ), + NESTED PATH '$[*]' COLUMNS ( + a22 text + ) + ) + ) + ); + +\sv jsonb_table_view + +EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM jsonb_table_view; + +DROP VIEW jsonb_table_view; +DROP DOMAIN jsonb_test_domain; + +-- JSON_TABLE: ON EMPTY/ON ERROR behavior +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js), + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$')) jt; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$') ERROR ON ERROR) jt + ON true; + +SELECT * +FROM + (VALUES ('1'), ('"err"')) vals(js) + LEFT OUTER JOIN + JSON_TABLE(vals.js::jsonb, '$' COLUMNS (a int PATH '$' ERROR ON ERROR)) jt + ON true; + +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH '$.a' ERROR ON EMPTY)) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'strict $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; +SELECT * FROM JSON_TABLE(jsonb '1', '$' COLUMNS (a int PATH 'lax $.a' ERROR ON EMPTY) ERROR ON ERROR) jt; + +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH '$' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'strict $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int PATH 'lax $.a' DEFAULT 1 ON EMPTY DEFAULT 2 ON ERROR)) jt; + +-- JSON_TABLE: EXISTS PATH types +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int2 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a int8 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a float4 EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a char(3) EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a json EXISTS PATH '$.a')); +SELECT * FROM JSON_TABLE(jsonb '"a"', '$' COLUMNS (a jsonb EXISTS PATH '$.a')); + +-- JSON_TABLE: nested paths and plans + +-- Should fail (column names must be distinct) +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + a int, + b text, + a jsonb + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + b int, + NESTED PATH '$' + COLUMNS ( + c int, + b text + ) + ) +) jt; + +SELECT * FROM JSON_TABLE( + jsonb '[]', '$' + COLUMNS ( + NESTED PATH '$' + COLUMNS ( + b int + ), + NESTED PATH '$' + COLUMNS ( + NESTED PATH '$' + COLUMNS ( + c int, + b text + ) + ) + ) +) jt; + +-- JSON_TABLE: plan execution + +CREATE TEMP TABLE jsonb_table_test (js jsonb); + +INSERT INTO jsonb_table_test +VALUES ( + '[ + {"a": 1, "b": [], "c": []}, + {"a": 2, "b": [1, 2, 3], "c": [10, null, 20]}, + {"a": 3, "b": [1, 2], "c": []}, + {"x": "4", "b": [1, 2], "c": 123} + ]' +); + +-- unspecified 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 '$' ) + ) + ) jt; + +-- Should succeed (JSON arguments are passed to root and nested paths) +SELECT * +FROM + generate_series(1, 4) x, + generate_series(1, 3) y, + JSON_TABLE(jsonb + '[[1,2,3],[2,3,4,5],[3,4,5,6]]', + 'strict $[*] ? (@[*] < $x)' + PASSING x AS x, y AS y + COLUMNS ( + y text FORMAT JSON PATH '$', + NESTED PATH 'strict $[*] ? (@ >= $y)' + COLUMNS ( + z int PATH '$' + ) + ) + ) jt; + +-- Should fail (JSON arguments are not passed to column paths) +SELECT * +FROM JSON_TABLE( + jsonb '[1,2,3]', + '$[*] ? (@ < $x)' + PASSING 10 AS x + COLUMNS (y text FORMAT JSON PATH '$ ? (@ < $x)') + ) jt; + +-- Extension: non-constant JSON path +SELECT JSON_EXISTS(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_VALUE(jsonb '{"a": 123}', '$' || '.' || 'b' DEFAULT 'foo' ON EMPTY); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a'); +SELECT JSON_QUERY(jsonb '{"a": 123}', '$' || '.' || 'a' WITH WRAPPER); +-- Should fail (invalid path) +SELECT JSON_QUERY(jsonb '{"a": 123}', 'error' || ' ' || 'error'); +-- Should fail (not supported) +SELECT * FROM JSON_TABLE(jsonb '{"a": 123}', '$' || '.' || 'a' COLUMNS (foo int)); + +-- Test parallel JSON_VALUE() +CREATE TABLE test_parallel_jsonb_value AS +SELECT i::text::jsonb AS js +FROM generate_series(1, 1000000) i; + +-- Should be non-parallel due to subtransactions +EXPLAIN (COSTS OFF) +SELECT sum(JSON_VALUE(js, '$' RETURNING numeric)) FROM test_parallel_jsonb_value; +SELECT sum(JSON_VALUE(js, '$' RETURNING numeric)) FROM test_parallel_jsonb_value; + +-- Should be parallel +EXPLAIN (COSTS OFF) +SELECT sum(JSON_VALUE(js, '$' RETURNING numeric ERROR ON ERROR)) FROM test_parallel_jsonb_value; +SELECT sum(JSON_VALUE(js, '$' RETURNING numeric ERROR ON ERROR)) FROM test_parallel_jsonb_value; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 72fafb795b..4b12c575ab 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1238,6 +1238,14 @@ JsonPathParseResult JsonPathPredicateCallback JsonPathString JsonSemAction +JsonTable +JsonTableColumn +JsonTableColumnType +JsonTableContext +JsonTableJoinState +JsonTableParent +JsonTableScanState +JsonTableSibling JsonTokenType JsonTransformStringValuesAction JsonTypeCategory @@ -2645,6 +2653,7 @@ TableFunc TableFuncRoutine TableFuncScan TableFuncScanState +TableFuncType TableInfo TableLikeClause TableSampleClause