mirror of
https://github.com/postgres/postgres.git
synced 2025-07-07 00:36:50 +03:00
Add SQL/JSON query functions
This introduces the following SQL/JSON functions for querying JSON data using jsonpath expressions: JSON_EXISTS(), which can be used to apply a jsonpath expression to a JSON value to check if it yields any values. JSON_QUERY(), which can be used to to apply a jsonpath expression to a JSON value to get a JSON object, an array, or a string. There are various options to control whether multi-value result uses array wrappers and whether the singleton scalar strings are quoted or not. JSON_VALUE(), which can be used to apply a jsonpath expression to a JSON value to return a single scalar value, producing an error if it multiple values are matched. Both JSON_VALUE() and JSON_QUERY() functions have options for handling EMPTY and ERROR conditions, which can be used to specify the behavior when no values are matched and when an error occurs during jsonpath evaluation, respectively. Author: Nikita Glukhov <n.gluhov@postgrespro.ru> Author: Teodor Sigaev <teodor@sigaev.ru> Author: Oleg Bartunov <obartunov@gmail.com> Author: Alexander Korotkov <aekorotkov@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> Author: Amit Langote <amitlangote09@gmail.com> Author: Peter Eisentraut <peter@eisentraut.org> Author: Jian He <jian.universality@gmail.com> Reviewers have included (in no particular order): Andres Freund, Alexander Korotkov, Pavel Stehule, Andrew Alsup, Erik Rijkers, Zihong Yu, Himanshu Upadhyaya, Daniel Gustafsson, Justin Pryzby, Álvaro Herrera, Jian He, Anton A. Melnikov, Nikita Malakhov, Peter Eisentraut, Tomas Vondra Discussion: https://postgr.es/m/cd0bb935-0158-78a7-08b5-904886deac4b@postgrespro.ru Discussion: https://postgr.es/m/20220616233130.rparivafipt6doj3@alap3.anarazel.de Discussion: https://postgr.es/m/abd9b83b-aa66-f230-3d6d-734817f0995d%40postgresql.org Discussion: https://postgr.es/m/CA+HiwqHROpf9e644D8BRqYvaAPmgBZVup-xKMDPk-nd4EpgzHw@mail.gmail.com Discussion: https://postgr.es/m/CA+HiwqE4XTdfb1nW=Ojoy_tQSRhYt-q_kb6i5d4xcKyrLC1Nbg@mail.gmail.com
This commit is contained in:
@ -72,8 +72,8 @@
|
||||
#include "utils/datum.h"
|
||||
#include "utils/expandedrecord.h"
|
||||
#include "utils/json.h"
|
||||
#include "utils/jsonb.h"
|
||||
#include "utils/jsonfuncs.h"
|
||||
#include "utils/jsonpath.h"
|
||||
#include "utils/lsyscache.h"
|
||||
#include "utils/memutils.h"
|
||||
#include "utils/timestamp.h"
|
||||
@ -180,6 +180,7 @@ static pg_attribute_always_inline void ExecAggPlainTransByRef(AggState *aggstate
|
||||
AggStatePerGroup pergroup,
|
||||
ExprContext *aggcontext,
|
||||
int setno);
|
||||
static char *ExecGetJsonValueItemString(JsonbValue *item, bool *resnull);
|
||||
|
||||
/*
|
||||
* ScalarArrayOpExprHashEntry
|
||||
@ -481,6 +482,9 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
|
||||
&&CASE_EEOP_XMLEXPR,
|
||||
&&CASE_EEOP_JSON_CONSTRUCTOR,
|
||||
&&CASE_EEOP_IS_JSON,
|
||||
&&CASE_EEOP_JSONEXPR_PATH,
|
||||
&&CASE_EEOP_JSONEXPR_COERCION,
|
||||
&&CASE_EEOP_JSONEXPR_COERCION_FINISH,
|
||||
&&CASE_EEOP_AGGREF,
|
||||
&&CASE_EEOP_GROUPING_FUNC,
|
||||
&&CASE_EEOP_WINDOW_FUNC,
|
||||
@ -1554,6 +1558,28 @@ ExecInterpExpr(ExprState *state, ExprContext *econtext, bool *isnull)
|
||||
EEO_NEXT();
|
||||
}
|
||||
|
||||
EEO_CASE(EEOP_JSONEXPR_PATH)
|
||||
{
|
||||
/* too complex for an inline implementation */
|
||||
EEO_JUMP(ExecEvalJsonExprPath(state, op, econtext));
|
||||
}
|
||||
|
||||
EEO_CASE(EEOP_JSONEXPR_COERCION)
|
||||
{
|
||||
/* too complex for an inline implementation */
|
||||
ExecEvalJsonCoercion(state, op, econtext);
|
||||
|
||||
EEO_NEXT();
|
||||
}
|
||||
|
||||
EEO_CASE(EEOP_JSONEXPR_COERCION_FINISH)
|
||||
{
|
||||
/* too complex for an inline implementation */
|
||||
ExecEvalJsonCoercionFinish(state, op);
|
||||
|
||||
EEO_NEXT();
|
||||
}
|
||||
|
||||
EEO_CASE(EEOP_AGGREF)
|
||||
{
|
||||
/*
|
||||
@ -4222,6 +4248,316 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op)
|
||||
*op->resvalue = BoolGetDatum(res);
|
||||
}
|
||||
|
||||
/*
|
||||
* Evaluate a jsonpath against a document, both of which must have been
|
||||
* evaluated and their values saved in op->d.jsonexpr.jsestate.
|
||||
*
|
||||
* If an error occurs during JsonPath* evaluation or when coercing its result
|
||||
* to the RETURNING type, JsonExprState.error is set to true, provided the
|
||||
* ON ERROR behavior is not ERROR. Similarly, if JsonPath{Query|Value}() found
|
||||
* no matching items, JsonExprState.empty is set to true, provided the ON EMPTY
|
||||
* behavior is not ERROR. That is to signal to the subsequent steps that check
|
||||
* those flags to return the ON ERROR / ON EMPTY expression.
|
||||
*
|
||||
* Return value is the step address to be performed next. It will be one of
|
||||
* jump_error, jump_empty, jump_eval_coercion, or jump_end, all given in
|
||||
* op->d.jsonexpr.jsestate.
|
||||
*/
|
||||
int
|
||||
ExecEvalJsonExprPath(ExprState *state, ExprEvalStep *op,
|
||||
ExprContext *econtext)
|
||||
{
|
||||
JsonExprState *jsestate = op->d.jsonexpr.jsestate;
|
||||
JsonExpr *jsexpr = jsestate->jsexpr;
|
||||
Datum item;
|
||||
JsonPath *path;
|
||||
bool throw_error = jsexpr->on_error->btype == JSON_BEHAVIOR_ERROR;
|
||||
bool error = false,
|
||||
empty = false;
|
||||
int jump_eval_coercion = jsestate->jump_eval_coercion;
|
||||
char *val_string = NULL;
|
||||
|
||||
item = jsestate->formatted_expr.value;
|
||||
path = DatumGetJsonPathP(jsestate->pathspec.value);
|
||||
|
||||
/* Set error/empty to false. */
|
||||
memset(&jsestate->error, 0, sizeof(NullableDatum));
|
||||
memset(&jsestate->empty, 0, sizeof(NullableDatum));
|
||||
|
||||
/*
|
||||
* Also reset ErrorSaveContext contents for the next row. Since we don't
|
||||
* set details_wanted, we don't need to also reset error_data, which would
|
||||
* be NULL anyway.
|
||||
*/
|
||||
Assert(!jsestate->escontext.details_wanted &&
|
||||
jsestate->escontext.error_data == NULL);
|
||||
jsestate->escontext.error_occurred = false;
|
||||
|
||||
switch (jsexpr->op)
|
||||
{
|
||||
case JSON_EXISTS_OP:
|
||||
{
|
||||
bool exists = JsonPathExists(item, path,
|
||||
!throw_error ? &error : NULL,
|
||||
jsestate->args);
|
||||
|
||||
if (!error)
|
||||
{
|
||||
*op->resvalue = BoolGetDatum(exists);
|
||||
*op->resnull = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case JSON_QUERY_OP:
|
||||
*op->resvalue = JsonPathQuery(item, path, jsexpr->wrapper, &empty,
|
||||
!throw_error ? &error : NULL,
|
||||
jsestate->args);
|
||||
|
||||
*op->resnull = (DatumGetPointer(*op->resvalue) == NULL);
|
||||
|
||||
/* Handle OMIT QUOTES. */
|
||||
if (!*op->resnull && jsexpr->omit_quotes)
|
||||
{
|
||||
val_string = JsonbUnquote(DatumGetJsonbP(*op->resvalue));
|
||||
|
||||
/*
|
||||
* Pass the string as a text value to the cast expression if
|
||||
* one present. If not, use the input function call below to
|
||||
* do the coercion.
|
||||
*/
|
||||
if (jump_eval_coercion >= 0)
|
||||
*op->resvalue =
|
||||
DirectFunctionCall1(textin,
|
||||
PointerGetDatum(val_string));
|
||||
}
|
||||
break;
|
||||
|
||||
case JSON_VALUE_OP:
|
||||
{
|
||||
JsonbValue *jbv = JsonPathValue(item, path, &empty,
|
||||
!throw_error ? &error : NULL,
|
||||
jsestate->args);
|
||||
|
||||
if (jbv == NULL)
|
||||
{
|
||||
/* Will be coerced with coercion_expr, if any. */
|
||||
*op->resvalue = (Datum) 0;
|
||||
*op->resnull = true;
|
||||
}
|
||||
else if (!error && !empty)
|
||||
{
|
||||
if (jsexpr->returning->typid == JSONOID ||
|
||||
jsexpr->returning->typid == JSONBOID)
|
||||
{
|
||||
val_string = DatumGetCString(DirectFunctionCall1(jsonb_out,
|
||||
JsonbPGetDatum(JsonbValueToJsonb(jbv))));
|
||||
}
|
||||
else
|
||||
{
|
||||
val_string = ExecGetJsonValueItemString(jbv, op->resnull);
|
||||
|
||||
/*
|
||||
* Pass the string as a text value to the cast
|
||||
* expression if one present. If not, use the input
|
||||
* function call below to do the coercion.
|
||||
*/
|
||||
*op->resvalue = PointerGetDatum(val_string);
|
||||
if (jump_eval_coercion >= 0)
|
||||
*op->resvalue = DirectFunctionCall1(textin, *op->resvalue);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
elog(ERROR, "unrecognized SQL/JSON expression op %d",
|
||||
(int) jsexpr->op);
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Coerce the result value to the RETURNING type by calling its input
|
||||
* function.
|
||||
*/
|
||||
if (!*op->resnull && jsexpr->use_io_coercion)
|
||||
{
|
||||
FunctionCallInfo fcinfo;
|
||||
|
||||
Assert(jump_eval_coercion == -1);
|
||||
fcinfo = jsestate->input_fcinfo;
|
||||
Assert(fcinfo != NULL);
|
||||
Assert(val_string != NULL);
|
||||
fcinfo->args[0].value = PointerGetDatum(val_string);
|
||||
fcinfo->args[0].isnull = *op->resnull;
|
||||
|
||||
/*
|
||||
* Second and third arguments are already set up in
|
||||
* ExecInitJsonExpr().
|
||||
*/
|
||||
|
||||
fcinfo->isnull = false;
|
||||
*op->resvalue = FunctionCallInvoke(fcinfo);
|
||||
if (SOFT_ERROR_OCCURRED(&jsestate->escontext))
|
||||
error = true;
|
||||
}
|
||||
|
||||
/* Handle ON EMPTY. */
|
||||
if (empty)
|
||||
{
|
||||
if (jsexpr->on_empty)
|
||||
{
|
||||
if (jsexpr->on_empty->btype == JSON_BEHAVIOR_ERROR)
|
||||
ereport(ERROR,
|
||||
errcode(ERRCODE_NO_SQL_JSON_ITEM),
|
||||
errmsg("no SQL/JSON item"));
|
||||
else
|
||||
jsestate->empty.value = BoolGetDatum(true);
|
||||
|
||||
Assert(jsestate->jump_empty >= 0);
|
||||
return jsestate->jump_empty;
|
||||
}
|
||||
else if (jsexpr->on_error->btype == JSON_BEHAVIOR_ERROR)
|
||||
ereport(ERROR,
|
||||
errcode(ERRCODE_NO_SQL_JSON_ITEM),
|
||||
errmsg("no SQL/JSON item"));
|
||||
else
|
||||
jsestate->error.value = BoolGetDatum(true);
|
||||
|
||||
*op->resvalue = (Datum) 0;
|
||||
*op->resnull = true;
|
||||
|
||||
Assert(!throw_error && jsestate->jump_error >= 0);
|
||||
return jsestate->jump_error;
|
||||
}
|
||||
|
||||
/*
|
||||
* ON ERROR. Wouldn't get here if the behavior is ERROR, because they
|
||||
* would have already been thrown.
|
||||
*/
|
||||
if (error)
|
||||
{
|
||||
Assert(!throw_error && jsestate->jump_error >= 0);
|
||||
*op->resvalue = (Datum) 0;
|
||||
*op->resnull = true;
|
||||
jsestate->error.value = BoolGetDatum(true);
|
||||
return jsestate->jump_error;
|
||||
}
|
||||
|
||||
return jump_eval_coercion >= 0 ? jump_eval_coercion : jsestate->jump_end;
|
||||
}
|
||||
|
||||
/*
|
||||
* Convert the given JsonbValue to its C string representation
|
||||
*
|
||||
* *resnull is set if the JsonbValue is a jbvNull.
|
||||
*/
|
||||
static char *
|
||||
ExecGetJsonValueItemString(JsonbValue *item, bool *resnull)
|
||||
{
|
||||
*resnull = false;
|
||||
|
||||
/* get coercion state reference and datum of the corresponding SQL type */
|
||||
switch (item->type)
|
||||
{
|
||||
case jbvNull:
|
||||
*resnull = true;
|
||||
return NULL;
|
||||
|
||||
case jbvString:
|
||||
{
|
||||
char *str = palloc(item->val.string.len + 1);
|
||||
|
||||
memcpy(str, item->val.string.val, item->val.string.len);
|
||||
str[item->val.string.len] = '\0';
|
||||
return str;
|
||||
}
|
||||
|
||||
case jbvNumeric:
|
||||
return DatumGetCString(DirectFunctionCall1(numeric_out,
|
||||
NumericGetDatum(item->val.numeric)));
|
||||
|
||||
case jbvBool:
|
||||
return DatumGetCString(DirectFunctionCall1(boolout,
|
||||
BoolGetDatum(item->val.boolean)));
|
||||
|
||||
case jbvDatetime:
|
||||
switch (item->val.datetime.typid)
|
||||
{
|
||||
case DATEOID:
|
||||
return DatumGetCString(DirectFunctionCall1(date_out,
|
||||
item->val.datetime.value));
|
||||
case TIMEOID:
|
||||
return DatumGetCString(DirectFunctionCall1(time_out,
|
||||
item->val.datetime.value));
|
||||
case TIMETZOID:
|
||||
return DatumGetCString(DirectFunctionCall1(timetz_out,
|
||||
item->val.datetime.value));
|
||||
case TIMESTAMPOID:
|
||||
return DatumGetCString(DirectFunctionCall1(timestamp_out,
|
||||
item->val.datetime.value));
|
||||
case TIMESTAMPTZOID:
|
||||
return DatumGetCString(DirectFunctionCall1(timestamptz_out,
|
||||
item->val.datetime.value));
|
||||
default:
|
||||
elog(ERROR, "unexpected jsonb datetime type oid %u",
|
||||
item->val.datetime.typid);
|
||||
}
|
||||
break;
|
||||
|
||||
case jbvArray:
|
||||
case jbvObject:
|
||||
case jbvBinary:
|
||||
return DatumGetCString(DirectFunctionCall1(jsonb_out,
|
||||
JsonbPGetDatum(JsonbValueToJsonb(item))));
|
||||
|
||||
default:
|
||||
elog(ERROR, "unexpected jsonb value type %d", item->type);
|
||||
}
|
||||
|
||||
Assert(false);
|
||||
*resnull = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* Coerce a jsonb value produced by ExecEvalJsonExprPath() or an ON ERROR /
|
||||
* ON EMPTY behavior expression to the target type.
|
||||
*
|
||||
* Any soft errors that occur here will be checked by
|
||||
* EEOP_JSONEXPR_COERCION_FINISH that will run after this.
|
||||
*/
|
||||
void
|
||||
ExecEvalJsonCoercion(ExprState *state, ExprEvalStep *op,
|
||||
ExprContext *econtext)
|
||||
{
|
||||
ErrorSaveContext *escontext = op->d.jsonexpr_coercion.escontext;
|
||||
|
||||
*op->resvalue = json_populate_type(*op->resvalue, JSONBOID,
|
||||
op->d.jsonexpr_coercion.targettype,
|
||||
op->d.jsonexpr_coercion.targettypmod,
|
||||
&op->d.jsonexpr_coercion.json_populate_type_cache,
|
||||
econtext->ecxt_per_query_memory,
|
||||
op->resnull, (Node *) escontext);
|
||||
}
|
||||
|
||||
/*
|
||||
* Checks if an error occurred either when evaluating JsonExpr.coercion_expr or
|
||||
* in ExecEvalJsonCoercion(). If so, this sets JsonExprState.error to trigger
|
||||
* the ON ERROR handling steps.
|
||||
*/
|
||||
void
|
||||
ExecEvalJsonCoercionFinish(ExprState *state, ExprEvalStep *op)
|
||||
{
|
||||
JsonExprState *jsestate = op->d.jsonexpr.jsestate;
|
||||
|
||||
if (SOFT_ERROR_OCCURRED(&jsestate->escontext))
|
||||
{
|
||||
*op->resvalue = (Datum) 0;
|
||||
*op->resnull = true;
|
||||
jsestate->error.value = BoolGetDatum(true);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* ExecEvalGroupingFunc
|
||||
|
Reference in New Issue
Block a user