From df510efe85a56c14d575ce526023aeb4468438ac Mon Sep 17 00:00:00 2001 From: Srinath Reddy Sadipiralla Date: Thu, 10 Sep 2026 23:19:28 +0530 Subject: [PATCH v4 5/5] SQL/JSON: support "= PATH " source in JSON_TRANSFORM INSERT and REPLACE previously accepted only a value expression as their source. Per SQL Standard, the source may instead be "PATH ", in which case the value to insert or replace is produced at run time by evaluating that jsonpath against the input document, the same way JSON_QUERY without a wrapper does. This also activates the ON EMPTY and ON ERROR behavior clauses, which earlier revisions parsed but rejected because they are meaningful only for a PATH-valued source. Following the standard's General Rules: - a source path that yields no items triggers ON EMPTY, defaulting to NULL ON EMPTY; - a source path that yields more than one item, or whose evaluation raises a structural error, triggers ON ERROR, defaulting to ERROR ON ERROR. This path uses soft-error handling so that the IGNORE and NULL behaviors return a value instead of throwing. The target path and the PATH source are both coerced to jsonpath during parse analysis. The source value is derived during execution in ExecEvalJsonTransform. --- src/backend/executor/execExpr.c | 21 +++++++- src/backend/executor/execExprInterp.c | 62 ++++++++++++++++++++++- src/backend/nodes/nodeFuncs.c | 3 ++ src/backend/optimizer/util/clauses.c | 18 +++++++ src/backend/parser/gram.y | 38 ++++++++++++++ src/backend/parser/parse_expr.c | 72 +++++++++++++++++++++++---- src/backend/utils/adt/ruleutils.c | 15 +++++- src/include/nodes/execnodes.h | 6 ++- src/include/nodes/primnodes.h | 18 ++++++- 9 files changed, 235 insertions(+), 18 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 37ac7e144fa..4a977d9a05a 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -5111,9 +5111,26 @@ ExecInitJsonTransformExpr(JsonExpr *jsexpr, ExprState *state, ExprEvalPushStep(state, scratch); /* - * Evaluate the action's value_expr, if any. REMOVE has no value. + * Evaluate the action's source. For the "= PATH " form we + * compile the source jsonpath; ExecEvalJsonTransform runs it against the + * input document to derive the value. Otherwise we compile the value + * expression (RENAME's new key name, or INSERT/REPLACE's value). REMOVE + * has neither. */ - if (action->value_expr != NULL) + if (action->value_is_path) + { + ExecInitExprRec((Expr *) action->source_pathspec, state, + &jtstate->source_pathspec.value, + &jtstate->source_pathspec.isnull); + + /* JUMP to return-NULL landing pad if the source path is NULL */ + jumps_return_null = lappend_int(jumps_return_null, state->steps_len); + scratch->opcode = EEOP_JUMP_IF_NULL; + scratch->resnull = &jtstate->source_pathspec.isnull; + scratch->d.jump.jumpdone = -1; /* patched below */ + ExprEvalPushStep(state, scratch); + } + else if (action->value_expr != NULL) { ExecInitExprRec((Expr *) action->value_expr, state, &jtstate->action_value.value, diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 4c38eb219ad..5d342f6e5f9 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -5530,7 +5530,67 @@ ExecEvalJsonTransform(ExprState *state, ExprEvalStep *op, /* Build the value the action needs, honoring ON NULL for INSERT/REPLACE. */ if (action->op == TRANSFORM_INSERT || action->op == TRANSFORM_REPLACE) { - if (jtstate->action_value.isnull) + if (action->value_is_path) + { + /* + * "= PATH " source : derive the value by running + * the source jsonpath against the input document with the + * PASSING variables, exactly as JSON_QUERY WITHOUT WRAPPER + * does. A sequence of more than one item is an error; zero + * items triggers ON EMPTY; an evaluation error triggers + * ON ERROR. ON NULL does not apply to a PATH source. + */ + JsonPath *jp_src = DatumGetJsonPathP(jtstate->source_pathspec.value); + bool throw_error = (action->on_error == JSON_TRANSFORM_BEHAVIOR_ERROR); + bool src_empty = false; + bool src_error = false; + Datum srcval; + + srcval = JsonPathQuery(jtstate->formatted_expr.value, jp_src, + JSW_NONE, &src_empty, + throw_error ? NULL : &src_error, + jtstate->args, NULL); + + if (src_error) + { + /* soft error (incl. >1 item); on_error is IGNORE or NULL */ + if (action->on_error == JSON_TRANSFORM_BEHAVIOR_IGNORE) + { + *op->resvalue = JsonbPGetDatum(in); + *op->resnull = false; + return; + } + /* NULL ON ERROR */ + newvalbuf.type = jbvNull; + newval = &newvalbuf; + } + else if (src_empty) + { + switch (action->on_empty) + { + case JSON_TRANSFORM_BEHAVIOR_ERROR: + ereport(ERROR, + errcode(ERRCODE_NO_SQL_JSON_ITEM), + errmsg("no SQL/JSON item found for the JSON_TRANSFORM source path")); + break; + case JSON_TRANSFORM_BEHAVIOR_IGNORE: + *op->resvalue = JsonbPGetDatum(in); + *op->resnull = false; + return; + default: + /* NULL ON EMPTY */ + newvalbuf.type = jbvNull; + newval = &newvalbuf; + break; + } + } + else + { + JsonbToJsonbValue(DatumGetJsonbP(srcval), &newvalbuf); + newval = &newvalbuf; + } + } + else if (jtstate->action_value.isnull) { switch (action->on_null) { diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 0cc15f54feb..6efd1669722 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -2395,6 +2395,8 @@ expression_tree_walker_impl(Node *node, return true; if (WALK(jexpr->action->value_expr)) return true; + if (WALK(jexpr->action->source_pathspec)) + return true; } } break; @@ -3474,6 +3476,7 @@ expression_tree_mutator_impl(Node *node, FLATCOPY(newact, jexpr->action, JsonTransformAction); MUTATE(newact->pathspec, jexpr->action->pathspec, Node *); MUTATE(newact->value_expr, jexpr->action->value_expr, Node *); + MUTATE(newact->source_pathspec, jexpr->action->source_pathspec, Node *); newnode->action = newact; } return (Node *) newnode; diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 59a4d418911..a05174b3c8b 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -453,6 +453,24 @@ contain_mutable_functions_walker(Node *node, void *context) Const *cnst; Node *path_spec; + /* + * An INSERT/REPLACE "= PATH " source is a jsonpath evaluated + * at run time against the input document; a non-constant or mutable + * one makes the whole expression mutable. + */ + if (jexpr->action != NULL && jexpr->action->source_pathspec != NULL) + { + if (!IsA(jexpr->action->source_pathspec, Const)) + return true; + + cnst = castNode(Const, jexpr->action->source_pathspec); + Assert(cnst->consttype == JSONPATHOID); + if (!cnst->constisnull && + jspIsMutable(DatumGetJsonPathP(cnst->constvalue), + jexpr->passing_names, jexpr->passing_values)) + return true; + } + if(jexpr->action) path_spec = jexpr->action->pathspec; else diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 81e408e5a26..416770dada8 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -912,6 +912,16 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); %nonassoc UNBOUNDED NESTED /* ideally would have same precedence as IDENT */ %nonassoc IDENT PARTITION RANGE ROWS GROUPS PRECEDING FOLLOWING CUBE ROLLUP SET KEYS OBJECT_P SCALAR TO USING VALUE_P WITH WITHOUT PATH +/* + * The string-literal token SCONST is given a precedence just above PATH so + * that, in the JSON_TRANSFORM INSERT/REPLACE source "= PATH Sconst", the + * parser shifts the string as the jsonpath source rather than reducing PATH + * as an unreserved keyword introducing a "path 'literal'" typed constant. + * This only affects that one construct: without it the sole shift/reduce + * conflicts involving SCONST are those two, so the assignment cannot mask any + * other ambiguity. + */ +%nonassoc SCONST /* * IGNORE is given a precedence so the shift/reduce conflict between window * null-treatment (IGNORE NULLS) and a JSON_TRANSFORM behavior clause @@ -17649,6 +17659,20 @@ json_transform_action: $$ = (Node *) n; } | + /* INSERT path_expr = PATH jsonpath_source */ + INSERT a_expr '=' PATH Sconst json_transform_behavior_list_opt + { + JsonTransformAction *n = makeNode(JsonTransformAction); + n->op = TRANSFORM_INSERT; + n->pathspec = $2; + n->value_is_path = true; + n->source_pathspec = makeStringConst($5, @5); + n->behaviors = $6; + n->location = @1; + + $$ = (Node *) n; + } + | RENAME a_expr '=' Sconst json_transform_behavior_list_opt { JsonTransformAction *n = makeNode(JsonTransformAction); @@ -17673,6 +17697,20 @@ json_transform_action: $$ = (Node *) n; } | + /* REPLACE path_expr = PATH jsonpath_source */ + REPLACE a_expr '=' PATH Sconst json_transform_behavior_list_opt + { + JsonTransformAction *n = makeNode(JsonTransformAction); + n->op = TRANSFORM_REPLACE; + n->pathspec = $2; + n->value_is_path = true; + n->source_pathspec = makeStringConst($5, @5); + n->behaviors = $6; + n->location = @1; + + $$ = (Node *) n; + } + | REMOVE a_expr json_transform_behavior_list_opt { JsonTransformAction *n = makeNode(JsonTransformAction); diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index 68fa4f6fa7a..d2abc066ec3 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4406,16 +4406,24 @@ resolveJsonTransformBehaviors(ParseState *pstate, { ListCell *lc; - /* Standard implicit defaults. */ + /* + * Standard implicit defaults. ON EMPTY / ON ERROR apply only + * to INSERT/REPLACE and are consulted only for a PATH-valued + * source; their defaults are NULL ON EMPTY and ERROR ON ERROR. + */ switch (action->op) { case TRANSFORM_INSERT: action->on_existing = JSON_TRANSFORM_BEHAVIOR_ERROR; action->on_null = JSON_TRANSFORM_BEHAVIOR_NULL; + action->on_empty = JSON_TRANSFORM_BEHAVIOR_NULL; + action->on_error = JSON_TRANSFORM_BEHAVIOR_ERROR; break; case TRANSFORM_REPLACE: action->on_missing = JSON_TRANSFORM_BEHAVIOR_IGNORE; action->on_null = JSON_TRANSFORM_BEHAVIOR_NULL; + action->on_empty = JSON_TRANSFORM_BEHAVIOR_NULL; + action->on_error = JSON_TRANSFORM_BEHAVIOR_ERROR; break; case TRANSFORM_REMOVE: case TRANSFORM_RENAME: @@ -4479,10 +4487,27 @@ resolveJsonTransformBehaviors(ParseState *pstate, case JSON_TRANSFORM_TARGET_EMPTY: case JSON_TRANSFORM_TARGET_ERROR: - ereport(ERROR, - errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("ON EMPTY and ON ERROR clauses are not yet supported in JSON_TRANSFORM"), - parser_errposition(pstate, clause->location)); + if (action->op != TRANSFORM_INSERT && + action->op != TRANSFORM_REPLACE) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("%s is only valid for JSON_TRANSFORM INSERT and REPLACE", + clause->target == JSON_TRANSFORM_TARGET_EMPTY ? + "ON EMPTY" : "ON ERROR"), + parser_errposition(pstate, clause->location)); + if (behavior != JSON_TRANSFORM_BEHAVIOR_ERROR && + behavior != JSON_TRANSFORM_BEHAVIOR_IGNORE && + behavior != JSON_TRANSFORM_BEHAVIOR_NULL) + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("%s behavior must be ERROR, IGNORE, or NULL", + clause->target == JSON_TRANSFORM_TARGET_EMPTY ? + "ON EMPTY" : "ON ERROR"), + parser_errposition(pstate, clause->location)); + if (clause->target == JSON_TRANSFORM_TARGET_EMPTY) + action->on_empty = behavior; + else + action->on_error = behavior; break; } } @@ -4730,11 +4755,38 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) { case TRANSFORM_INSERT: case TRANSFORM_REPLACE: - analyzed_jst_action->value_expr = transformJsonValueExpr(pstate, func_name, - (JsonValueExpr *) jst_action->value_expr, - default_format, - JSONBOID, - false); + if (jst_action->value_is_path) + { + /* + * "= PATH " source: the value to insert/replace + * is produced at run time by evaluating this jsonpath + * against the input document. Coerce it to jsonpath just + * like the target path. + */ + Node *src = transformExprRecurse(pstate, jst_action->source_pathspec); + Oid srctype = exprType(src); + int srcloc = exprLocation(src); + Node *coerced_src = coerce_to_target_type(pstate, src, srctype, + JSONPATHOID, -1, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + srcloc); + + if (coerced_src == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be of type %s, not of type %s", + "jsonpath", format_type_be(srctype)), + parser_errposition(pstate, srcloc))); + analyzed_jst_action->value_is_path = true; + analyzed_jst_action->source_pathspec = coerced_src; + } + else + analyzed_jst_action->value_expr = transformJsonValueExpr(pstate, func_name, + (JsonValueExpr *) jst_action->value_expr, + default_format, + JSONBOID, + false); break; case TRANSFORM_RENAME: { diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 5cd11dc170c..705af13bd00 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -10645,12 +10645,25 @@ get_rule_expr(Node *node, deparse_context *context, if(jexpr->action->op != TRANSFORM_REMOVE) { appendStringInfoString(buf, " = "); - get_rule_expr(jexpr->action->value_expr, context, showimplicit); + if (jexpr->action->value_is_path) + { + appendStringInfoString(buf, "PATH "); + get_json_path_spec(jexpr->action->source_pathspec, + context, showimplicit); + } + else + get_rule_expr(jexpr->action->value_expr, context, showimplicit); } get_json_transform_behavior(buf, jexpr->action->on_existing, "EXISTING"); get_json_transform_behavior(buf, jexpr->action->on_missing, "MISSING"); get_json_transform_behavior(buf, jexpr->action->on_null, "NULL"); + /* ON EMPTY / ON ERROR apply only to a PATH-valued source */ + if (jexpr->action->value_is_path) + { + get_json_transform_behavior(buf, jexpr->action->on_empty, "EMPTY"); + get_json_transform_behavior(buf, jexpr->action->on_error, "ERROR"); + } } else get_json_path_spec(jexpr->path_spec, context, showimplicit); diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index b292947c85b..fc27be61c93 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1101,9 +1101,11 @@ typedef struct JsonTransformExprState /* Runtime slots — filled by prior steps */ NullableDatum formatted_expr; /* input jsonb document */ - NullableDatum pathspec; /* compiled jsonpath Datum */ + NullableDatum pathspec; /* compiled target jsonpath Datum */ NullableDatum action_value; /* value to for transform ops (NULL for - * REMOVE) */ + * REMOVE, or when the source is a PATH) */ + NullableDatum source_pathspec; /* compiled source jsonpath Datum, for the + * INSERT/REPLACE "= PATH " form */ /* PASSING args (only used if jsonpath needs them) */ List *args; /* List of JsonPathVariable */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 95d963a8fad..90334c8f28d 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1747,14 +1747,28 @@ typedef struct JsonTransformAction { NodeTag type; JsonTransformOp op; - Node *pathspec; /* The JSON Path: '$.a' */ - Node *value_expr; + Node *pathspec; /* The target JSON Path: '$.a' */ + + /* + * The source of an INSERT/REPLACE value. Per SQL/JSON, the source is + * either a value expression (value_is_path = false; value_expr holds it) + * or "PATH " (value_is_path = true; source_pathspec holds the + * jsonpath, evaluated against the input document at run time). RENAME + * carries its new key name in value_expr; REMOVE has neither. + */ + bool value_is_path; /* is the source a "PATH "? */ + Node *value_expr; /* value source, or RENAME target name */ + Node *source_pathspec; /* jsonpath source (PATH form), else NULL */ + /* raw ON-clauses from the grammar (transient; resolved in analysis) */ List *behaviors; /* resolved behaviors (filled by parse analysis from defaults + clauses) */ JsonTransformBehavior on_existing; JsonTransformBehavior on_missing; JsonTransformBehavior on_null; + /* ON EMPTY / ON ERROR, consulted only for a PATH-valued source */ + JsonTransformBehavior on_empty; + JsonTransformBehavior on_error; ParseLoc location; /* token location, or -1 if unknown */ } JsonTransformAction; -- 2.43.0