From 6a2e6ebf080365f48e99f389dda9e3d40b121112 Mon Sep 17 00:00:00 2001 From: Srinath Reddy Sadipiralla Date: Fri, 24 Apr 2026 23:06:35 +0530 Subject: [PATCH 1/1] SQL/JSON: Add initial JSON_TRANSFORM implementation This patch introduces JSON_TRANSFORM(), a SQL/JSON function that yields a new JSON value by applying a modification to an input JSON value. --- src/backend/executor/execExpr.c | 159 ++++++++++++++++++++++ src/backend/executor/execExprInterp.c | 187 ++++++++++++++++++++++++++ src/backend/parser/gram.y | 68 +++++++++- src/backend/parser/parse_expr.c | 108 ++++++++++++--- src/backend/parser/parse_target.c | 3 + src/include/executor/execExpr.h | 9 ++ src/include/nodes/execnodes.h | 14 ++ src/include/nodes/parsenodes.h | 1 + src/include/nodes/primnodes.h | 20 +++ src/include/parser/kwlist.h | 2 + 10 files changed, 552 insertions(+), 19 deletions(-) diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index 77229141b38..0d14fc8c474 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -99,6 +99,9 @@ static void ExecBuildAggTransCall(ExprState *state, AggState *aggstate, static void ExecInitJsonExpr(JsonExpr *jsexpr, ExprState *state, Datum *resv, bool *resnull, ExprEvalStep *scratch); +static void ExecInitJsonTransformExpr(JsonExpr *jsexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch); static void ExecInitJsonCoercion(ExprState *state, JsonReturning *returning, ErrorSaveContext *escontext, bool omit_quotes, bool exists_coerce, @@ -2525,6 +2528,8 @@ ExecInitExprRec(Expr *node, ExprState *state, if (jsexpr->op == JSON_TABLE_OP) ExecInitExprRec((Expr *) jsexpr->formatted_expr, state, resv, resnull); + else if (jsexpr->op == JSON_TRANSFORM_OP) + ExecInitJsonTransformExpr(jsexpr, state, resv, resnull, &scratch); else ExecInitJsonExpr(jsexpr, state, resv, resnull, &scratch); break; @@ -5071,6 +5076,160 @@ ExecInitJsonExpr(JsonExpr *jsexpr, ExprState *state, jsestate->jump_end = state->steps_len; } +/* + * Compile a JSON_TRANSFORM expression into a sequence of expression-eval + * steps. + * + * JSON_TRANSFORM is fundamentally different from JSON_QUERY/VALUE/EXISTS: + * those ops query a document, we mutate one. So we don't reuse + * ExecInitJsonExpr; we emit our own step sequence tailored to the mutation + * pipeline. + * + * For now, a single action is attached to the JsonExpr. The step layout is: + * + * steps[..] evaluate formatted_expr (input doc) -> jtstate->formatted_expr + * steps[K] EEOP_JUMP_IF_NULL (if doc is NULL, jump to M) + * steps[..] evaluate action->pathspec -> jtstate->pathspec + * steps[L] EEOP_JUMP_IF_NULL (if path is NULL, jump to M) + * steps[..] evaluate action->value_expr (if any) -> jtstate->action_value + * steps[..] evaluate PASSING args (currently unused by mutation, but + * we evaluate them for parity with JSON_QUERY and for future use) + * steps[P] EEOP_JSON_TRANSFORM (handler: do the mutation) + * steps[M] EEOP_CONST(NULL) (return-null landing pad) + * + */ +static void +ExecInitJsonTransformExpr(JsonExpr *jsexpr, ExprState *state, + Datum *resv, bool *resnull, + ExprEvalStep *scratch) +{ + JsonTransformExprState *jtstate = palloc0(sizeof(JsonTransformExprState)); + JsonTransformAction *action = jsexpr->action; + List *jumps_return_null = NIL; + ListCell *argexprlc; + ListCell *argnamelc; + ListCell *lc; + + Assert(action != NULL); + + jtstate->jsexpr = jsexpr; + + /* + * Evaluate formatted_expr storing the result into + * jtstate->formatted_expr. + */ + ExecInitExprRec((Expr *) jsexpr->formatted_expr, state, + &jtstate->formatted_expr.value, + &jtstate->formatted_expr.isnull); + + /* JUMP to return-NULL landing pad if formatted_expr is NULL */ + jumps_return_null = lappend_int(jumps_return_null, state->steps_len); + scratch->opcode = EEOP_JUMP_IF_NULL; + scratch->resnull = &jtstate->formatted_expr.isnull; + scratch->d.jump.jumpdone = -1; /* patched below */ + ExprEvalPushStep(state, scratch); + + /* + * Evaluate the action's pathspec (a compiled jsonpath Const) into + * jtstate->pathspec. + */ + ExecInitExprRec((Expr *) action->pathspec, state, + &jtstate->pathspec.value, + &jtstate->pathspec.isnull); + + /* JUMP to return-NULL landing pad if pathspec is NULL */ + jumps_return_null = lappend_int(jumps_return_null, state->steps_len); + scratch->opcode = EEOP_JUMP_IF_NULL; + scratch->resnull = &jtstate->pathspec.isnull; + scratch->d.jump.jumpdone = -1; /* patched below */ + ExprEvalPushStep(state, scratch); + + /* + * Evaluate the action's value_expr, if any. REMOVE has no value. + */ + if (action->value_expr != NULL) + { + ExecInitExprRec((Expr *) action->value_expr, state, + &jtstate->action_value.value, + &jtstate->action_value.isnull); + } + + /* + * Steps to compute PASSING args. These don't feed the mutation functions + * directly, but the jsonpath engine may reference them if in future we + * switch to native jsonpath-aware mutation. For now we evaluate but don't + * use them; kept for forward compatibility and for parity with how + * JSON_QUERY handles PASSING. + */ + jtstate->args = NIL; + forboth(argexprlc, jsexpr->passing_values, + argnamelc, jsexpr->passing_names) + { + Expr *argexpr = (Expr *) lfirst(argexprlc); + String *argname = lfirst_node(String, argnamelc); + JsonPathVariable *var = palloc(sizeof(*var)); + + var->name = argname->sval; + var->namelen = strlen(var->name); + var->typid = exprType((Node *) argexpr); + var->typmod = exprTypmod((Node *) argexpr); + + ExecInitExprRec((Expr *) argexpr, state, &var->value, &var->isnull); + + jtstate->args = lappend(jtstate->args, var); + } + + /* + * The main step: EEOP_JSON_TRANSFORM. Its handler branches on + * action->op, converts the jsonpath to a text[] path, and calls the + * appropriate jsonb mutation function, writing the result to resv. + */ + scratch->opcode = EEOP_JSON_TRANSFORM; + scratch->resvalue = resv; + scratch->resnull = resnull; + scratch->d.json_transform.jtstate = jtstate; + ExprEvalPushStep(state, scratch); + + /* + * Unconditional JUMP over the NULL landing pad below. Without this, a + * successful main step would fall through into EEOP_CONST(NULL) and have + * its result overwritten with NULL. + */ + { + int jump_past_null = state->steps_len; + + scratch->opcode = EEOP_JUMP; + scratch->d.jump.jumpdone = -1; /* patched below */ + ExprEvalPushStep(state, scratch); + + /* + * Patch the JUMP_IF_NULL placeholders to land on the NULL pad we are + * about to emit. + */ + foreach(lc, jumps_return_null) + { + ExprEvalStep *as = &state->steps[lfirst_int(lc)]; + + as->d.jump.jumpdone = state->steps_len; + } + + /* Return-NULL landing pad */ + scratch->opcode = EEOP_CONST; + scratch->resvalue = resv; + scratch->resnull = resnull; + scratch->d.constval.value = (Datum) 0; + scratch->d.constval.isnull = true; + ExprEvalPushStep(state, scratch); + + /* + * Patch the unconditional JUMP to land just past the NULL pad. + * Success path (via JUMP) and null path (via fallthrough) converge + * here. + */ + state->steps[jump_past_null].d.jump.jumpdone = state->steps_len; + } +} + /* * Initialize a EEOP_JSONEXPR_COERCION step to coerce the value given in resv * to the given RETURNING type. diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c index 0634af964a9..a9a43caf2ae 100644 --- a/src/backend/executor/execExprInterp.c +++ b/src/backend/executor/execExprInterp.c @@ -579,6 +579,7 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) &&CASE_EEOP_JSON_CONSTRUCTOR, &&CASE_EEOP_IS_JSON, &&CASE_EEOP_JSONEXPR_PATH, + &&CASE_EEOP_JSON_TRANSFORM, &&CASE_EEOP_JSONEXPR_COERCION, &&CASE_EEOP_JSONEXPR_COERCION_FINISH, &&CASE_EEOP_AGGREF, @@ -1942,6 +1943,13 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull) EEO_JUMP(ExecEvalJsonExprPath(state, op, econtext)); } + EEO_CASE(EEOP_JSON_TRANSFORM) + { + /* too complex for an inline implementation */ + ExecEvalJsonTransform(state, op, econtext); + EEO_NEXT(); + } + EEO_CASE(EEOP_JSONEXPR_COERCION) { /* too complex for an inline implementation */ @@ -5102,6 +5110,185 @@ ExecEvalJsonExprPath(ExprState *state, ExprEvalStep *op, return jump_eval_coercion >= 0 ? jump_eval_coercion : jsestate->jump_end; } +/* Forward declarations for SQL-callable jsonb functions we invoke directly. */ +extern Datum jsonb_set(PG_FUNCTION_ARGS); +extern Datum jsonb_insert(PG_FUNCTION_ARGS); +extern Datum jsonb_delete_path(PG_FUNCTION_ARGS); + +/* + * Convert a simple jsonpath (chain of jpiRoot + jpiKey items only) into a + * text[] Datum suitable for passing to jsonb_set / jsonb_insert / + * jsonb_delete_path. + * + * If the jsonpath contains any item type other than jpiRoot/jpiKey (e.g., + * jpiAnyKey, jpiIndexArray, jpiFilter), raise an ereport. The JSON_TRANSFORM + * spec restricts target paths to member accessors, but because the jsonpath + * type is general-purpose we must enforce the restriction here. For now we additionally disallow jpiAnyKey (wildcard '.*') + * since the text[] API can't express it. + */ +static Datum +JsonPathToTextArray(JsonPath *jp) +{ + JsonPathItem v; + ArrayBuildState *astate; + MemoryContext curctx = CurrentMemoryContext; + + astate = initArrayResult(TEXTOID, curctx, false); + + jspInit(&v, jp); + + /* Per spec, the path must begin with '$'. */ + if (v.type != jpiRoot) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TRANSFORM target path must start with the context variable $, not a named variable. The transformation applies to the input document.")); + + /* + * Walk the chain. Each subsequent item must be a jpiKey; anything else + * is rejected. + */ + while (jspHasNext(&v)) + { + JsonPathItem next; + char *name; + int32 namelen; + text *t; + + jspGetNext(&v, &next); + v = next; + + if (v.type != jpiKey) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TRANSFORM target path may only contain named member accessors"), + errdetail("Only '.key' accessors are supported now; wildcards, array subscripts, filters, and methods are not allowed.")); + + name = jspGetString(&v, &namelen); + t = cstring_to_text_with_len(name, namelen); + + astate = accumArrayResult(astate, + PointerGetDatum(t), + false, + TEXTOID, + curctx); + } + + /* + * At least one key must follow the root (otherwise the path is just '$' + * which has nothing to target). + */ + if (astate->nelems == 0) + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TRANSFORM target path must name at least one member")); + + return makeArrayResult(astate, curctx); +} + +/* + * Runtime handler for EEOP_JSON_TRANSFORM. + * + * By the time we get here, prior steps have populated: + * jtstate->formatted_expr — the input jsonb (guaranteed non-null; the + * EEOP_JUMP_IF_NULL before us handles null) + * jtstate->pathspec — the compiled jsonpath Datum (non-null) + * jtstate->action_value — the value for INSERT/REPLACE/RENAME + * (isnull=true for REMOVE, or if user passed NULL) + * + * We branch on action->op, convert the jsonpath to a text[], and delegate + * to the existing jsonb_set / jsonb_insert / jsonb_delete_path C functions. + * + * Note on behavior clauses: per spec 6.44, each action supports per-action + * ON EXISTING / ON MISSING / ON NULL / ON EMPTY / ON ERROR. This v1 + * implementation uses HARDCODED behavior — whatever jsonb_set/insert/delete + * do naturally: + * - REMOVE : no-op if path missing (matches spec default IGNORE ON MISSING) + * - INSERT : error if key exists (matches spec default ERROR ON EXISTING) + * - REPLACE : no-op if path missing (matches spec default IGNORE ON MISSING) + * - RENAME : not yet implemented; raises an error + * + * Per-action behavior clauses (e.g., IGNORE ON EXISTING, NULL ON NULL) are + * not yet supported. + */ +void +ExecEvalJsonTransform(ExprState *state, ExprEvalStep *op, + ExprContext *econtext) +{ + JsonTransformExprState *jtstate = op->d.json_transform.jtstate; + JsonExpr *jsexpr = jtstate->jsexpr; + JsonTransformAction *action = jsexpr->action; + Jsonb *in; + JsonPath *jp; + Datum path_array; + Datum result; + + /* + * The JUMP_IF_NULL guards in the step array already skip us if + * formatted_expr or pathspec is NULL. + */ + Assert(!jtstate->formatted_expr.isnull); + Assert(!jtstate->pathspec.isnull); + + in = DatumGetJsonbP(jtstate->formatted_expr.value); + jp = DatumGetJsonPathP(jtstate->pathspec.value); + + /* Validate + convert jsonpath to text[] */ + path_array = JsonPathToTextArray(jp); + + switch (action->op) + { + case TRANSFORM_REMOVE: + result = DirectFunctionCall2(jsonb_delete_path, + JsonbPGetDatum(in), + path_array); + break; + + case TRANSFORM_INSERT: + if (jtstate->action_value.isnull) + ereport(ERROR, + errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("NULL value for INSERT not allowed in JSON_TRANSFORM")); + /* insert_after=false means before (standard insert semantic) */ + result = DirectFunctionCall4(jsonb_insert, + JsonbPGetDatum(in), + path_array, + jtstate->action_value.value, + BoolGetDatum(false)); + break; + + case TRANSFORM_REPLACE: + if (jtstate->action_value.isnull) + ereport(ERROR, + errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), + errmsg("NULL value for REPLACE not allowed in JSON_TRANSFORM")); + /* create_missing=false: REPLACE is no-op if path missing */ + result = DirectFunctionCall4(jsonb_set, + JsonbPGetDatum(in), + path_array, + jtstate->action_value.value, + BoolGetDatum(false)); + break; + + case TRANSFORM_RENAME: + + /* + * RENAME operates on KEYS, not values. remove/insert/replace + * work on values at a given path, so RENAME isn't trivially + * expressible in terms of them. + */ + ereport(ERROR, + errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("JSON_TRANSFORM RENAME is not yet implemented")); + break; + + default: + elog(ERROR, "unrecognized JsonTransformOp: %d", (int) action->op); + } + + *op->resvalue = result; + *op->resnull = false; +} + /* * Convert the given JsonbValue to its C string representation * diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index ff4e1388c55..22281dc69c7 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -672,6 +672,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_table json_table_column_definition json_table_column_path_clause_opt + json_transform_action %type json_name_and_value_list json_value_expr_list json_array_aggregate_order_by_clause_opt @@ -781,7 +782,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_TABLE JSON_VALUE + JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_TRANSFORM JSON_VALUE KEEP KEY KEYS @@ -809,7 +810,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); QUOTE QUOTES RANGE READ REAL REASSIGN RECURSIVE REF_P REFERENCES REFERENCING - REFRESH REINDEX RELATIONSHIP RELATIVE_P RELEASE RENAME REPACK REPEATABLE REPLACE REPLICA + REFRESH REINDEX RELATIONSHIP RELATIVE_P RELEASE REMOVE RENAME REPACK REPEATABLE REPLACE REPLICA RESET RESPECT_P RESTART RESTRICT RETURN RETURNING RETURNS REVOKE RIGHT ROLE ROLLBACK ROLLUP ROUTINE ROUTINES ROW ROWS RULE @@ -17170,6 +17171,19 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_TRANSFORM '(' + json_value_expr ',' json_transform_action + json_passing_clause_opt + ')' + { + JsonFuncExpr *n = makeNode(JsonFuncExpr); + n->op = JSON_TRANSFORM_OP; + n->context_item = (JsonValueExpr *) $3; + n->action = $5; + n->passing = $6; + n->location = @1; + $$ = (Node *) n; + } ; @@ -18050,6 +18064,52 @@ json_returning_clause_opt: | /* EMPTY */ { $$ = NULL; } ; +json_transform_action: + /* INSERT path_expr = value_expr */ + INSERT a_expr '=' json_value_expr + { + JsonTransformAction *n = makeNode(JsonTransformAction); + n->op = TRANSFORM_INSERT; + n->pathspec = $2; + n->value_expr = $4; + n->location = @1; + + $$ = (Node *) n; + } + | + RENAME a_expr '=' Sconst + { + JsonTransformAction *n = makeNode(JsonTransformAction); + n->op = TRANSFORM_RENAME; + n->pathspec = $2; + n->value_expr = makeStringConst($4, @4); + n->location = @1; + + $$ = (Node *) n; + } + | + REPLACE a_expr '=' json_value_expr + { + JsonTransformAction *n = makeNode(JsonTransformAction); + n->op = TRANSFORM_REPLACE; + n->pathspec = $2; + n->value_expr = $4; + n->location = @1; + + $$ = (Node *) n; + } + | + REMOVE a_expr + { + JsonTransformAction *n = makeNode(JsonTransformAction); + n->op = TRANSFORM_REMOVE; + n->pathspec = $2; + n->value_expr = NULL; + n->location = @1; + + $$ = (Node *) n; + }; + /* * We must assign the only-JSON production a precedence less than IDENT in * order to favor shifting over reduction when JSON is followed by VALUE_P, @@ -19061,6 +19121,7 @@ unreserved_keyword: | RELATIONSHIP | RELATIVE_P | RELEASE + | REMOVE | RENAME | REPACK | REPEATABLE @@ -19209,6 +19270,7 @@ col_name_keyword: | JSON_SCALAR | JSON_SERIALIZE | JSON_TABLE + | JSON_TRANSFORM | JSON_VALUE | LEAST | MERGE_ACTION @@ -19584,6 +19646,7 @@ bare_label_keyword: | JSON_SCALAR | JSON_SERIALIZE | JSON_TABLE + | JSON_TRANSFORM | JSON_VALUE | KEEP | KEY @@ -19710,6 +19773,7 @@ bare_label_keyword: | RELATIONSHIP | RELATIVE_P | RELEASE + | REMOVE | RENAME | REPACK | REPEATABLE diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index f535f3b9351..47f8b54e995 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -4326,6 +4326,7 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) Node *coerced_path_spec; const char *func_name = NULL; JsonFormatType default_format; + JsonTransformAction *jst_action = func->action; switch (func->op) { @@ -4345,6 +4346,10 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) func_name = "JSON_TABLE"; default_format = JS_FORMAT_JSONB; break; + case JSON_TRANSFORM_OP: + func_name = "JSON_TRANSFORM"; + default_format = JS_FORMAT_JSONB; + break; default: elog(ERROR, "invalid JsonFuncExpr op %d", (int) func->op); default_format = JS_FORMAT_DEFAULT; /* keep compiler quiet */ @@ -4526,7 +4531,7 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->location = func->location; jsexpr->op = func->op; jsexpr->column_name = func->column_name; - + /* * jsonpath machinery can only handle jsonb documents, so coerce the input * if not already of jsonb type. @@ -4538,22 +4543,81 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) false); jsexpr->format = func->context_item->format; - path_spec = transformExprRecurse(pstate, func->pathspec); - pathspec_type = exprType(path_spec); - pathspec_loc = exprLocation(path_spec); - coerced_path_spec = coerce_to_target_type(pstate, path_spec, - pathspec_type, - JSONPATHOID, -1, - COERCION_EXPLICIT, - COERCE_IMPLICIT_CAST, - pathspec_loc); - if (coerced_path_spec == NULL) - ereport(ERROR, - (errcode(ERRCODE_DATATYPE_MISMATCH), - errmsg("JSON path expression must be of type %s, not of type %s", - "jsonpath", format_type_be(pathspec_type)), - parser_errposition(pstate, pathspec_loc))); - jsexpr->path_spec = coerced_path_spec; + if (jst_action) + { + JsonTransformAction *analyzed_jst_action = makeNode(JsonTransformAction); + + analyzed_jst_action->op = jst_action->op; + analyzed_jst_action->location = jst_action->location; + + switch (jst_action->op) + { + case TRANSFORM_INSERT: + case TRANSFORM_REPLACE: + analyzed_jst_action->value_expr = transformJsonValueExpr(pstate, func_name, + (JsonValueExpr *) jst_action->value_expr, + default_format, + JSONBOID, + false); + break; + case TRANSFORM_RENAME: + Node *v = transformExprRecurse(pstate, jst_action->value_expr); + + v = coerce_to_target_type(pstate, v, exprType(v), + TEXTOID, -1, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + exprLocation(v)); + if (v == NULL) + ereport(ERROR, + errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("RENAME target must be convertible to text"), + parser_errposition(pstate, exprLocation(jst_action->value_expr))); + analyzed_jst_action->value_expr = v; + break; + case TRANSFORM_REMOVE: + /* REMOVE has no value_expr */ + analyzed_jst_action->value_expr = NULL; + break; + } + + path_spec = transformExprRecurse(pstate, jst_action->pathspec); + pathspec_type = exprType(path_spec); + pathspec_loc = exprLocation(path_spec); + coerced_path_spec = coerce_to_target_type(pstate, path_spec, + pathspec_type, + JSONPATHOID, -1, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + pathspec_loc); + if (coerced_path_spec == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be of type %s, not of type %s", + "jsonpath", format_type_be(pathspec_type)), + parser_errposition(pstate, pathspec_loc))); + analyzed_jst_action->pathspec = coerced_path_spec; + jsexpr->action = analyzed_jst_action; + } + else + { + path_spec = transformExprRecurse(pstate, func->pathspec); + pathspec_type = exprType(path_spec); + pathspec_loc = exprLocation(path_spec); + coerced_path_spec = coerce_to_target_type(pstate, path_spec, + pathspec_type, + JSONPATHOID, -1, + COERCION_EXPLICIT, + COERCE_IMPLICIT_CAST, + pathspec_loc); + if (coerced_path_spec == NULL) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("JSON path expression must be of type %s, not of type %s", + "jsonpath", format_type_be(pathspec_type)), + parser_errposition(pstate, pathspec_loc))); + jsexpr->path_spec = coerced_path_spec; + } /* Transform and coerce the PASSING arguments to jsonb. */ transformJsonPassingArgs(pstate, func_name, @@ -4695,6 +4759,16 @@ transformJsonFuncExpr(ParseState *pstate, JsonFuncExpr *func) jsexpr->returning); break; + case JSON_TRANSFORM_OP: + /* Return type is always jsonb */ + if (!OidIsValid(jsexpr->returning->typid)) + { + jsexpr->returning->typid = JSONBOID; + jsexpr->returning->typmod = -1; + } + jsexpr->collation = get_typcollation(jsexpr->returning->typid); + /* No top-level ON EMPTY / ON ERROR for JSON_TRANSFORM */ + break; default: elog(ERROR, "invalid JsonFuncExpr op %d", (int) func->op); break; diff --git a/src/backend/parser/parse_target.c b/src/backend/parser/parse_target.c index 541fef5f183..529616c1152 100644 --- a/src/backend/parser/parse_target.c +++ b/src/backend/parser/parse_target.c @@ -2034,6 +2034,9 @@ FigureColnameInternal(Node *node, char **name) case JSON_VALUE_OP: *name = "json_value"; return 2; + case JSON_TRANSFORM_OP: + *name = "json_transform"; + return 2; /* JSON_TABLE_OP can't happen here. */ default: elog(ERROR, "unrecognized JsonExpr op: %d", diff --git a/src/include/executor/execExpr.h b/src/include/executor/execExpr.h index c61b3d624d5..bb385db0bd1 100644 --- a/src/include/executor/execExpr.h +++ b/src/include/executor/execExpr.h @@ -266,6 +266,7 @@ typedef enum ExprEvalOp EEOP_JSON_CONSTRUCTOR, EEOP_IS_JSON, EEOP_JSONEXPR_PATH, + EEOP_JSON_TRANSFORM, EEOP_JSONEXPR_COERCION, EEOP_JSONEXPR_COERCION_FINISH, EEOP_AGGREF, @@ -760,6 +761,12 @@ typedef struct ExprEvalStep struct JsonExprState *jsestate; } jsonexpr; + /* for EEOP_JSON_TRANSFORM */ + struct + { + struct JsonTransformExprState *jtstate; + } json_transform; + /* for EEOP_JSONEXPR_COERCION */ struct { @@ -894,6 +901,8 @@ extern void ExecEvalJsonConstructor(ExprState *state, ExprEvalStep *op, extern void ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op); extern int ExecEvalJsonExprPath(ExprState *state, ExprEvalStep *op, ExprContext *econtext); +extern void ExecEvalJsonTransform(ExprState *state, ExprEvalStep *op, + ExprContext *econtext); extern void ExecEvalJsonCoercion(ExprState *state, ExprEvalStep *op, ExprContext *econtext); extern void ExecEvalJsonCoercionFinish(ExprState *state, ExprEvalStep *op); diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 13359180d25..98f90ce76d8 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1095,6 +1095,20 @@ typedef struct DomainConstraintState ExprState *check_exprstate; /* check_expr's eval state, or NULL */ } DomainConstraintState; +typedef struct JsonTransformExprState +{ + JsonExpr *jsexpr; /* back-pointer to analyzed node */ + + /* Runtime slots — filled by prior steps */ + NullableDatum formatted_expr; /* input jsonb document */ + NullableDatum pathspec; /* compiled jsonpath Datum */ + NullableDatum action_value; /* value to for transform ops (NULL for + * REMOVE) */ + + /* PASSING args (only used if jsonpath needs them) */ + List *args; /* List of JsonPathVariable */ +} JsonTransformExprState; + /* * State for JsonExpr evaluation, too big to inline. * diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h index 91377a6cde3..1081589d0be 100644 --- a/src/include/nodes/parsenodes.h +++ b/src/include/nodes/parsenodes.h @@ -1960,6 +1960,7 @@ typedef struct JsonFuncExpr * not for a JSON_TABLE() */ JsonValueExpr *context_item; /* context item expression */ Node *pathspec; /* JSON path specification expression */ + JsonTransformAction *action; /* Actions: INSERT/REMOVE/RENAME/REPLACE */ List *passing; /* list of PASSING clause arguments, if any */ JsonOutput *output; /* output clause, if specified */ JsonBehavior *on_empty; /* ON EMPTY behavior */ diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfc946c20b..f8ae1f9de93 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -1712,6 +1712,23 @@ typedef struct JsonValueExpr JsonFormat *format; /* FORMAT clause, if specified */ } JsonValueExpr; +typedef enum JsonTransformOp +{ + TRANSFORM_INSERT, + TRANSFORM_REMOVE, + TRANSFORM_RENAME, + TRANSFORM_REPLACE, +} JsonTransformOp; + +typedef struct JsonTransformAction +{ + NodeTag type; + JsonTransformOp op; + Node *pathspec; /* The JSON Path: '$.a' */ + Node *value_expr; + ParseLoc location; /* token location, or -1 if unknown */ +} JsonTransformAction; + typedef enum JsonConstructorType { JSCTOR_JSON_OBJECT = 1, @@ -1831,6 +1848,7 @@ typedef enum JsonExprOp JSON_QUERY_OP, /* JSON_QUERY() */ JSON_VALUE_OP, /* JSON_VALUE() */ JSON_TABLE_OP, /* JSON_TABLE() */ + JSON_TRANSFORM_OP, /* JSON_TRANSFORM() */ } JsonExprOp; /* @@ -1856,6 +1874,8 @@ typedef struct JsonExpr /* jsonpath-valued expression containing the query pattern */ Node *path_spec; + JsonTransformAction *action; + /* Expected type/format of the output. */ JsonReturning *returning; diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h index 51ead54f015..38b09abd34c 100644 --- a/src/include/parser/kwlist.h +++ b/src/include/parser/kwlist.h @@ -249,6 +249,7 @@ 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_transform", JSON_TRANSFORM, 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) @@ -385,6 +386,7 @@ PG_KEYWORD("reindex", REINDEX, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("relationship", RELATIONSHIP, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("relative", RELATIVE_P, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("release", RELEASE, UNRESERVED_KEYWORD, BARE_LABEL) +PG_KEYWORD("remove", REMOVE, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("rename", RENAME, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("repack", REPACK, UNRESERVED_KEYWORD, BARE_LABEL) PG_KEYWORD("repeatable", REPEATABLE, UNRESERVED_KEYWORD, BARE_LABEL) -- 2.43.0