1
0
mirror of https://github.com/postgres/postgres.git synced 2025-06-16 06:01:02 +03:00

New json functions.

json_build_array() and json_build_object allow for the construction of
arbitrarily complex json trees. json_object() turns a one or two
dimensional array, or two separate arrays, into a json_object of
name/value pairs, similarly to the hstore() function.
json_object_agg() aggregates its two arguments into a single json object
as name value pairs.

Catalog version bumped.

Andrew Dunstan, reviewed by Marko Tiikkaja.
This commit is contained in:
Andrew Dunstan
2014-01-28 17:48:21 -05:00
parent 9132b189bf
commit 105639900b
10 changed files with 1221 additions and 71 deletions

View File

@ -68,6 +68,10 @@ static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims,
bool use_line_feeds);
static void array_to_json_internal(Datum array, StringInfo result,
bool use_line_feeds);
static void datum_to_json(Datum val, bool is_null, StringInfo result,
TYPCATEGORY tcategory, Oid typoutputfunc, bool key_scalar);
static void add_json(Datum val, bool is_null, StringInfo result,
Oid val_type, bool key_scalar);
/* the null action object used for pure validation */
static JsonSemAction nullSemAction =
@ -1219,7 +1223,7 @@ extract_mb_char(char *s)
*/
static void
datum_to_json(Datum val, bool is_null, StringInfo result,
TYPCATEGORY tcategory, Oid typoutputfunc)
TYPCATEGORY tcategory, Oid typoutputfunc, bool key_scalar)
{
char *outputstr;
text *jsontext;
@ -1241,24 +1245,32 @@ datum_to_json(Datum val, bool is_null, StringInfo result,
composite_to_json(val, result, false);
break;
case TYPCATEGORY_BOOLEAN:
if (DatumGetBool(val))
appendStringInfoString(result, "true");
if (!key_scalar)
appendStringInfoString(result, DatumGetBool(val) ? "true" : "false");
else
appendStringInfoString(result, "false");
escape_json(result, DatumGetBool(val) ? "true" : "false");
break;
case TYPCATEGORY_NUMERIC:
outputstr = OidOutputFunctionCall(typoutputfunc, val);
/*
* Don't call escape_json here if it's a valid JSON number.
*/
dummy_lex.input = *outputstr == '-' ? outputstr + 1 : outputstr;
dummy_lex.input_length = strlen(dummy_lex.input);
json_lex_number(&dummy_lex, dummy_lex.input, &numeric_error);
if (!numeric_error)
appendStringInfoString(result, outputstr);
else
if (key_scalar)
{
/* always quote keys */
escape_json(result, outputstr);
}
else
{
/*
* Don't call escape_json for a non-key if it's a valid JSON
* number.
*/
dummy_lex.input = *outputstr == '-' ? outputstr + 1 : outputstr;
dummy_lex.input_length = strlen(dummy_lex.input);
json_lex_number(&dummy_lex, dummy_lex.input, &numeric_error);
if (!numeric_error)
appendStringInfoString(result, outputstr);
else
escape_json(result, outputstr);
}
pfree(outputstr);
break;
case TYPCATEGORY_JSON:
@ -1276,6 +1288,10 @@ datum_to_json(Datum val, bool is_null, StringInfo result,
break;
default:
outputstr = OidOutputFunctionCall(typoutputfunc, val);
if (key_scalar && *outputstr == '\0')
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("key value must not be empty")));
escape_json(result, outputstr);
pfree(outputstr);
break;
@ -1309,7 +1325,7 @@ array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, Datum *vals,
if (dim + 1 == ndims)
{
datum_to_json(vals[*valcount], nulls[*valcount], result, tcategory,
typoutputfunc);
typoutputfunc, false);
(*valcount)++;
}
else
@ -1490,13 +1506,75 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds)
else
tcategory = TypeCategory(tupdesc->attrs[i]->atttypid);
datum_to_json(val, isnull, result, tcategory, typoutput);
datum_to_json(val, isnull, result, tcategory, typoutput, false);
}
appendStringInfoChar(result, '}');
ReleaseTupleDesc(tupdesc);
}
/*
* append Json for orig_val to result. If it's a field key, make sure it's
* of an acceptable type and is quoted.
*/
static void
add_json(Datum val, bool is_null, StringInfo result, Oid val_type, bool key_scalar)
{
TYPCATEGORY tcategory;
Oid typoutput;
bool typisvarlena;
Oid castfunc = InvalidOid;
if (val_type == InvalidOid)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("could not determine input data type")));
getTypeOutputInfo(val_type, &typoutput, &typisvarlena);
if (val_type > FirstNormalObjectId)
{
HeapTuple tuple;
Form_pg_cast castForm;
tuple = SearchSysCache2(CASTSOURCETARGET,
ObjectIdGetDatum(val_type),
ObjectIdGetDatum(JSONOID));
if (HeapTupleIsValid(tuple))
{
castForm = (Form_pg_cast) GETSTRUCT(tuple);
if (castForm->castmethod == COERCION_METHOD_FUNCTION)
castfunc = typoutput = castForm->castfunc;
ReleaseSysCache(tuple);
}
}
if (castfunc != InvalidOid)
tcategory = TYPCATEGORY_JSON_CAST;
else if (val_type == RECORDARRAYOID)
tcategory = TYPCATEGORY_ARRAY;
else if (val_type == RECORDOID)
tcategory = TYPCATEGORY_COMPOSITE;
else if (val_type == JSONOID)
tcategory = TYPCATEGORY_JSON;
else
tcategory = TypeCategory(val_type);
if (key_scalar &&
(tcategory == TYPCATEGORY_ARRAY ||
tcategory == TYPCATEGORY_COMPOSITE ||
tcategory == TYPCATEGORY_JSON ||
tcategory == TYPCATEGORY_JSON_CAST))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("key value must be scalar, not array, composite or json")));
datum_to_json(val, is_null, result, tcategory, typoutput, key_scalar);
}
/*
* SQL function array_to_json(row)
*/
@ -1616,7 +1694,7 @@ to_json(PG_FUNCTION_ARGS)
else
tcategory = TypeCategory(val_type);
datum_to_json(val, false, result, tcategory, typoutput);
datum_to_json(val, false, result, tcategory, typoutput, false);
PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len));
}
@ -1672,7 +1750,7 @@ json_agg_transfn(PG_FUNCTION_ARGS)
if (PG_ARGISNULL(1))
{
val = (Datum) 0;
datum_to_json(val, true, state, 0, InvalidOid);
datum_to_json(val, true, state, 0, InvalidOid, false);
PG_RETURN_POINTER(state);
}
@ -1716,7 +1794,7 @@ json_agg_transfn(PG_FUNCTION_ARGS)
appendStringInfoString(state, "\n ");
}
datum_to_json(val, false, state, tcategory, typoutput);
datum_to_json(val, false, state, tcategory, typoutput, false);
/*
* The transition type for array_agg() is declared to be "internal", which
@ -1747,6 +1825,467 @@ json_agg_finalfn(PG_FUNCTION_ARGS)
PG_RETURN_TEXT_P(cstring_to_text_with_len(state->data, state->len));
}
/*
* json_object_agg transition function.
*
* aggregate two input columns as a single json value.
*/
Datum
json_object_agg_transfn(PG_FUNCTION_ARGS)
{
Oid val_type;
MemoryContext aggcontext,
oldcontext;
StringInfo state;
Datum arg;
if (!AggCheckCallContext(fcinfo, &aggcontext))
{
/* cannot be called directly because of internal-type argument */
elog(ERROR, "json_agg_transfn called in non-aggregate context");
}
if (PG_ARGISNULL(0))
{
/*
* Make this StringInfo in a context where it will persist for the
* duration off the aggregate call. It's only needed for this initial
* piece, as the StringInfo routines make sure they use the right
* context to enlarge the object if necessary.
*/
oldcontext = MemoryContextSwitchTo(aggcontext);
state = makeStringInfo();
MemoryContextSwitchTo(oldcontext);
appendStringInfoString(state, "{ ");
}
else
{
state = (StringInfo) PG_GETARG_POINTER(0);
appendStringInfoString(state, ", ");
}
if (PG_ARGISNULL(1))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("field name must not be null")));
val_type = get_fn_expr_argtype(fcinfo->flinfo, 1);
/*
* turn a constant (more or less literal) value that's of unknown type
* into text. Unknowns come in as a cstring pointer.
*/
if (val_type == UNKNOWNOID && get_fn_expr_arg_stable(fcinfo->flinfo, 1))
{
val_type = TEXTOID;
arg = CStringGetTextDatum(PG_GETARG_POINTER(1));
}
else
{
arg = PG_GETARG_DATUM(1);
}
if (val_type == InvalidOid || val_type == UNKNOWNOID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("arg 1: could not determine data type")));
add_json(arg, false, state, val_type, true);
appendStringInfoString(state, " : ");
val_type = get_fn_expr_argtype(fcinfo->flinfo, 2);
/* see comments above */
if (val_type == UNKNOWNOID && get_fn_expr_arg_stable(fcinfo->flinfo, 2))
{
val_type = TEXTOID;
if (PG_ARGISNULL(2))
arg = (Datum) 0;
else
arg = CStringGetTextDatum(PG_GETARG_POINTER(2));
}
else
{
arg = PG_GETARG_DATUM(2);
}
if (val_type == InvalidOid || val_type == UNKNOWNOID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("arg 2: could not determine data type")));
add_json(arg, PG_ARGISNULL(2), state, val_type, false);
PG_RETURN_POINTER(state);
}
/*
* json_object_agg final function.
*
*/
Datum
json_object_agg_finalfn(PG_FUNCTION_ARGS)
{
StringInfo state;
/* cannot be called directly because of internal-type argument */
Assert(AggCheckCallContext(fcinfo, NULL));
state = PG_ARGISNULL(0) ? NULL : (StringInfo) PG_GETARG_POINTER(0);
if (state == NULL)
PG_RETURN_TEXT_P(cstring_to_text("{}"));
appendStringInfoString(state, " }");
PG_RETURN_TEXT_P(cstring_to_text_with_len(state->data, state->len));
}
/*
* SQL function json_build_object(variadic "any")
*/
Datum
json_build_object(PG_FUNCTION_ARGS)
{
int nargs = PG_NARGS();
int i;
Datum arg;
char *sep = "";
StringInfo result;
Oid val_type;
if (nargs % 2 != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("invalid number or arguments: object must be matched key value pairs")));
result = makeStringInfo();
appendStringInfoChar(result, '{');
for (i = 0; i < nargs; i += 2)
{
/* process key */
if (PG_ARGISNULL(i))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("arg %d: key cannot be null", i + 1)));
val_type = get_fn_expr_argtype(fcinfo->flinfo, i);
/*
* turn a constant (more or less literal) value that's of unknown type
* into text. Unknowns come in as a cstring pointer.
*/
if (val_type == UNKNOWNOID && get_fn_expr_arg_stable(fcinfo->flinfo, i))
{
val_type = TEXTOID;
if (PG_ARGISNULL(i))
arg = (Datum) 0;
else
arg = CStringGetTextDatum(PG_GETARG_POINTER(i));
}
else
{
arg = PG_GETARG_DATUM(i);
}
if (val_type == InvalidOid || val_type == UNKNOWNOID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("arg %d: could not determine data type", i + 1)));
appendStringInfoString(result, sep);
sep = ", ";
add_json(arg, false, result, val_type, true);
appendStringInfoString(result, " : ");
/* process value */
val_type = get_fn_expr_argtype(fcinfo->flinfo, i + 1);
/* see comments above */
if (val_type == UNKNOWNOID && get_fn_expr_arg_stable(fcinfo->flinfo, i + 1))
{
val_type = TEXTOID;
if (PG_ARGISNULL(i + 1))
arg = (Datum) 0;
else
arg = CStringGetTextDatum(PG_GETARG_POINTER(i + 1));
}
else
{
arg = PG_GETARG_DATUM(i + 1);
}
if (val_type == InvalidOid || val_type == UNKNOWNOID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("arg %d: could not determine data type", i + 2)));
add_json(arg, PG_ARGISNULL(i + 1), result, val_type, false);
}
appendStringInfoChar(result, '}');
PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len));
}
/*
* degenerate case of json_build_object where it gets 0 arguments.
*/
Datum
json_build_object_noargs(PG_FUNCTION_ARGS)
{
PG_RETURN_TEXT_P(cstring_to_text_with_len("{}", 2));
}
/*
* SQL function json_build_array(variadic "any")
*/
Datum
json_build_array(PG_FUNCTION_ARGS)
{
int nargs = PG_NARGS();
int i;
Datum arg;
char *sep = "";
StringInfo result;
Oid val_type;
result = makeStringInfo();
appendStringInfoChar(result, '[');
for (i = 0; i < nargs; i++)
{
val_type = get_fn_expr_argtype(fcinfo->flinfo, i);
arg = PG_GETARG_DATUM(i + 1);
/* see comments in json_build_object above */
if (val_type == UNKNOWNOID && get_fn_expr_arg_stable(fcinfo->flinfo, i))
{
val_type = TEXTOID;
if (PG_ARGISNULL(i))
arg = (Datum) 0;
else
arg = CStringGetTextDatum(PG_GETARG_POINTER(i));
}
else
{
arg = PG_GETARG_DATUM(i);
}
if (val_type == InvalidOid || val_type == UNKNOWNOID)
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("arg %d: could not determine data type", i + 1)));
appendStringInfoString(result, sep);
sep = ", ";
add_json(arg, PG_ARGISNULL(i), result, val_type, false);
}
appendStringInfoChar(result, ']');
PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len));
}
/*
* degenerate case of json_build_array where it gets 0 arguments.
*/
Datum
json_build_array_noargs(PG_FUNCTION_ARGS)
{
PG_RETURN_TEXT_P(cstring_to_text_with_len("[]", 2));
}
/*
* SQL function json_object(text[])
*
* take a one or two dimensional array of text as name vale pairs
* for a json object.
*
*/
Datum
json_object(PG_FUNCTION_ARGS)
{
ArrayType *in_array = PG_GETARG_ARRAYTYPE_P(0);
int ndims = ARR_NDIM(in_array);
StringInfoData result;
Datum *in_datums;
bool *in_nulls;
int in_count,
count,
i;
text *rval;
char *v;
switch (ndims)
{
case 0:
PG_RETURN_DATUM(CStringGetTextDatum("{}"));
break;
case 1:
if ((ARR_DIMS(in_array)[0]) % 2)
ereport(ERROR,
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
errmsg("array must have even number of elements")));
break;
case 2:
if ((ARR_DIMS(in_array)[1]) != 2)
ereport(ERROR,
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
errmsg("array must have two columns")));
break;
default:
ereport(ERROR,
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
errmsg("wrong number of array subscripts")));
}
deconstruct_array(in_array,
TEXTOID, -1, false, 'i',
&in_datums, &in_nulls, &in_count);
count = in_count / 2;
initStringInfo(&result);
appendStringInfoChar(&result, '{');
for (i = 0; i < count; ++i)
{
if (in_nulls[i * 2])
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("null value not allowed for object key")));
v = TextDatumGetCString(in_datums[i * 2]);
if (v[0] == '\0')
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("empty value not allowed for object key")));
if (i > 0)
appendStringInfoString(&result, ", ");
escape_json(&result, v);
appendStringInfoString(&result, " : ");
pfree(v);
if (in_nulls[i * 2 + 1])
appendStringInfoString(&result, "null");
else
{
v = TextDatumGetCString(in_datums[i * 2 + 1]);
escape_json(&result, v);
pfree(v);
}
}
appendStringInfoChar(&result, '}');
pfree(in_datums);
pfree(in_nulls);
rval = cstring_to_text_with_len(result.data, result.len);
pfree(result.data);
PG_RETURN_TEXT_P(rval);
}
/*
* SQL function json_object(text[], text[])
*
* take separate name and value arrays of text to construct a json object
* pairwise.
*/
Datum
json_object_two_arg(PG_FUNCTION_ARGS)
{
ArrayType *key_array = PG_GETARG_ARRAYTYPE_P(0);
ArrayType *val_array = PG_GETARG_ARRAYTYPE_P(1);
int nkdims = ARR_NDIM(key_array);
int nvdims = ARR_NDIM(val_array);
StringInfoData result;
Datum *key_datums,
*val_datums;
bool *key_nulls,
*val_nulls;
int key_count,
val_count,
i;
text *rval;
char *v;
if (nkdims > 1 || nkdims != nvdims)
ereport(ERROR,
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
errmsg("wrong number of array subscripts")));
if (nkdims == 0)
PG_RETURN_DATUM(CStringGetTextDatum("{}"));
deconstruct_array(key_array,
TEXTOID, -1, false, 'i',
&key_datums, &key_nulls, &key_count);
deconstruct_array(val_array,
TEXTOID, -1, false, 'i',
&val_datums, &val_nulls, &val_count);
if (key_count != val_count)
ereport(ERROR,
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
errmsg("mismatched array dimensions")));
initStringInfo(&result);
appendStringInfoChar(&result, '{');
for (i = 0; i < key_count; ++i)
{
if (key_nulls[i])
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("null value not allowed for object key")));
v = TextDatumGetCString(key_datums[i]);
if (v[0] == '\0')
ereport(ERROR,
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
errmsg("empty value not allowed for object key")));
if (i > 0)
appendStringInfoString(&result, ", ");
escape_json(&result, v);
appendStringInfoString(&result, " : ");
pfree(v);
if (val_nulls[i])
appendStringInfoString(&result, "null");
else
{
v = TextDatumGetCString(val_datums[i]);
escape_json(&result, v);
pfree(v);
}
}
appendStringInfoChar(&result, '}');
pfree(key_datums);
pfree(key_nulls);
pfree(val_datums);
pfree(val_nulls);
rval = cstring_to_text_with_len(result.data, result.len);
pfree(result.data);
PG_RETURN_TEXT_P(rval);
}
/*
* Produce a JSON string literal, properly escaping characters in the text.
*/