1
0
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:
Amit Langote
2024-03-21 17:06:27 +09:00
parent a145f424d5
commit 6185c9737c
34 changed files with 4815 additions and 36 deletions

View File

@ -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