/*------------------------------------------------------------------------- * * parse_jsontable.c * parsing of JSON_TABLE * * Portions Copyright (c) 1996-2025, 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_type.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/fmgrprotos.h" #include "utils/json.h" #include "utils/lsyscache.h" /* Context for transformJsonTableColumns() */ typedef struct JsonTableParseContext { ParseState *pstate; JsonTable *jt; TableFunc *tf; List *pathNames; /* list of all path and columns names */ int pathNameId; /* path name id counter */ } JsonTableParseContext; static JsonTablePlan *transformJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingArgs, JsonTablePathSpec *pathspec); static JsonTablePlan *transformJsonTableNestedColumns(JsonTableParseContext *cxt, List *passingArgs, List *columns); static JsonFuncExpr *transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, List *passingArgs); static bool isCompositeType(Oid typid); static JsonTablePlan *makeJsonTablePathScan(JsonTablePathSpec *pathspec, bool errorOnError, int colMin, int colMax, JsonTablePlan *childplan); static void CheckDuplicateColumnOrPathNames(JsonTableParseContext *cxt, List *columns); static bool LookupPathOrColumnName(JsonTableParseContext *cxt, char *name); static char *generateJsonTablePathName(JsonTableParseContext *cxt); static JsonTablePlan *makeJsonTableSiblingJoin(JsonTablePlan *lplan, JsonTablePlan *rplan); /* * transformJsonTable - * Transform a raw JsonTable into TableFunc * * Mainly, this transforms the JSON_TABLE() document-generating expression * (jt->context_item) and the column-generating expressions (jt->columns) to * populate TableFunc.docexpr and TableFunc.colvalexprs, respectively. Also, * the PASSING values (jt->passing) are transformed and added into * TableFunc.passingvalexprs. */ ParseNamespaceItem * transformJsonTable(ParseState *pstate, JsonTable *jt) { TableFunc *tf; JsonFuncExpr *jfe; JsonExpr *je; JsonTablePathSpec *rootPathSpec = jt->pathspec; bool is_lateral; JsonTableParseContext cxt = {pstate}; Assert(IsA(rootPathSpec->string, A_Const) && castNode(A_Const, rootPathSpec->string)->val.node.type == T_String); if (jt->on_error && jt->on_error->btype != JSON_BEHAVIOR_ERROR && jt->on_error->btype != JSON_BEHAVIOR_EMPTY && jt->on_error->btype != JSON_BEHAVIOR_EMPTY_ARRAY) ereport(ERROR, errcode(ERRCODE_SYNTAX_ERROR), errmsg("invalid %s behavior", "ON ERROR"), errdetail("Only EMPTY [ ARRAY ] or ERROR is allowed in the top-level ON ERROR clause."), parser_errposition(pstate, jt->on_error->location)); cxt.pathNameId = 0; if (rootPathSpec->name == NULL) rootPathSpec->name = generateJsonTablePathName(&cxt); cxt.pathNames = list_make1(rootPathSpec->name); CheckDuplicateColumnOrPathNames(&cxt, jt->columns); /* * 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 = makeNode(TableFunc); tf->functype = TFT_JSON_TABLE; /* * Transform JsonFuncExpr representing the top JSON_TABLE context_item and * pathspec into a dummy JSON_TABLE_OP JsonExpr. */ jfe = makeNode(JsonFuncExpr); jfe->op = JSON_TABLE_OP; jfe->context_item = jt->context_item; jfe->pathspec = (Node *) rootPathSpec->string; jfe->passing = jt->passing; jfe->on_empty = NULL; jfe->on_error = jt->on_error; jfe->location = jt->location; tf->docexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); /* * Create a JsonTablePlan that will generate row pattern that becomes * source data for JSON path expressions in jt->columns. This also adds * the columns' transformed JsonExpr nodes into tf->colvalexprs. */ cxt.jt = jt; cxt.tf = tf; tf->plan = (Node *) transformJsonTableColumns(&cxt, jt->columns, jt->passing, rootPathSpec); /* * Copy the transformed PASSING arguments into the TableFunc node, because * they are evaluated separately from the JsonExpr that we just put in * TableFunc.docexpr. JsonExpr.passing_values is still kept around for * get_json_table(). */ je = (JsonExpr *) tf->docexpr; tf->passingvalexprs = copyObject(je->passing_values); 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); } /* * Check if a column / path name is duplicated in the given shared list of * names. */ static void CheckDuplicateColumnOrPathNames(JsonTableParseContext *cxt, List *columns) { ListCell *lc1; foreach(lc1, columns) { JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc1)); if (jtc->coltype == JTC_NESTED) { if (jtc->pathspec->name) { if (LookupPathOrColumnName(cxt, jtc->pathspec->name)) ereport(ERROR, errcode(ERRCODE_DUPLICATE_ALIAS), errmsg("duplicate JSON_TABLE column or path name: %s", jtc->pathspec->name), parser_errposition(cxt->pstate, jtc->pathspec->name_location)); cxt->pathNames = lappend(cxt->pathNames, jtc->pathspec->name); } CheckDuplicateColumnOrPathNames(cxt, jtc->columns); } else { if (LookupPathOrColumnName(cxt, jtc->name)) ereport(ERROR, errcode(ERRCODE_DUPLICATE_ALIAS), errmsg("duplicate JSON_TABLE column or path name: %s", jtc->name), parser_errposition(cxt->pstate, jtc->location)); cxt->pathNames = lappend(cxt->pathNames, jtc->name); } } } /* * Lookup a column/path name in the given name list, returning true if already * there. */ static bool LookupPathOrColumnName(JsonTableParseContext *cxt, char *name) { ListCell *lc; foreach(lc, cxt->pathNames) { if (strcmp(name, (const char *) lfirst(lc)) == 0) return true; } return false; } /* Generate a new unique JSON_TABLE path name. */ static char * generateJsonTablePathName(JsonTableParseContext *cxt) { char namebuf[32]; char *name = namebuf; snprintf(namebuf, sizeof(namebuf), "json_table_path_%d", cxt->pathNameId++); name = pstrdup(name); cxt->pathNames = lappend(cxt->pathNames, name); return name; } /* * Create a JsonTablePlan that will supply the source row for 'columns' * using 'pathspec' and append the columns' transformed JsonExpr nodes and * their type/collation information to cxt->tf. */ static JsonTablePlan * transformJsonTableColumns(JsonTableParseContext *cxt, List *columns, List *passingArgs, JsonTablePathSpec *pathspec) { ParseState *pstate = cxt->pstate; JsonTable *jt = cxt->jt; TableFunc *tf = cxt->tf; ListCell *col; bool ordinality_found = false; bool errorOnError = jt->on_error && jt->on_error->btype == JSON_BEHAVIOR_ERROR; Oid contextItemTypid = exprType(tf->docexpr); int colMin, colMax; JsonTablePlan *childplan; /* Start of column range */ colMin = list_length(tf->colvalexprs); foreach(col, columns) { JsonTableColumn *rawc = castNode(JsonTableColumn, lfirst(col)); Oid typid; int32 typmod; Oid typcoll = InvalidOid; Node *colexpr; if (rawc->coltype != JTC_NESTED) { Assert(rawc->name); 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: if (ordinality_found) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("only one FOR ORDINALITY column is allowed"), parser_errposition(pstate, rawc->location))); ordinality_found = true; colexpr = NULL; typid = INT4OID; typmod = -1; break; case JTC_REGULAR: typenameTypeIdAndMod(pstate, rawc->typeName, &typid, &typmod); /* * Use JTC_FORMATTED so as to use JSON_QUERY for this column * if the specified type is one that's better handled using * JSON_QUERY() or if non-default WRAPPER or QUOTES behavior * is specified. */ if (isCompositeType(typid) || rawc->quotes != JS_QUOTES_UNSPEC || rawc->wrapper != JSW_UNSPEC) rawc->coltype = JTC_FORMATTED; /* FALLTHROUGH */ case JTC_FORMATTED: case JTC_EXISTS: { JsonFuncExpr *jfe; CaseTestExpr *param = makeNode(CaseTestExpr); param->collation = InvalidOid; param->typeId = contextItemTypid; param->typeMod = -1; jfe = transformJsonTableColumn(rawc, (Node *) param, passingArgs); colexpr = transformExpr(pstate, (Node *) jfe, EXPR_KIND_FROM_FUNCTION); assign_expr_collations(pstate, colexpr); typid = exprType(colexpr); typmod = exprTypmod(colexpr); typcoll = exprCollation(colexpr); break; } case JTC_NESTED: continue; default: elog(ERROR, "unknown JSON_TABLE column type: %d", (int) rawc->coltype); break; } tf->coltypes = lappend_oid(tf->coltypes, typid); tf->coltypmods = lappend_int(tf->coltypmods, typmod); tf->colcollations = lappend_oid(tf->colcollations, typcoll); tf->colvalexprs = lappend(tf->colvalexprs, colexpr); } /* End of column range. */ if (list_length(tf->colvalexprs) == colMin) { /* No columns in this Scan beside the nested ones. */ colMax = colMin = -1; } else colMax = list_length(tf->colvalexprs) - 1; /* Recursively transform nested columns */ childplan = transformJsonTableNestedColumns(cxt, passingArgs, columns); /* Create a "parent" scan responsible for all columns handled above. */ return makeJsonTablePathScan(pathspec, errorOnError, colMin, colMax, childplan); } /* * Check if the type is "composite" for the purpose of checking whether to use * JSON_VALUE() or JSON_QUERY() for a given JsonTableColumn. */ static bool isCompositeType(Oid typid) { char typtype = get_typtype(typid); return typid == JSONOID || typid == JSONBOID || typid == RECORDOID || type_is_array(typid) || typtype == TYPTYPE_COMPOSITE || /* domain over one of the above? */ (typtype == TYPTYPE_DOMAIN && isCompositeType(getBaseType(typid))); } /* * Transform JSON_TABLE column definition into a JsonFuncExpr * This turns: * - regular column into JSON_VALUE() * - FORMAT JSON column into JSON_QUERY() * - EXISTS column into JSON_EXISTS() */ static JsonFuncExpr * transformJsonTableColumn(JsonTableColumn *jtc, Node *contextItemExpr, List *passingArgs) { Node *pathspec; JsonFuncExpr *jfexpr = makeNode(JsonFuncExpr); if (jtc->coltype == JTC_REGULAR) jfexpr->op = JSON_VALUE_OP; else if (jtc->coltype == JTC_EXISTS) jfexpr->op = JSON_EXISTS_OP; else jfexpr->op = JSON_QUERY_OP; /* Pass the column name so any runtime JsonExpr errors can print it. */ Assert(jtc->name != NULL); jfexpr->column_name = pstrdup(jtc->name); jfexpr->context_item = makeJsonValueExpr((Expr *) contextItemExpr, NULL, makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1)); if (jtc->pathspec) pathspec = (Node *) jtc->pathspec->string; else { /* Construct default path as '$."column_name"' */ StringInfoData path; initStringInfo(&path); appendStringInfoString(&path, "$."); escape_json(&path, jtc->name); pathspec = makeStringConst(path.data, -1); } jfexpr->pathspec = pathspec; jfexpr->passing = passingArgs; jfexpr->output = makeNode(JsonOutput); jfexpr->output->typeName = jtc->typeName; jfexpr->output->returning = makeNode(JsonReturning); jfexpr->output->returning->format = jtc->format; jfexpr->on_empty = jtc->on_empty; jfexpr->on_error = jtc->on_error; jfexpr->quotes = jtc->quotes; jfexpr->wrapper = jtc->wrapper; jfexpr->location = jtc->location; return jfexpr; } /* * Recursively transform nested columns and create child plan(s) that will be * used to evaluate their row patterns. */ static JsonTablePlan * transformJsonTableNestedColumns(JsonTableParseContext *cxt, List *passingArgs, List *columns) { JsonTablePlan *plan = NULL; ListCell *lc; /* * If there are multiple NESTED COLUMNS clauses in 'columns', their * respective plans will be combined using a "sibling join" plan, which * effectively does a UNION of the sets of rows coming from each nested * plan. */ foreach(lc, columns) { JsonTableColumn *jtc = castNode(JsonTableColumn, lfirst(lc)); JsonTablePlan *nested; if (jtc->coltype != JTC_NESTED) continue; if (jtc->pathspec->name == NULL) jtc->pathspec->name = generateJsonTablePathName(cxt); nested = transformJsonTableColumns(cxt, jtc->columns, passingArgs, jtc->pathspec); if (plan) plan = makeJsonTableSiblingJoin(plan, nested); else plan = nested; } return plan; } /* * Create a JsonTablePlan for given path and ON ERROR behavior. * * colMin and colMin give the range of columns computed by this scan in the * global flat list of column expressions that will be passed to the * JSON_TABLE's TableFunc. Both are -1 when all of columns are nested and * thus computed by 'childplan'. */ static JsonTablePlan * makeJsonTablePathScan(JsonTablePathSpec *pathspec, bool errorOnError, int colMin, int colMax, JsonTablePlan *childplan) { JsonTablePathScan *scan = makeNode(JsonTablePathScan); char *pathstring; Const *value; Assert(IsA(pathspec->string, A_Const)); pathstring = castNode(A_Const, pathspec->string)->val.sval.sval; value = makeConst(JSONPATHOID, -1, InvalidOid, -1, DirectFunctionCall1(jsonpath_in, CStringGetDatum(pathstring)), false, false); scan->plan.type = T_JsonTablePathScan; scan->path = makeJsonTablePath(value, pathspec->name); scan->errorOnError = errorOnError; scan->child = childplan; scan->colMin = colMin; scan->colMax = colMax; return (JsonTablePlan *) scan; } /* * Create a JsonTablePlan that will perform a join of the rows coming from * 'lplan' and 'rplan'. * * The default way of "joining" the rows is to perform a UNION between the * sets of rows from 'lplan' and 'rplan'. */ static JsonTablePlan * makeJsonTableSiblingJoin(JsonTablePlan *lplan, JsonTablePlan *rplan) { JsonTableSiblingJoin *join = makeNode(JsonTableSiblingJoin); join->plan.type = T_JsonTableSiblingJoin; join->lplan = lplan; join->rplan = rplan; return (JsonTablePlan *) join; }