mirror of
https://github.com/postgres/postgres.git
synced 2025-04-20 00:42:27 +03:00
This patch introduces the SQL standard IS JSON predicate. It operates on text and bytea values representing JSON, as well as on the json and jsonb types. Each test has IS and IS NOT variants and supports a WITH UNIQUE KEYS flag. The tests are: IS JSON [VALUE] IS JSON ARRAY IS JSON OBJECT IS JSON SCALAR These should be self-explanatory. The WITH UNIQUE KEYS flag makes these return false when duplicate keys exist in any object within the value, not necessarily directly contained in the outermost object. 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: Amit Langote <amitlangote09@gmail.com> Author: Andrew Dunstan <andrew@dunslane.net> 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. Discussion: https://postgr.es/m/CAF4Au4w2x-5LTnN_bxky-mq4=WOqsGsxSpENCzHRAzSnEd8+WQ@mail.gmail.com 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
1826 lines
43 KiB
C
1826 lines
43 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* json.c
|
|
* JSON data type support.
|
|
*
|
|
* Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
|
|
* Portions Copyright (c) 1994, Regents of the University of California
|
|
*
|
|
* IDENTIFICATION
|
|
* src/backend/utils/adt/json.c
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#include "postgres.h"
|
|
|
|
#include "catalog/pg_proc.h"
|
|
#include "catalog/pg_type.h"
|
|
#include "common/hashfn.h"
|
|
#include "funcapi.h"
|
|
#include "libpq/pqformat.h"
|
|
#include "miscadmin.h"
|
|
#include "parser/parse_coerce.h"
|
|
#include "utils/array.h"
|
|
#include "utils/builtins.h"
|
|
#include "utils/date.h"
|
|
#include "utils/datetime.h"
|
|
#include "utils/json.h"
|
|
#include "utils/jsonfuncs.h"
|
|
#include "utils/lsyscache.h"
|
|
#include "utils/typcache.h"
|
|
|
|
typedef enum /* type categories for datum_to_json */
|
|
{
|
|
JSONTYPE_NULL, /* null, so we didn't bother to identify */
|
|
JSONTYPE_BOOL, /* boolean (built-in types only) */
|
|
JSONTYPE_NUMERIC, /* numeric (ditto) */
|
|
JSONTYPE_DATE, /* we use special formatting for datetimes */
|
|
JSONTYPE_TIMESTAMP,
|
|
JSONTYPE_TIMESTAMPTZ,
|
|
JSONTYPE_JSON, /* JSON itself (and JSONB) */
|
|
JSONTYPE_ARRAY, /* array */
|
|
JSONTYPE_COMPOSITE, /* composite */
|
|
JSONTYPE_CAST, /* something with an explicit cast to JSON */
|
|
JSONTYPE_OTHER /* all else */
|
|
} JsonTypeCategory;
|
|
|
|
|
|
/*
|
|
* Support for fast key uniqueness checking.
|
|
*
|
|
* We maintain a hash table of used keys in JSON objects for fast detection
|
|
* of duplicates.
|
|
*/
|
|
/* Common context for key uniqueness check */
|
|
typedef struct HTAB *JsonUniqueCheckState; /* hash table for key names */
|
|
|
|
/* Hash entry for JsonUniqueCheckState */
|
|
typedef struct JsonUniqueHashEntry
|
|
{
|
|
const char *key;
|
|
int key_len;
|
|
int object_id;
|
|
} JsonUniqueHashEntry;
|
|
|
|
/* Stack element for key uniqueness check during JSON parsing */
|
|
typedef struct JsonUniqueStackEntry
|
|
{
|
|
struct JsonUniqueStackEntry *parent;
|
|
int object_id;
|
|
} JsonUniqueStackEntry;
|
|
|
|
/* Context struct for key uniqueness check during JSON parsing */
|
|
typedef struct JsonUniqueParsingState
|
|
{
|
|
JsonLexContext *lex;
|
|
JsonUniqueCheckState check;
|
|
JsonUniqueStackEntry *stack;
|
|
int id_counter;
|
|
bool unique;
|
|
} JsonUniqueParsingState;
|
|
|
|
/* Context struct for key uniqueness check during JSON building */
|
|
typedef struct JsonUniqueBuilderState
|
|
{
|
|
JsonUniqueCheckState check; /* unique check */
|
|
StringInfoData skipped_keys; /* skipped keys with NULL values */
|
|
MemoryContext mcxt; /* context for saving skipped keys */
|
|
} JsonUniqueBuilderState;
|
|
|
|
|
|
/* State struct for JSON aggregation */
|
|
typedef struct JsonAggState
|
|
{
|
|
StringInfo str;
|
|
JsonTypeCategory key_category;
|
|
Oid key_output_func;
|
|
JsonTypeCategory val_category;
|
|
Oid val_output_func;
|
|
JsonUniqueBuilderState unique_check;
|
|
} JsonAggState;
|
|
|
|
static void composite_to_json(Datum composite, StringInfo result,
|
|
bool use_line_feeds);
|
|
static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims,
|
|
Datum *vals, bool *nulls, int *valcount,
|
|
JsonTypeCategory tcategory, Oid outfuncoid,
|
|
bool use_line_feeds);
|
|
static void array_to_json_internal(Datum array, StringInfo result,
|
|
bool use_line_feeds);
|
|
static void json_categorize_type(Oid typoid,
|
|
JsonTypeCategory *tcategory,
|
|
Oid *outfuncoid);
|
|
static void datum_to_json(Datum val, bool is_null, StringInfo result,
|
|
JsonTypeCategory tcategory, Oid outfuncoid,
|
|
bool key_scalar);
|
|
static void add_json(Datum val, bool is_null, StringInfo result,
|
|
Oid val_type, bool key_scalar);
|
|
static text *catenate_stringinfo_string(StringInfo buffer, const char *addon);
|
|
|
|
/*
|
|
* Input.
|
|
*/
|
|
Datum
|
|
json_in(PG_FUNCTION_ARGS)
|
|
{
|
|
char *json = PG_GETARG_CSTRING(0);
|
|
text *result = cstring_to_text(json);
|
|
JsonLexContext *lex;
|
|
|
|
/* validate it */
|
|
lex = makeJsonLexContext(result, false);
|
|
if (!pg_parse_json_or_errsave(lex, &nullSemAction, fcinfo->context))
|
|
PG_RETURN_NULL();
|
|
|
|
/* Internal representation is the same as text */
|
|
PG_RETURN_TEXT_P(result);
|
|
}
|
|
|
|
/*
|
|
* Output.
|
|
*/
|
|
Datum
|
|
json_out(PG_FUNCTION_ARGS)
|
|
{
|
|
/* we needn't detoast because text_to_cstring will handle that */
|
|
Datum txt = PG_GETARG_DATUM(0);
|
|
|
|
PG_RETURN_CSTRING(TextDatumGetCString(txt));
|
|
}
|
|
|
|
/*
|
|
* Binary send.
|
|
*/
|
|
Datum
|
|
json_send(PG_FUNCTION_ARGS)
|
|
{
|
|
text *t = PG_GETARG_TEXT_PP(0);
|
|
StringInfoData buf;
|
|
|
|
pq_begintypsend(&buf);
|
|
pq_sendtext(&buf, VARDATA_ANY(t), VARSIZE_ANY_EXHDR(t));
|
|
PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
|
|
}
|
|
|
|
/*
|
|
* Binary receive.
|
|
*/
|
|
Datum
|
|
json_recv(PG_FUNCTION_ARGS)
|
|
{
|
|
StringInfo buf = (StringInfo) PG_GETARG_POINTER(0);
|
|
char *str;
|
|
int nbytes;
|
|
JsonLexContext *lex;
|
|
|
|
str = pq_getmsgtext(buf, buf->len - buf->cursor, &nbytes);
|
|
|
|
/* Validate it. */
|
|
lex = makeJsonLexContextCstringLen(str, nbytes, GetDatabaseEncoding(), false);
|
|
pg_parse_json_or_ereport(lex, &nullSemAction);
|
|
|
|
PG_RETURN_TEXT_P(cstring_to_text_with_len(str, nbytes));
|
|
}
|
|
|
|
/*
|
|
* Determine how we want to print values of a given type in datum_to_json.
|
|
*
|
|
* Given the datatype OID, return its JsonTypeCategory, as well as the type's
|
|
* output function OID. If the returned category is JSONTYPE_CAST, we
|
|
* return the OID of the type->JSON cast function instead.
|
|
*/
|
|
static void
|
|
json_categorize_type(Oid typoid,
|
|
JsonTypeCategory *tcategory,
|
|
Oid *outfuncoid)
|
|
{
|
|
bool typisvarlena;
|
|
|
|
/* Look through any domain */
|
|
typoid = getBaseType(typoid);
|
|
|
|
*outfuncoid = InvalidOid;
|
|
|
|
/*
|
|
* We need to get the output function for everything except date and
|
|
* timestamp types, array and composite types, booleans, and non-builtin
|
|
* types where there's a cast to json.
|
|
*/
|
|
|
|
switch (typoid)
|
|
{
|
|
case BOOLOID:
|
|
*tcategory = JSONTYPE_BOOL;
|
|
break;
|
|
|
|
case INT2OID:
|
|
case INT4OID:
|
|
case INT8OID:
|
|
case FLOAT4OID:
|
|
case FLOAT8OID:
|
|
case NUMERICOID:
|
|
getTypeOutputInfo(typoid, outfuncoid, &typisvarlena);
|
|
*tcategory = JSONTYPE_NUMERIC;
|
|
break;
|
|
|
|
case DATEOID:
|
|
*tcategory = JSONTYPE_DATE;
|
|
break;
|
|
|
|
case TIMESTAMPOID:
|
|
*tcategory = JSONTYPE_TIMESTAMP;
|
|
break;
|
|
|
|
case TIMESTAMPTZOID:
|
|
*tcategory = JSONTYPE_TIMESTAMPTZ;
|
|
break;
|
|
|
|
case JSONOID:
|
|
case JSONBOID:
|
|
getTypeOutputInfo(typoid, outfuncoid, &typisvarlena);
|
|
*tcategory = JSONTYPE_JSON;
|
|
break;
|
|
|
|
default:
|
|
/* Check for arrays and composites */
|
|
if (OidIsValid(get_element_type(typoid)) || typoid == ANYARRAYOID
|
|
|| typoid == ANYCOMPATIBLEARRAYOID || typoid == RECORDARRAYOID)
|
|
*tcategory = JSONTYPE_ARRAY;
|
|
else if (type_is_rowtype(typoid)) /* includes RECORDOID */
|
|
*tcategory = JSONTYPE_COMPOSITE;
|
|
else
|
|
{
|
|
/* It's probably the general case ... */
|
|
*tcategory = JSONTYPE_OTHER;
|
|
/* but let's look for a cast to json, if it's not built-in */
|
|
if (typoid >= FirstNormalObjectId)
|
|
{
|
|
Oid castfunc;
|
|
CoercionPathType ctype;
|
|
|
|
ctype = find_coercion_pathway(JSONOID, typoid,
|
|
COERCION_EXPLICIT,
|
|
&castfunc);
|
|
if (ctype == COERCION_PATH_FUNC && OidIsValid(castfunc))
|
|
{
|
|
*tcategory = JSONTYPE_CAST;
|
|
*outfuncoid = castfunc;
|
|
}
|
|
else
|
|
{
|
|
/* non builtin type with no cast */
|
|
getTypeOutputInfo(typoid, outfuncoid, &typisvarlena);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* any other builtin type */
|
|
getTypeOutputInfo(typoid, outfuncoid, &typisvarlena);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Turn a Datum into JSON text, appending the string to "result".
|
|
*
|
|
* tcategory and outfuncoid are from a previous call to json_categorize_type,
|
|
* except that if is_null is true then they can be invalid.
|
|
*
|
|
* If key_scalar is true, the value is being printed as a key, so insist
|
|
* it's of an acceptable type, and force it to be quoted.
|
|
*/
|
|
static void
|
|
datum_to_json(Datum val, bool is_null, StringInfo result,
|
|
JsonTypeCategory tcategory, Oid outfuncoid,
|
|
bool key_scalar)
|
|
{
|
|
char *outputstr;
|
|
text *jsontext;
|
|
|
|
check_stack_depth();
|
|
|
|
/* callers are expected to ensure that null keys are not passed in */
|
|
Assert(!(key_scalar && is_null));
|
|
|
|
if (is_null)
|
|
{
|
|
appendStringInfoString(result, "null");
|
|
return;
|
|
}
|
|
|
|
if (key_scalar &&
|
|
(tcategory == JSONTYPE_ARRAY ||
|
|
tcategory == JSONTYPE_COMPOSITE ||
|
|
tcategory == JSONTYPE_JSON ||
|
|
tcategory == JSONTYPE_CAST))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("key value must be scalar, not array, composite, or json")));
|
|
|
|
switch (tcategory)
|
|
{
|
|
case JSONTYPE_ARRAY:
|
|
array_to_json_internal(val, result, false);
|
|
break;
|
|
case JSONTYPE_COMPOSITE:
|
|
composite_to_json(val, result, false);
|
|
break;
|
|
case JSONTYPE_BOOL:
|
|
outputstr = DatumGetBool(val) ? "true" : "false";
|
|
if (key_scalar)
|
|
escape_json(result, outputstr);
|
|
else
|
|
appendStringInfoString(result, outputstr);
|
|
break;
|
|
case JSONTYPE_NUMERIC:
|
|
outputstr = OidOutputFunctionCall(outfuncoid, val);
|
|
|
|
/*
|
|
* Don't call escape_json for a non-key if it's a valid JSON
|
|
* number.
|
|
*/
|
|
if (!key_scalar && IsValidJsonNumber(outputstr, strlen(outputstr)))
|
|
appendStringInfoString(result, outputstr);
|
|
else
|
|
escape_json(result, outputstr);
|
|
pfree(outputstr);
|
|
break;
|
|
case JSONTYPE_DATE:
|
|
{
|
|
char buf[MAXDATELEN + 1];
|
|
|
|
JsonEncodeDateTime(buf, val, DATEOID, NULL);
|
|
appendStringInfo(result, "\"%s\"", buf);
|
|
}
|
|
break;
|
|
case JSONTYPE_TIMESTAMP:
|
|
{
|
|
char buf[MAXDATELEN + 1];
|
|
|
|
JsonEncodeDateTime(buf, val, TIMESTAMPOID, NULL);
|
|
appendStringInfo(result, "\"%s\"", buf);
|
|
}
|
|
break;
|
|
case JSONTYPE_TIMESTAMPTZ:
|
|
{
|
|
char buf[MAXDATELEN + 1];
|
|
|
|
JsonEncodeDateTime(buf, val, TIMESTAMPTZOID, NULL);
|
|
appendStringInfo(result, "\"%s\"", buf);
|
|
}
|
|
break;
|
|
case JSONTYPE_JSON:
|
|
/* JSON and JSONB output will already be escaped */
|
|
outputstr = OidOutputFunctionCall(outfuncoid, val);
|
|
appendStringInfoString(result, outputstr);
|
|
pfree(outputstr);
|
|
break;
|
|
case JSONTYPE_CAST:
|
|
/* outfuncoid refers to a cast function, not an output function */
|
|
jsontext = DatumGetTextPP(OidFunctionCall1(outfuncoid, val));
|
|
outputstr = text_to_cstring(jsontext);
|
|
appendStringInfoString(result, outputstr);
|
|
pfree(outputstr);
|
|
pfree(jsontext);
|
|
break;
|
|
default:
|
|
outputstr = OidOutputFunctionCall(outfuncoid, val);
|
|
escape_json(result, outputstr);
|
|
pfree(outputstr);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Encode 'value' of datetime type 'typid' into JSON string in ISO format using
|
|
* optionally preallocated buffer 'buf'. Optional 'tzp' determines time-zone
|
|
* offset (in seconds) in which we want to show timestamptz.
|
|
*/
|
|
char *
|
|
JsonEncodeDateTime(char *buf, Datum value, Oid typid, const int *tzp)
|
|
{
|
|
if (!buf)
|
|
buf = palloc(MAXDATELEN + 1);
|
|
|
|
switch (typid)
|
|
{
|
|
case DATEOID:
|
|
{
|
|
DateADT date;
|
|
struct pg_tm tm;
|
|
|
|
date = DatumGetDateADT(value);
|
|
|
|
/* Same as date_out(), but forcing DateStyle */
|
|
if (DATE_NOT_FINITE(date))
|
|
EncodeSpecialDate(date, buf);
|
|
else
|
|
{
|
|
j2date(date + POSTGRES_EPOCH_JDATE,
|
|
&(tm.tm_year), &(tm.tm_mon), &(tm.tm_mday));
|
|
EncodeDateOnly(&tm, USE_XSD_DATES, buf);
|
|
}
|
|
}
|
|
break;
|
|
case TIMEOID:
|
|
{
|
|
TimeADT time = DatumGetTimeADT(value);
|
|
struct pg_tm tt,
|
|
*tm = &tt;
|
|
fsec_t fsec;
|
|
|
|
/* Same as time_out(), but forcing DateStyle */
|
|
time2tm(time, tm, &fsec);
|
|
EncodeTimeOnly(tm, fsec, false, 0, USE_XSD_DATES, buf);
|
|
}
|
|
break;
|
|
case TIMETZOID:
|
|
{
|
|
TimeTzADT *time = DatumGetTimeTzADTP(value);
|
|
struct pg_tm tt,
|
|
*tm = &tt;
|
|
fsec_t fsec;
|
|
int tz;
|
|
|
|
/* Same as timetz_out(), but forcing DateStyle */
|
|
timetz2tm(time, tm, &fsec, &tz);
|
|
EncodeTimeOnly(tm, fsec, true, tz, USE_XSD_DATES, buf);
|
|
}
|
|
break;
|
|
case TIMESTAMPOID:
|
|
{
|
|
Timestamp timestamp;
|
|
struct pg_tm tm;
|
|
fsec_t fsec;
|
|
|
|
timestamp = DatumGetTimestamp(value);
|
|
/* Same as timestamp_out(), but forcing DateStyle */
|
|
if (TIMESTAMP_NOT_FINITE(timestamp))
|
|
EncodeSpecialTimestamp(timestamp, buf);
|
|
else if (timestamp2tm(timestamp, NULL, &tm, &fsec, NULL, NULL) == 0)
|
|
EncodeDateTime(&tm, fsec, false, 0, NULL, USE_XSD_DATES, buf);
|
|
else
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
|
|
errmsg("timestamp out of range")));
|
|
}
|
|
break;
|
|
case TIMESTAMPTZOID:
|
|
{
|
|
TimestampTz timestamp;
|
|
struct pg_tm tm;
|
|
int tz;
|
|
fsec_t fsec;
|
|
const char *tzn = NULL;
|
|
|
|
timestamp = DatumGetTimestampTz(value);
|
|
|
|
/*
|
|
* If a time zone is specified, we apply the time-zone shift,
|
|
* convert timestamptz to pg_tm as if it were without a time
|
|
* zone, and then use the specified time zone for converting
|
|
* the timestamp into a string.
|
|
*/
|
|
if (tzp)
|
|
{
|
|
tz = *tzp;
|
|
timestamp -= (TimestampTz) tz * USECS_PER_SEC;
|
|
}
|
|
|
|
/* Same as timestamptz_out(), but forcing DateStyle */
|
|
if (TIMESTAMP_NOT_FINITE(timestamp))
|
|
EncodeSpecialTimestamp(timestamp, buf);
|
|
else if (timestamp2tm(timestamp, tzp ? NULL : &tz, &tm, &fsec,
|
|
tzp ? NULL : &tzn, NULL) == 0)
|
|
{
|
|
if (tzp)
|
|
tm.tm_isdst = 1; /* set time-zone presence flag */
|
|
|
|
EncodeDateTime(&tm, fsec, true, tz, tzn, USE_XSD_DATES, buf);
|
|
}
|
|
else
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
|
|
errmsg("timestamp out of range")));
|
|
}
|
|
break;
|
|
default:
|
|
elog(ERROR, "unknown jsonb value datetime type oid %u", typid);
|
|
return NULL;
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
/*
|
|
* Process a single dimension of an array.
|
|
* If it's the innermost dimension, output the values, otherwise call
|
|
* ourselves recursively to process the next dimension.
|
|
*/
|
|
static void
|
|
array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, Datum *vals,
|
|
bool *nulls, int *valcount, JsonTypeCategory tcategory,
|
|
Oid outfuncoid, bool use_line_feeds)
|
|
{
|
|
int i;
|
|
const char *sep;
|
|
|
|
Assert(dim < ndims);
|
|
|
|
sep = use_line_feeds ? ",\n " : ",";
|
|
|
|
appendStringInfoChar(result, '[');
|
|
|
|
for (i = 1; i <= dims[dim]; i++)
|
|
{
|
|
if (i > 1)
|
|
appendStringInfoString(result, sep);
|
|
|
|
if (dim + 1 == ndims)
|
|
{
|
|
datum_to_json(vals[*valcount], nulls[*valcount], result, tcategory,
|
|
outfuncoid, false);
|
|
(*valcount)++;
|
|
}
|
|
else
|
|
{
|
|
/*
|
|
* Do we want line feeds on inner dimensions of arrays? For now
|
|
* we'll say no.
|
|
*/
|
|
array_dim_to_json(result, dim + 1, ndims, dims, vals, nulls,
|
|
valcount, tcategory, outfuncoid, false);
|
|
}
|
|
}
|
|
|
|
appendStringInfoChar(result, ']');
|
|
}
|
|
|
|
/*
|
|
* Turn an array into JSON.
|
|
*/
|
|
static void
|
|
array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds)
|
|
{
|
|
ArrayType *v = DatumGetArrayTypeP(array);
|
|
Oid element_type = ARR_ELEMTYPE(v);
|
|
int *dim;
|
|
int ndim;
|
|
int nitems;
|
|
int count = 0;
|
|
Datum *elements;
|
|
bool *nulls;
|
|
int16 typlen;
|
|
bool typbyval;
|
|
char typalign;
|
|
JsonTypeCategory tcategory;
|
|
Oid outfuncoid;
|
|
|
|
ndim = ARR_NDIM(v);
|
|
dim = ARR_DIMS(v);
|
|
nitems = ArrayGetNItems(ndim, dim);
|
|
|
|
if (nitems <= 0)
|
|
{
|
|
appendStringInfoString(result, "[]");
|
|
return;
|
|
}
|
|
|
|
get_typlenbyvalalign(element_type,
|
|
&typlen, &typbyval, &typalign);
|
|
|
|
json_categorize_type(element_type,
|
|
&tcategory, &outfuncoid);
|
|
|
|
deconstruct_array(v, element_type, typlen, typbyval,
|
|
typalign, &elements, &nulls,
|
|
&nitems);
|
|
|
|
array_dim_to_json(result, 0, ndim, dim, elements, nulls, &count, tcategory,
|
|
outfuncoid, use_line_feeds);
|
|
|
|
pfree(elements);
|
|
pfree(nulls);
|
|
}
|
|
|
|
/*
|
|
* Turn a composite / record into JSON.
|
|
*/
|
|
static void
|
|
composite_to_json(Datum composite, StringInfo result, bool use_line_feeds)
|
|
{
|
|
HeapTupleHeader td;
|
|
Oid tupType;
|
|
int32 tupTypmod;
|
|
TupleDesc tupdesc;
|
|
HeapTupleData tmptup,
|
|
*tuple;
|
|
int i;
|
|
bool needsep = false;
|
|
const char *sep;
|
|
|
|
sep = use_line_feeds ? ",\n " : ",";
|
|
|
|
td = DatumGetHeapTupleHeader(composite);
|
|
|
|
/* Extract rowtype info and find a tupdesc */
|
|
tupType = HeapTupleHeaderGetTypeId(td);
|
|
tupTypmod = HeapTupleHeaderGetTypMod(td);
|
|
tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
|
|
|
|
/* Build a temporary HeapTuple control structure */
|
|
tmptup.t_len = HeapTupleHeaderGetDatumLength(td);
|
|
tmptup.t_data = td;
|
|
tuple = &tmptup;
|
|
|
|
appendStringInfoChar(result, '{');
|
|
|
|
for (i = 0; i < tupdesc->natts; i++)
|
|
{
|
|
Datum val;
|
|
bool isnull;
|
|
char *attname;
|
|
JsonTypeCategory tcategory;
|
|
Oid outfuncoid;
|
|
Form_pg_attribute att = TupleDescAttr(tupdesc, i);
|
|
|
|
if (att->attisdropped)
|
|
continue;
|
|
|
|
if (needsep)
|
|
appendStringInfoString(result, sep);
|
|
needsep = true;
|
|
|
|
attname = NameStr(att->attname);
|
|
escape_json(result, attname);
|
|
appendStringInfoChar(result, ':');
|
|
|
|
val = heap_getattr(tuple, i + 1, tupdesc, &isnull);
|
|
|
|
if (isnull)
|
|
{
|
|
tcategory = JSONTYPE_NULL;
|
|
outfuncoid = InvalidOid;
|
|
}
|
|
else
|
|
json_categorize_type(att->atttypid, &tcategory, &outfuncoid);
|
|
|
|
datum_to_json(val, isnull, result, tcategory, outfuncoid, false);
|
|
}
|
|
|
|
appendStringInfoChar(result, '}');
|
|
ReleaseTupleDesc(tupdesc);
|
|
}
|
|
|
|
/*
|
|
* Append JSON text for "val" to "result".
|
|
*
|
|
* This is just a thin wrapper around datum_to_json. If the same type will be
|
|
* printed many times, avoid using this; better to do the json_categorize_type
|
|
* lookups only once.
|
|
*/
|
|
static void
|
|
add_json(Datum val, bool is_null, StringInfo result,
|
|
Oid val_type, bool key_scalar)
|
|
{
|
|
JsonTypeCategory tcategory;
|
|
Oid outfuncoid;
|
|
|
|
if (val_type == InvalidOid)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("could not determine input data type")));
|
|
|
|
if (is_null)
|
|
{
|
|
tcategory = JSONTYPE_NULL;
|
|
outfuncoid = InvalidOid;
|
|
}
|
|
else
|
|
json_categorize_type(val_type,
|
|
&tcategory, &outfuncoid);
|
|
|
|
datum_to_json(val, is_null, result, tcategory, outfuncoid, key_scalar);
|
|
}
|
|
|
|
/*
|
|
* SQL function array_to_json(row)
|
|
*/
|
|
Datum
|
|
array_to_json(PG_FUNCTION_ARGS)
|
|
{
|
|
Datum array = PG_GETARG_DATUM(0);
|
|
StringInfo result;
|
|
|
|
result = makeStringInfo();
|
|
|
|
array_to_json_internal(array, result, false);
|
|
|
|
PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len));
|
|
}
|
|
|
|
/*
|
|
* SQL function array_to_json(row, prettybool)
|
|
*/
|
|
Datum
|
|
array_to_json_pretty(PG_FUNCTION_ARGS)
|
|
{
|
|
Datum array = PG_GETARG_DATUM(0);
|
|
bool use_line_feeds = PG_GETARG_BOOL(1);
|
|
StringInfo result;
|
|
|
|
result = makeStringInfo();
|
|
|
|
array_to_json_internal(array, result, use_line_feeds);
|
|
|
|
PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len));
|
|
}
|
|
|
|
/*
|
|
* SQL function row_to_json(row)
|
|
*/
|
|
Datum
|
|
row_to_json(PG_FUNCTION_ARGS)
|
|
{
|
|
Datum array = PG_GETARG_DATUM(0);
|
|
StringInfo result;
|
|
|
|
result = makeStringInfo();
|
|
|
|
composite_to_json(array, result, false);
|
|
|
|
PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len));
|
|
}
|
|
|
|
/*
|
|
* SQL function row_to_json(row, prettybool)
|
|
*/
|
|
Datum
|
|
row_to_json_pretty(PG_FUNCTION_ARGS)
|
|
{
|
|
Datum array = PG_GETARG_DATUM(0);
|
|
bool use_line_feeds = PG_GETARG_BOOL(1);
|
|
StringInfo result;
|
|
|
|
result = makeStringInfo();
|
|
|
|
composite_to_json(array, result, use_line_feeds);
|
|
|
|
PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len));
|
|
}
|
|
|
|
/*
|
|
* Is the given type immutable when coming out of a JSON context?
|
|
*
|
|
* At present, datetimes are all considered mutable, because they
|
|
* depend on timezone. XXX we should also drill down into objects
|
|
* and arrays, but do not.
|
|
*/
|
|
bool
|
|
to_json_is_immutable(Oid typoid)
|
|
{
|
|
JsonTypeCategory tcategory;
|
|
Oid outfuncoid;
|
|
|
|
json_categorize_type(typoid, &tcategory, &outfuncoid);
|
|
|
|
switch (tcategory)
|
|
{
|
|
case JSONTYPE_BOOL:
|
|
case JSONTYPE_JSON:
|
|
case JSONTYPE_NULL:
|
|
return true;
|
|
|
|
case JSONTYPE_DATE:
|
|
case JSONTYPE_TIMESTAMP:
|
|
case JSONTYPE_TIMESTAMPTZ:
|
|
return false;
|
|
|
|
case JSONTYPE_ARRAY:
|
|
return false; /* TODO recurse into elements */
|
|
|
|
case JSONTYPE_COMPOSITE:
|
|
return false; /* TODO recurse into fields */
|
|
|
|
case JSONTYPE_NUMERIC:
|
|
case JSONTYPE_CAST:
|
|
case JSONTYPE_OTHER:
|
|
return func_volatile(outfuncoid) == PROVOLATILE_IMMUTABLE;
|
|
}
|
|
|
|
return false; /* not reached */
|
|
}
|
|
|
|
/*
|
|
* SQL function to_json(anyvalue)
|
|
*/
|
|
Datum
|
|
to_json(PG_FUNCTION_ARGS)
|
|
{
|
|
Datum val = PG_GETARG_DATUM(0);
|
|
Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0);
|
|
StringInfo result;
|
|
JsonTypeCategory tcategory;
|
|
Oid outfuncoid;
|
|
|
|
if (val_type == InvalidOid)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("could not determine input data type")));
|
|
|
|
json_categorize_type(val_type,
|
|
&tcategory, &outfuncoid);
|
|
|
|
result = makeStringInfo();
|
|
|
|
datum_to_json(val, false, result, tcategory, outfuncoid, false);
|
|
|
|
PG_RETURN_TEXT_P(cstring_to_text_with_len(result->data, result->len));
|
|
}
|
|
|
|
/*
|
|
* json_agg transition function
|
|
*
|
|
* aggregate input column as a json array value.
|
|
*/
|
|
static Datum
|
|
json_agg_transfn_worker(FunctionCallInfo fcinfo, bool absent_on_null)
|
|
{
|
|
MemoryContext aggcontext,
|
|
oldcontext;
|
|
JsonAggState *state;
|
|
Datum val;
|
|
|
|
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))
|
|
{
|
|
Oid arg_type = get_fn_expr_argtype(fcinfo->flinfo, 1);
|
|
|
|
if (arg_type == InvalidOid)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("could not determine input data type")));
|
|
|
|
/*
|
|
* Make this state object in a context where it will persist for the
|
|
* duration of the aggregate call. MemoryContextSwitchTo is only
|
|
* needed the first time, as the StringInfo routines make sure they
|
|
* use the right context to enlarge the object if necessary.
|
|
*/
|
|
oldcontext = MemoryContextSwitchTo(aggcontext);
|
|
state = (JsonAggState *) palloc(sizeof(JsonAggState));
|
|
state->str = makeStringInfo();
|
|
MemoryContextSwitchTo(oldcontext);
|
|
|
|
appendStringInfoChar(state->str, '[');
|
|
json_categorize_type(arg_type, &state->val_category,
|
|
&state->val_output_func);
|
|
}
|
|
else
|
|
{
|
|
state = (JsonAggState *) PG_GETARG_POINTER(0);
|
|
}
|
|
|
|
if (absent_on_null && PG_ARGISNULL(1))
|
|
PG_RETURN_POINTER(state);
|
|
|
|
if (state->str->len > 1)
|
|
appendStringInfoString(state->str, ", ");
|
|
|
|
/* fast path for NULLs */
|
|
if (PG_ARGISNULL(1))
|
|
{
|
|
datum_to_json((Datum) 0, true, state->str, JSONTYPE_NULL,
|
|
InvalidOid, false);
|
|
PG_RETURN_POINTER(state);
|
|
}
|
|
|
|
val = PG_GETARG_DATUM(1);
|
|
|
|
/* add some whitespace if structured type and not first item */
|
|
if (!PG_ARGISNULL(0) && state->str->len > 1 &&
|
|
(state->val_category == JSONTYPE_ARRAY ||
|
|
state->val_category == JSONTYPE_COMPOSITE))
|
|
{
|
|
appendStringInfoString(state->str, "\n ");
|
|
}
|
|
|
|
datum_to_json(val, false, state->str, state->val_category,
|
|
state->val_output_func, false);
|
|
|
|
/*
|
|
* The transition type for json_agg() is declared to be "internal", which
|
|
* is a pass-by-value type the same size as a pointer. So we can safely
|
|
* pass the JsonAggState pointer through nodeAgg.c's machinations.
|
|
*/
|
|
PG_RETURN_POINTER(state);
|
|
}
|
|
|
|
|
|
/*
|
|
* json_agg aggregate function
|
|
*/
|
|
Datum
|
|
json_agg_transfn(PG_FUNCTION_ARGS)
|
|
{
|
|
return json_agg_transfn_worker(fcinfo, false);
|
|
}
|
|
|
|
/*
|
|
* json_agg_strict aggregate function
|
|
*/
|
|
Datum
|
|
json_agg_strict_transfn(PG_FUNCTION_ARGS)
|
|
{
|
|
return json_agg_transfn_worker(fcinfo, true);
|
|
}
|
|
|
|
/*
|
|
* json_agg final function
|
|
*/
|
|
Datum
|
|
json_agg_finalfn(PG_FUNCTION_ARGS)
|
|
{
|
|
JsonAggState *state;
|
|
|
|
/* cannot be called directly because of internal-type argument */
|
|
Assert(AggCheckCallContext(fcinfo, NULL));
|
|
|
|
state = PG_ARGISNULL(0) ?
|
|
NULL :
|
|
(JsonAggState *) PG_GETARG_POINTER(0);
|
|
|
|
/* NULL result for no rows in, as is standard with aggregates */
|
|
if (state == NULL)
|
|
PG_RETURN_NULL();
|
|
|
|
/* Else return state with appropriate array terminator added */
|
|
PG_RETURN_TEXT_P(catenate_stringinfo_string(state->str, "]"));
|
|
}
|
|
|
|
/* Functions implementing hash table for key uniqueness check */
|
|
static uint32
|
|
json_unique_hash(const void *key, Size keysize)
|
|
{
|
|
const JsonUniqueHashEntry *entry = (JsonUniqueHashEntry *) key;
|
|
uint32 hash = hash_bytes_uint32(entry->object_id);
|
|
|
|
hash ^= hash_bytes((const unsigned char *) entry->key, entry->key_len);
|
|
|
|
return DatumGetUInt32(hash);
|
|
}
|
|
|
|
static int
|
|
json_unique_hash_match(const void *key1, const void *key2, Size keysize)
|
|
{
|
|
const JsonUniqueHashEntry *entry1 = (const JsonUniqueHashEntry *) key1;
|
|
const JsonUniqueHashEntry *entry2 = (const JsonUniqueHashEntry *) key2;
|
|
|
|
if (entry1->object_id != entry2->object_id)
|
|
return entry1->object_id > entry2->object_id ? 1 : -1;
|
|
|
|
if (entry1->key_len != entry2->key_len)
|
|
return entry1->key_len > entry2->key_len ? 1 : -1;
|
|
|
|
return strncmp(entry1->key, entry2->key, entry1->key_len);
|
|
}
|
|
|
|
/*
|
|
* Uniqueness detection support.
|
|
*
|
|
* In order to detect uniqueness during building or parsing of a JSON
|
|
* object, we maintain a hash table of key names already seen.
|
|
*/
|
|
static void
|
|
json_unique_check_init(JsonUniqueCheckState *cxt)
|
|
{
|
|
HASHCTL ctl;
|
|
|
|
memset(&ctl, 0, sizeof(ctl));
|
|
ctl.keysize = sizeof(JsonUniqueHashEntry);
|
|
ctl.entrysize = sizeof(JsonUniqueHashEntry);
|
|
ctl.hcxt = CurrentMemoryContext;
|
|
ctl.hash = json_unique_hash;
|
|
ctl.match = json_unique_hash_match;
|
|
|
|
*cxt = hash_create("json object hashtable",
|
|
32,
|
|
&ctl,
|
|
HASH_ELEM | HASH_CONTEXT | HASH_FUNCTION | HASH_COMPARE);
|
|
}
|
|
|
|
static void
|
|
json_unique_builder_init(JsonUniqueBuilderState *cxt)
|
|
{
|
|
json_unique_check_init(&cxt->check);
|
|
cxt->mcxt = CurrentMemoryContext;
|
|
cxt->skipped_keys.data = NULL;
|
|
}
|
|
|
|
static bool
|
|
json_unique_check_key(JsonUniqueCheckState *cxt, const char *key, int object_id)
|
|
{
|
|
JsonUniqueHashEntry entry;
|
|
bool found;
|
|
|
|
entry.key = key;
|
|
entry.key_len = strlen(key);
|
|
entry.object_id = object_id;
|
|
|
|
(void) hash_search(*cxt, &entry, HASH_ENTER, &found);
|
|
|
|
return !found;
|
|
}
|
|
|
|
/*
|
|
* On-demand initialization of a throwaway StringInfo. This is used to
|
|
* read a key name that we don't need to store in the output object, for
|
|
* duplicate key detection when the value is NULL.
|
|
*/
|
|
static StringInfo
|
|
json_unique_builder_get_throwawaybuf(JsonUniqueBuilderState *cxt)
|
|
{
|
|
StringInfo out = &cxt->skipped_keys;
|
|
|
|
if (!out->data)
|
|
{
|
|
MemoryContext oldcxt = MemoryContextSwitchTo(cxt->mcxt);
|
|
|
|
initStringInfo(out);
|
|
MemoryContextSwitchTo(oldcxt);
|
|
}
|
|
else
|
|
/* Just reset the string to empty */
|
|
out->len = 0;
|
|
|
|
return out;
|
|
}
|
|
|
|
/*
|
|
* json_object_agg transition function.
|
|
*
|
|
* aggregate two input columns as a single json object value.
|
|
*/
|
|
static Datum
|
|
json_object_agg_transfn_worker(FunctionCallInfo fcinfo,
|
|
bool absent_on_null, bool unique_keys)
|
|
{
|
|
MemoryContext aggcontext,
|
|
oldcontext;
|
|
JsonAggState *state;
|
|
StringInfo out;
|
|
Datum arg;
|
|
bool skip;
|
|
int key_offset;
|
|
|
|
if (!AggCheckCallContext(fcinfo, &aggcontext))
|
|
{
|
|
/* cannot be called directly because of internal-type argument */
|
|
elog(ERROR, "json_object_agg_transfn called in non-aggregate context");
|
|
}
|
|
|
|
if (PG_ARGISNULL(0))
|
|
{
|
|
Oid arg_type;
|
|
|
|
/*
|
|
* Make the StringInfo in a context where it will persist for the
|
|
* duration of the aggregate call. Switching context is only needed
|
|
* for this initial step, as the StringInfo and dynahash routines make
|
|
* sure they use the right context to enlarge the object if necessary.
|
|
*/
|
|
oldcontext = MemoryContextSwitchTo(aggcontext);
|
|
state = (JsonAggState *) palloc(sizeof(JsonAggState));
|
|
state->str = makeStringInfo();
|
|
if (unique_keys)
|
|
json_unique_builder_init(&state->unique_check);
|
|
else
|
|
memset(&state->unique_check, 0, sizeof(state->unique_check));
|
|
MemoryContextSwitchTo(oldcontext);
|
|
|
|
arg_type = get_fn_expr_argtype(fcinfo->flinfo, 1);
|
|
|
|
if (arg_type == InvalidOid)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("could not determine data type for argument %d", 1)));
|
|
|
|
json_categorize_type(arg_type, &state->key_category,
|
|
&state->key_output_func);
|
|
|
|
arg_type = get_fn_expr_argtype(fcinfo->flinfo, 2);
|
|
|
|
if (arg_type == InvalidOid)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("could not determine data type for argument %d", 2)));
|
|
|
|
json_categorize_type(arg_type, &state->val_category,
|
|
&state->val_output_func);
|
|
|
|
appendStringInfoString(state->str, "{ ");
|
|
}
|
|
else
|
|
{
|
|
state = (JsonAggState *) PG_GETARG_POINTER(0);
|
|
}
|
|
|
|
/*
|
|
* Note: since json_object_agg() is declared as taking type "any", the
|
|
* parser will not do any type conversion on unknown-type literals (that
|
|
* is, undecorated strings or NULLs). Such values will arrive here as
|
|
* type UNKNOWN, which fortunately does not matter to us, since
|
|
* unknownout() works fine.
|
|
*/
|
|
|
|
if (PG_ARGISNULL(1))
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
|
|
errmsg("null value not allowed for object key")));
|
|
|
|
/* Skip null values if absent_on_null */
|
|
skip = absent_on_null && PG_ARGISNULL(2);
|
|
|
|
if (skip)
|
|
{
|
|
/*
|
|
* We got a NULL value and we're not storing those; if we're not
|
|
* testing key uniqueness, we're done. If we are, use the throwaway
|
|
* buffer to store the key name so that we can check it.
|
|
*/
|
|
if (!unique_keys)
|
|
PG_RETURN_POINTER(state);
|
|
|
|
out = json_unique_builder_get_throwawaybuf(&state->unique_check);
|
|
}
|
|
else
|
|
{
|
|
out = state->str;
|
|
|
|
/*
|
|
* Append comma delimiter only if we have already output some fields
|
|
* after the initial string "{ ".
|
|
*/
|
|
if (out->len > 2)
|
|
appendStringInfoString(out, ", ");
|
|
}
|
|
|
|
arg = PG_GETARG_DATUM(1);
|
|
|
|
key_offset = out->len;
|
|
|
|
datum_to_json(arg, false, out, state->key_category,
|
|
state->key_output_func, true);
|
|
|
|
if (unique_keys)
|
|
{
|
|
const char *key = &out->data[key_offset];
|
|
|
|
if (!json_unique_check_key(&state->unique_check.check, key, 0))
|
|
ereport(ERROR,
|
|
errcode(ERRCODE_DUPLICATE_JSON_OBJECT_KEY_VALUE),
|
|
errmsg("duplicate JSON key %s", key));
|
|
|
|
if (skip)
|
|
PG_RETURN_POINTER(state);
|
|
}
|
|
|
|
appendStringInfoString(state->str, " : ");
|
|
|
|
if (PG_ARGISNULL(2))
|
|
arg = (Datum) 0;
|
|
else
|
|
arg = PG_GETARG_DATUM(2);
|
|
|
|
datum_to_json(arg, PG_ARGISNULL(2), state->str, state->val_category,
|
|
state->val_output_func, false);
|
|
|
|
PG_RETURN_POINTER(state);
|
|
}
|
|
|
|
/*
|
|
* json_object_agg aggregate function
|
|
*/
|
|
Datum
|
|
json_object_agg_transfn(PG_FUNCTION_ARGS)
|
|
{
|
|
return json_object_agg_transfn_worker(fcinfo, false, false);
|
|
}
|
|
|
|
/*
|
|
* json_object_agg_strict aggregate function
|
|
*/
|
|
Datum
|
|
json_object_agg_strict_transfn(PG_FUNCTION_ARGS)
|
|
{
|
|
return json_object_agg_transfn_worker(fcinfo, true, false);
|
|
}
|
|
|
|
/*
|
|
* json_object_agg_unique aggregate function
|
|
*/
|
|
Datum
|
|
json_object_agg_unique_transfn(PG_FUNCTION_ARGS)
|
|
{
|
|
return json_object_agg_transfn_worker(fcinfo, false, true);
|
|
}
|
|
|
|
/*
|
|
* json_object_agg_unique_strict aggregate function
|
|
*/
|
|
Datum
|
|
json_object_agg_unique_strict_transfn(PG_FUNCTION_ARGS)
|
|
{
|
|
return json_object_agg_transfn_worker(fcinfo, true, true);
|
|
}
|
|
|
|
/*
|
|
* json_object_agg final function.
|
|
*/
|
|
Datum
|
|
json_object_agg_finalfn(PG_FUNCTION_ARGS)
|
|
{
|
|
JsonAggState *state;
|
|
|
|
/* cannot be called directly because of internal-type argument */
|
|
Assert(AggCheckCallContext(fcinfo, NULL));
|
|
|
|
state = PG_ARGISNULL(0) ? NULL : (JsonAggState *) PG_GETARG_POINTER(0);
|
|
|
|
/* NULL result for no rows in, as is standard with aggregates */
|
|
if (state == NULL)
|
|
PG_RETURN_NULL();
|
|
|
|
/* Else return state with appropriate object terminator added */
|
|
PG_RETURN_TEXT_P(catenate_stringinfo_string(state->str, " }"));
|
|
}
|
|
|
|
/*
|
|
* Helper function for aggregates: return given StringInfo's contents plus
|
|
* specified trailing string, as a text datum. We need this because aggregate
|
|
* final functions are not allowed to modify the aggregate state.
|
|
*/
|
|
static text *
|
|
catenate_stringinfo_string(StringInfo buffer, const char *addon)
|
|
{
|
|
/* custom version of cstring_to_text_with_len */
|
|
int buflen = buffer->len;
|
|
int addlen = strlen(addon);
|
|
text *result = (text *) palloc(buflen + addlen + VARHDRSZ);
|
|
|
|
SET_VARSIZE(result, buflen + addlen + VARHDRSZ);
|
|
memcpy(VARDATA(result), buffer->data, buflen);
|
|
memcpy(VARDATA(result) + buflen, addon, addlen);
|
|
|
|
return result;
|
|
}
|
|
|
|
Datum
|
|
json_build_object_worker(int nargs, Datum *args, bool *nulls, Oid *types,
|
|
bool absent_on_null, bool unique_keys)
|
|
{
|
|
int i;
|
|
const char *sep = "";
|
|
StringInfo result;
|
|
JsonUniqueBuilderState unique_check;
|
|
|
|
if (nargs % 2 != 0)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
|
errmsg("argument list must have even number of elements"),
|
|
/* translator: %s is a SQL function name */
|
|
errhint("The arguments of %s must consist of alternating keys and values.",
|
|
"json_build_object()")));
|
|
|
|
result = makeStringInfo();
|
|
|
|
appendStringInfoChar(result, '{');
|
|
|
|
if (unique_keys)
|
|
json_unique_builder_init(&unique_check);
|
|
|
|
for (i = 0; i < nargs; i += 2)
|
|
{
|
|
StringInfo out;
|
|
bool skip;
|
|
int key_offset;
|
|
|
|
/* Skip null values if absent_on_null */
|
|
skip = absent_on_null && nulls[i + 1];
|
|
|
|
if (skip)
|
|
{
|
|
/* If key uniqueness check is needed we must save skipped keys */
|
|
if (!unique_keys)
|
|
continue;
|
|
|
|
out = json_unique_builder_get_throwawaybuf(&unique_check);
|
|
}
|
|
else
|
|
{
|
|
appendStringInfoString(result, sep);
|
|
sep = ", ";
|
|
out = result;
|
|
}
|
|
|
|
/* process key */
|
|
if (nulls[i])
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
|
|
errmsg("null value not allowed for object key")));
|
|
|
|
/* save key offset before appending it */
|
|
key_offset = out->len;
|
|
|
|
add_json(args[i], false, out, types[i], true);
|
|
|
|
if (unique_keys)
|
|
{
|
|
/* check key uniqueness after key appending */
|
|
const char *key = &out->data[key_offset];
|
|
|
|
if (!json_unique_check_key(&unique_check.check, key, 0))
|
|
ereport(ERROR,
|
|
errcode(ERRCODE_DUPLICATE_JSON_OBJECT_KEY_VALUE),
|
|
errmsg("duplicate JSON key %s", key));
|
|
|
|
if (skip)
|
|
continue;
|
|
}
|
|
|
|
appendStringInfoString(result, " : ");
|
|
|
|
/* process value */
|
|
add_json(args[i + 1], nulls[i + 1], result, types[i + 1], false);
|
|
}
|
|
|
|
appendStringInfoChar(result, '}');
|
|
|
|
return PointerGetDatum(cstring_to_text_with_len(result->data, result->len));
|
|
}
|
|
|
|
/*
|
|
* SQL function json_build_object(variadic "any")
|
|
*/
|
|
Datum
|
|
json_build_object(PG_FUNCTION_ARGS)
|
|
{
|
|
Datum *args;
|
|
bool *nulls;
|
|
Oid *types;
|
|
|
|
/* build argument values to build the object */
|
|
int nargs = extract_variadic_args(fcinfo, 0, true,
|
|
&args, &types, &nulls);
|
|
|
|
if (nargs < 0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_DATUM(json_build_object_worker(nargs, args, nulls, types, false, false));
|
|
}
|
|
|
|
/*
|
|
* 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));
|
|
}
|
|
|
|
Datum
|
|
json_build_array_worker(int nargs, Datum *args, bool *nulls, Oid *types,
|
|
bool absent_on_null)
|
|
{
|
|
int i;
|
|
const char *sep = "";
|
|
StringInfo result;
|
|
|
|
result = makeStringInfo();
|
|
|
|
appendStringInfoChar(result, '[');
|
|
|
|
for (i = 0; i < nargs; i++)
|
|
{
|
|
if (absent_on_null && nulls[i])
|
|
continue;
|
|
|
|
appendStringInfoString(result, sep);
|
|
sep = ", ";
|
|
add_json(args[i], nulls[i], result, types[i], false);
|
|
}
|
|
|
|
appendStringInfoChar(result, ']');
|
|
|
|
return PointerGetDatum(cstring_to_text_with_len(result->data, result->len));
|
|
}
|
|
|
|
/*
|
|
* SQL function json_build_array(variadic "any")
|
|
*/
|
|
Datum
|
|
json_build_array(PG_FUNCTION_ARGS)
|
|
{
|
|
Datum *args;
|
|
bool *nulls;
|
|
Oid *types;
|
|
|
|
/* build argument values to build the object */
|
|
int nargs = extract_variadic_args(fcinfo, 0, true,
|
|
&args, &types, &nulls);
|
|
|
|
if (nargs < 0)
|
|
PG_RETURN_NULL();
|
|
|
|
PG_RETURN_DATUM(json_build_array_worker(nargs, args, nulls, types, false));
|
|
}
|
|
|
|
/*
|
|
* 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 key/value 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_builtin(in_array, TEXTOID, &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 (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 key 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_builtin(key_array, TEXTOID, &key_datums, &key_nulls, &key_count);
|
|
deconstruct_array_builtin(val_array, TEXTOID, &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 (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.
|
|
*/
|
|
void
|
|
escape_json(StringInfo buf, const char *str)
|
|
{
|
|
const char *p;
|
|
|
|
appendStringInfoCharMacro(buf, '"');
|
|
for (p = str; *p; p++)
|
|
{
|
|
switch (*p)
|
|
{
|
|
case '\b':
|
|
appendStringInfoString(buf, "\\b");
|
|
break;
|
|
case '\f':
|
|
appendStringInfoString(buf, "\\f");
|
|
break;
|
|
case '\n':
|
|
appendStringInfoString(buf, "\\n");
|
|
break;
|
|
case '\r':
|
|
appendStringInfoString(buf, "\\r");
|
|
break;
|
|
case '\t':
|
|
appendStringInfoString(buf, "\\t");
|
|
break;
|
|
case '"':
|
|
appendStringInfoString(buf, "\\\"");
|
|
break;
|
|
case '\\':
|
|
appendStringInfoString(buf, "\\\\");
|
|
break;
|
|
default:
|
|
if ((unsigned char) *p < ' ')
|
|
appendStringInfo(buf, "\\u%04x", (int) *p);
|
|
else
|
|
appendStringInfoCharMacro(buf, *p);
|
|
break;
|
|
}
|
|
}
|
|
appendStringInfoCharMacro(buf, '"');
|
|
}
|
|
|
|
/* Semantic actions for key uniqueness check */
|
|
static JsonParseErrorType
|
|
json_unique_object_start(void *_state)
|
|
{
|
|
JsonUniqueParsingState *state = _state;
|
|
JsonUniqueStackEntry *entry;
|
|
|
|
if (!state->unique)
|
|
return JSON_SUCCESS;
|
|
|
|
/* push object entry to stack */
|
|
entry = palloc(sizeof(*entry));
|
|
entry->object_id = state->id_counter++;
|
|
entry->parent = state->stack;
|
|
state->stack = entry;
|
|
|
|
return JSON_SUCCESS;
|
|
}
|
|
|
|
static JsonParseErrorType
|
|
json_unique_object_end(void *_state)
|
|
{
|
|
JsonUniqueParsingState *state = _state;
|
|
JsonUniqueStackEntry *entry;
|
|
|
|
if (!state->unique)
|
|
return JSON_SUCCESS;
|
|
|
|
entry = state->stack;
|
|
state->stack = entry->parent; /* pop object from stack */
|
|
pfree(entry);
|
|
return JSON_SUCCESS;
|
|
}
|
|
|
|
static JsonParseErrorType
|
|
json_unique_object_field_start(void *_state, char *field, bool isnull)
|
|
{
|
|
JsonUniqueParsingState *state = _state;
|
|
JsonUniqueStackEntry *entry;
|
|
|
|
if (!state->unique)
|
|
return JSON_SUCCESS;
|
|
|
|
/* find key collision in the current object */
|
|
if (json_unique_check_key(&state->check, field, state->stack->object_id))
|
|
return JSON_SUCCESS;
|
|
|
|
state->unique = false;
|
|
|
|
/* pop all objects entries */
|
|
while ((entry = state->stack))
|
|
{
|
|
state->stack = entry->parent;
|
|
pfree(entry);
|
|
}
|
|
return JSON_SUCCESS;
|
|
}
|
|
|
|
/* Validate JSON text and additionally check key uniqueness */
|
|
bool
|
|
json_validate(text *json, bool check_unique_keys, bool throw_error)
|
|
{
|
|
JsonLexContext *lex = makeJsonLexContext(json, check_unique_keys);
|
|
JsonSemAction uniqueSemAction = {0};
|
|
JsonUniqueParsingState state;
|
|
JsonParseErrorType result;
|
|
|
|
if (check_unique_keys)
|
|
{
|
|
state.lex = lex;
|
|
state.stack = NULL;
|
|
state.id_counter = 0;
|
|
state.unique = true;
|
|
json_unique_check_init(&state.check);
|
|
|
|
uniqueSemAction.semstate = &state;
|
|
uniqueSemAction.object_start = json_unique_object_start;
|
|
uniqueSemAction.object_field_start = json_unique_object_field_start;
|
|
uniqueSemAction.object_end = json_unique_object_end;
|
|
}
|
|
|
|
result = pg_parse_json(lex, check_unique_keys ? &uniqueSemAction : &nullSemAction);
|
|
|
|
if (result != JSON_SUCCESS)
|
|
{
|
|
if (throw_error)
|
|
json_errsave_error(result, lex, NULL);
|
|
|
|
return false; /* invalid json */
|
|
}
|
|
|
|
if (check_unique_keys && !state.unique)
|
|
{
|
|
if (throw_error)
|
|
ereport(ERROR,
|
|
(errcode(ERRCODE_DUPLICATE_JSON_OBJECT_KEY_VALUE),
|
|
errmsg("duplicate JSON object key value")));
|
|
|
|
return false; /* not unique keys */
|
|
}
|
|
|
|
return true; /* ok */
|
|
}
|
|
|
|
/*
|
|
* SQL function json_typeof(json) -> text
|
|
*
|
|
* Returns the type of the outermost JSON value as TEXT. Possible types are
|
|
* "object", "array", "string", "number", "boolean", and "null".
|
|
*
|
|
* Performs a single call to json_lex() to get the first token of the supplied
|
|
* value. This initial token uniquely determines the value's type. As our
|
|
* input must already have been validated by json_in() or json_recv(), the
|
|
* initial token should never be JSON_TOKEN_OBJECT_END, JSON_TOKEN_ARRAY_END,
|
|
* JSON_TOKEN_COLON, JSON_TOKEN_COMMA, or JSON_TOKEN_END.
|
|
*/
|
|
Datum
|
|
json_typeof(PG_FUNCTION_ARGS)
|
|
{
|
|
text *json = PG_GETARG_TEXT_PP(0);
|
|
JsonLexContext *lex = makeJsonLexContext(json, false);
|
|
char *type;
|
|
JsonTokenType tok;
|
|
JsonParseErrorType result;
|
|
|
|
/* Lex exactly one token from the input and check its type. */
|
|
result = json_lex(lex);
|
|
if (result != JSON_SUCCESS)
|
|
json_errsave_error(result, lex, NULL);
|
|
tok = lex->token_type;
|
|
|
|
switch (tok)
|
|
{
|
|
case JSON_TOKEN_OBJECT_START:
|
|
type = "object";
|
|
break;
|
|
case JSON_TOKEN_ARRAY_START:
|
|
type = "array";
|
|
break;
|
|
case JSON_TOKEN_STRING:
|
|
type = "string";
|
|
break;
|
|
case JSON_TOKEN_NUMBER:
|
|
type = "number";
|
|
break;
|
|
case JSON_TOKEN_TRUE:
|
|
case JSON_TOKEN_FALSE:
|
|
type = "boolean";
|
|
break;
|
|
case JSON_TOKEN_NULL:
|
|
type = "null";
|
|
break;
|
|
default:
|
|
elog(ERROR, "unexpected json token: %d", tok);
|
|
}
|
|
|
|
PG_RETURN_TEXT_P(cstring_to_text(type));
|
|
}
|