v1-0001-SQL-JSON-Add-initial-JSON_TRANSFORM-implementation.patch
application/octet-stream
Filename: v1-0001-SQL-JSON-Add-initial-JSON_TRANSFORM-implementation.patch
Type: application/octet-stream
Part: 1
Patch
Format: format-patch
Series: patch v1-0001
Subject: SQL/JSON: Add initial JSON_TRANSFORM implementation
| File | + | − |
|---|---|---|
| src/backend/executor/execExpr.c | 159 | 0 |
| src/backend/executor/execExprInterp.c | 187 | 0 |
| src/backend/parser/gram.y | 66 | 2 |
| src/backend/parser/parse_expr.c | 91 | 17 |
| src/backend/parser/parse_target.c | 3 | 0 |
| src/include/executor/execExpr.h | 9 | 0 |
| src/include/nodes/execnodes.h | 14 | 0 |
| src/include/nodes/parsenodes.h | 1 | 0 |
| src/include/nodes/primnodes.h | 20 | 0 |
| src/include/parser/kwlist.h | 2 | 0 |
From 6a2e6ebf080365f48e99f389dda9e3d40b121112 Mon Sep 17 00:00:00 2001
From: Srinath Reddy Sadipiralla <srinath2133@gmail.com>
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 <list> 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