diff options
author | Andrew Dunstan | 2013-03-10 21:35:36 +0000 |
---|---|---|
committer | Andrew Dunstan | 2013-03-10 21:35:36 +0000 |
commit | 38fb4d978c5bfc377ef979e2595e3472744a3b05 (patch) | |
tree | af34c36f949f83c6527921cdbac1892c22719ac5 /src | |
parent | dd28c410f77e415ce104cb42bf52c4a5457085da (diff) |
JSON generation improvements.
This adds the following:
json_agg(anyrecord) -> json
to_json(any) -> json
hstore_to_json(hstore) -> json (also used as a cast)
hstore_to_json_loose(hstore) -> json
The last provides heuristic treatment of numbers and booleans.
Also, in json generation, if any non-builtin type has a cast to json,
that function is used instead of the type's output function.
Andrew Dunstan, reviewed by Steve Singer.
Catalog version bumped.
Diffstat (limited to 'src')
-rw-r--r-- | src/backend/utils/adt/json.c | 317 | ||||
-rw-r--r-- | src/include/catalog/catversion.h | 2 | ||||
-rw-r--r-- | src/include/catalog/pg_aggregate.h | 3 | ||||
-rw-r--r-- | src/include/catalog/pg_proc.h | 8 | ||||
-rw-r--r-- | src/include/utils/json.h | 5 | ||||
-rw-r--r-- | src/test/regress/expected/json.out | 24 | ||||
-rw-r--r-- | src/test/regress/sql/json.sql | 12 |
7 files changed, 350 insertions, 21 deletions
diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index f330fcad6ee..82be9a1b669 100644 --- a/src/backend/utils/adt/json.c +++ b/src/backend/utils/adt/json.c @@ -14,6 +14,8 @@ #include "postgres.h" #include "access/htup_details.h" +#include "access/transam.h" +#include "catalog/pg_cast.h" #include "catalog/pg_type.h" #include "executor/spi.h" #include "lib/stringinfo.h" @@ -24,6 +26,7 @@ #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/json.h" +#include "utils/syscache.h" #include "utils/typcache.h" typedef enum /* types of JSON values */ @@ -42,7 +45,7 @@ typedef struct /* state of JSON lexer */ { char *input; /* whole string being parsed */ char *token_start; /* start of current token within input */ - char *token_terminator; /* end of previous or current token */ + char *token_terminator; /* end of previous or current token */ JsonValueType token_type; /* type of current token, once it's known */ } JsonLexContext; @@ -67,7 +70,7 @@ typedef enum /* required operations on state stack */ { JSON_STACKOP_NONE, /* no-op */ JSON_STACKOP_PUSH, /* push new JSON_PARSE_VALUE stack item */ - JSON_STACKOP_PUSH_WITH_PUSHBACK, /* push, then rescan current token */ + JSON_STACKOP_PUSH_WITH_PUSHBACK, /* push, then rescan current token */ JSON_STACKOP_POP /* pop, or expect end of input if no stack */ } JsonStackOp; @@ -77,19 +80,25 @@ static void json_lex_string(JsonLexContext *lex); static void json_lex_number(JsonLexContext *lex, char *s); static void report_parse_error(JsonParseStack *stack, JsonLexContext *lex); static void report_invalid_token(JsonLexContext *lex); -static int report_json_context(JsonLexContext *lex); +static int report_json_context(JsonLexContext *lex); static char *extract_mb_char(char *s); static void composite_to_json(Datum composite, StringInfo result, - bool use_line_feeds); + bool use_line_feeds); static void array_dim_to_json(StringInfo result, int dim, int ndims, int *dims, Datum *vals, bool *nulls, int *valcount, TYPCATEGORY tcategory, Oid typoutputfunc, bool use_line_feeds); static void array_to_json_internal(Datum array, StringInfo result, - bool use_line_feeds); + bool use_line_feeds); +/* + * All the defined type categories are upper case , so use lower case here + * so we avoid any possible clash. + */ /* fake type category for JSON so we can distinguish it in datum_to_json */ #define TYPCATEGORY_JSON 'j' +/* fake category for types that have a cast to json */ +#define TYPCATEGORY_JSON_CAST 'c' /* letters appearing in numeric output that aren't valid in a JSON number */ #define NON_NUMERIC_LETTER "NnAaIiFfTtYy" /* chars to consider as part of an alphanumeric token */ @@ -384,15 +393,15 @@ json_lex(JsonLexContext *lex) * unintuitive prefix thereof. */ for (p = s; JSON_ALPHANUMERIC_CHAR(*p); p++) - /* skip */ ; + /* skip */ ; if (p == s) { /* * We got some sort of unexpected punctuation or an otherwise * unexpected character, so just complain about that one - * character. (It can't be multibyte because the above loop - * will advance over any multibyte characters.) + * character. (It can't be multibyte because the above loop will + * advance over any multibyte characters.) */ lex->token_terminator = s + 1; report_invalid_token(lex); @@ -585,7 +594,7 @@ json_lex_number(JsonLexContext *lex, char *s) } /* - * Check for trailing garbage. As in json_lex(), any alphanumeric stuff + * Check for trailing garbage. As in json_lex(), any alphanumeric stuff * here should be considered part of the token for error-reporting * purposes. */ @@ -653,16 +662,16 @@ report_parse_error(JsonParseStack *stack, JsonLexContext *lex) ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("invalid input syntax for type json"), - errdetail("Expected \",\" or \"]\", but found \"%s\".", - token), + errdetail("Expected \",\" or \"]\", but found \"%s\".", + token), report_json_context(lex))); break; case JSON_PARSE_OBJECT_START: ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("invalid input syntax for type json"), - errdetail("Expected string or \"}\", but found \"%s\".", - token), + errdetail("Expected string or \"}\", but found \"%s\".", + token), report_json_context(lex))); break; case JSON_PARSE_OBJECT_LABEL: @@ -677,8 +686,8 @@ report_parse_error(JsonParseStack *stack, JsonLexContext *lex) ereport(ERROR, (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION), errmsg("invalid input syntax for type json"), - errdetail("Expected \",\" or \"}\", but found \"%s\".", - token), + errdetail("Expected \",\" or \"}\", but found \"%s\".", + token), report_json_context(lex))); break; case JSON_PARSE_OBJECT_COMMA: @@ -820,6 +829,7 @@ datum_to_json(Datum val, bool is_null, StringInfo result, TYPCATEGORY tcategory, Oid typoutputfunc) { char *outputstr; + text *jsontext; if (is_null) { @@ -862,6 +872,13 @@ datum_to_json(Datum val, bool is_null, StringInfo result, appendStringInfoString(result, outputstr); pfree(outputstr); break; + case TYPCATEGORY_JSON_CAST: + jsontext = DatumGetTextP(OidFunctionCall1(typoutputfunc, val)); + outputstr = text_to_cstring(jsontext); + appendStringInfoString(result, outputstr); + pfree(outputstr); + pfree(jsontext); + break; default: outputstr = OidOutputFunctionCall(typoutputfunc, val); escape_json(result, outputstr); @@ -935,6 +952,7 @@ array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds) Oid typioparam; Oid typoutputfunc; TYPCATEGORY tcategory; + Oid castfunc = InvalidOid; ndim = ARR_NDIM(v); dim = ARR_DIMS(v); @@ -950,11 +968,32 @@ array_to_json_internal(Datum array, StringInfo result, bool use_line_feeds) &typlen, &typbyval, &typalign, &typdelim, &typioparam, &typoutputfunc); + if (element_type > FirstNormalObjectId) + { + HeapTuple tuple; + Form_pg_cast castForm; + + tuple = SearchSysCache2(CASTSOURCETARGET, + ObjectIdGetDatum(element_type), + ObjectIdGetDatum(JSONOID)); + if (HeapTupleIsValid(tuple)) + { + castForm = (Form_pg_cast) GETSTRUCT(tuple); + + if (castForm->castmethod == COERCION_METHOD_FUNCTION) + castfunc = typoutputfunc = castForm->castfunc; + + ReleaseSysCache(tuple); + } + } + deconstruct_array(v, element_type, typlen, typbyval, typalign, &elements, &nulls, &nitems); - if (element_type == RECORDOID) + if (castfunc != InvalidOid) + tcategory = TYPCATEGORY_JSON_CAST; + else if (element_type == RECORDOID) tcategory = TYPCATEGORY_COMPOSITE; else if (element_type == JSONOID) tcategory = TYPCATEGORY_JSON; @@ -1009,6 +1048,7 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds) TYPCATEGORY tcategory; Oid typoutput; bool typisvarlena; + Oid castfunc = InvalidOid; if (tupdesc->attrs[i]->attisdropped) continue; @@ -1023,7 +1063,31 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds) origval = heap_getattr(tuple, i + 1, tupdesc, &isnull); - if (tupdesc->attrs[i]->atttypid == RECORDARRAYOID) + getTypeOutputInfo(tupdesc->attrs[i]->atttypid, + &typoutput, &typisvarlena); + + if (tupdesc->attrs[i]->atttypid > FirstNormalObjectId) + { + HeapTuple cast_tuple; + Form_pg_cast castForm; + + cast_tuple = SearchSysCache2(CASTSOURCETARGET, + ObjectIdGetDatum(tupdesc->attrs[i]->atttypid), + ObjectIdGetDatum(JSONOID)); + if (HeapTupleIsValid(cast_tuple)) + { + castForm = (Form_pg_cast) GETSTRUCT(cast_tuple); + + if (castForm->castmethod == COERCION_METHOD_FUNCTION) + castfunc = typoutput = castForm->castfunc; + + ReleaseSysCache(cast_tuple); + } + } + + if (castfunc != InvalidOid) + tcategory = TYPCATEGORY_JSON_CAST; + else if (tupdesc->attrs[i]->atttypid == RECORDARRAYOID) tcategory = TYPCATEGORY_ARRAY; else if (tupdesc->attrs[i]->atttypid == RECORDOID) tcategory = TYPCATEGORY_COMPOSITE; @@ -1032,9 +1096,6 @@ composite_to_json(Datum composite, StringInfo result, bool use_line_feeds) else tcategory = TypeCategory(tupdesc->attrs[i]->atttypid); - getTypeOutputInfo(tupdesc->attrs[i]->atttypid, - &typoutput, &typisvarlena); - /* * If we have a toasted datum, forcibly detoast it here to avoid * memory leakage inside the type's output routine. @@ -1122,6 +1183,222 @@ row_to_json_pretty(PG_FUNCTION_ARGS) } /* + * SQL function to_json(anyvalue) + */ +Datum +to_json(PG_FUNCTION_ARGS) +{ + Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 0); + StringInfo result; + Datum orig_val, + val; + 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"))); + + + result = makeStringInfo(); + + orig_val = PG_ARGISNULL(0) ? (Datum) 0 : PG_GETARG_DATUM(0); + + 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 we have a toasted datum, forcibly detoast it here to avoid memory + * leakage inside the type's output routine. + */ + if (typisvarlena && orig_val != (Datum) 0) + val = PointerGetDatum(PG_DETOAST_DATUM(orig_val)); + else + val = orig_val; + + datum_to_json(val, false, result, tcategory, typoutput); + + /* Clean up detoasted copy, if any */ + if (val != orig_val) + pfree(DatumGetPointer(val)); + + PG_RETURN_TEXT_P(cstring_to_text(result->data)); +} + +/* + * json_agg transition function + */ +Datum +json_agg_transfn(PG_FUNCTION_ARGS) +{ + Oid val_type = get_fn_expr_argtype(fcinfo->flinfo, 1); + MemoryContext aggcontext, + oldcontext; + StringInfo state; + Datum orig_val, + val; + 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"))); + + 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); + + appendStringInfoChar(state, '['); + } + else + { + state = (StringInfo) PG_GETARG_POINTER(0); + appendStringInfoString(state, ", "); + } + + /* fast path for NULLs */ + if (PG_ARGISNULL(1)) + { + orig_val = (Datum) 0; + datum_to_json(orig_val, true, state, 0, InvalidOid); + PG_RETURN_POINTER(state); + } + + + orig_val = PG_GETARG_DATUM(1); + + 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 we have a toasted datum, forcibly detoast it here to avoid memory + * leakage inside the type's output routine. + */ + if (typisvarlena) + val = PointerGetDatum(PG_DETOAST_DATUM(orig_val)); + else + val = orig_val; + + if (!PG_ARGISNULL(0) && + (tcategory == TYPCATEGORY_ARRAY || tcategory == TYPCATEGORY_COMPOSITE)) + { + appendStringInfoString(state, "\n "); + } + + datum_to_json(val, false, state, tcategory, typoutput); + + /* Clean up detoasted copy, if any */ + if (val != orig_val) + pfree(DatumGetPointer(val)); + + /* + * The transition type for array_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 ArrayBuildState pointer through nodeAgg.c's machinations. + */ + PG_RETURN_POINTER(state); +} + +/* + * json_agg final function + */ +Datum +json_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_NULL(); + + appendStringInfoChar(state, ']'); + + PG_RETURN_TEXT_P(cstring_to_text(state->data)); +} + +/* * Produce a JSON string literal, properly escaping characters in the text. */ void diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 4c36cbee9e9..9c63fa18b5b 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 201303081 +#define CATALOG_VERSION_NO 201303101 #endif diff --git a/src/include/catalog/pg_aggregate.h b/src/include/catalog/pg_aggregate.h index f19f734ce13..6fb10a94567 100644 --- a/src/include/catalog/pg_aggregate.h +++ b/src/include/catalog/pg_aggregate.h @@ -232,6 +232,9 @@ DATA(insert ( 3538 string_agg_transfn string_agg_finalfn 0 2281 _null_ )); /* bytea */ DATA(insert ( 3545 bytea_string_agg_transfn bytea_string_agg_finalfn 0 2281 _null_ )); +/* json */ +DATA(insert ( 3175 json_agg_transfn json_agg_finalfn 0 2281 _null_ )); + /* * prototypes for functions in pg_aggregate.c */ diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index 0e26ebf219f..c97056e1673 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -4106,6 +4106,14 @@ DATA(insert OID = 3155 ( row_to_json PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 DESCR("map row to json"); DATA(insert OID = 3156 ( row_to_json PGNSP PGUID 12 1 0 0 0 f f f f t f s 2 0 114 "2249 16" _null_ _null_ _null_ _null_ row_to_json_pretty _null_ _null_ _null_ )); DESCR("map row to json with optional pretty printing"); +DATA(insert OID = 3173 ( json_agg_transfn PGNSP PGUID 12 1 0 0 0 f f f f f f i 2 0 2281 "2281 2283" _null_ _null_ _null_ _null_ json_agg_transfn _null_ _null_ _null_ )); +DESCR("json aggregate transition function"); +DATA(insert OID = 3174 ( json_agg_finalfn PGNSP PGUID 12 1 0 0 0 f f f f f f i 1 0 114 "2281" _null_ _null_ _null_ _null_ json_agg_finalfn _null_ _null_ _null_ )); +DESCR("json aggregate final function"); +DATA(insert OID = 3175 ( json_agg PGNSP PGUID 12 1 0 0 0 t f f f f f i 1 0 114 "2283" _null_ _null_ _null_ _null_ aggregate_dummy _null_ _null_ _null_ )); +DESCR("aggregate input into json"); +DATA(insert OID = 3176 ( to_json PGNSP PGUID 12 1 0 0 0 f f f f t f s 1 0 114 "2283" _null_ _null_ _null_ _null_ to_json _null_ _null_ _null_ )); +DESCR("map input to json"); /* uuid */ DATA(insert OID = 2952 ( uuid_in PGNSP PGUID 12 1 0 0 0 f f f f t f i 1 0 2950 "2275" _null_ _null_ _null_ _null_ uuid_in _null_ _null_ _null_ )); diff --git a/src/include/utils/json.h b/src/include/utils/json.h index 34f37d7b8b1..caaa769449a 100644 --- a/src/include/utils/json.h +++ b/src/include/utils/json.h @@ -25,6 +25,11 @@ extern Datum array_to_json(PG_FUNCTION_ARGS); extern Datum array_to_json_pretty(PG_FUNCTION_ARGS); extern Datum row_to_json(PG_FUNCTION_ARGS); extern Datum row_to_json_pretty(PG_FUNCTION_ARGS); +extern Datum to_json(PG_FUNCTION_ARGS); + +extern Datum json_agg_transfn(PG_FUNCTION_ARGS); +extern Datum json_agg_finalfn(PG_FUNCTION_ARGS); + extern void escape_json(StringInfo buf, const char *str); #endif /* JSON_H */ diff --git a/src/test/regress/expected/json.out b/src/test/regress/expected/json.out index 2dfe7bb0eec..7ae18f1cb8c 100644 --- a/src/test/regress/expected/json.out +++ b/src/test/regress/expected/json.out @@ -403,6 +403,30 @@ SELECT row_to_json(row((select array_agg(x) as d from generate_series(5,10) x)), {"f1":[5,6,7,8,9,10]} (1 row) +--json_agg +SELECT json_agg(q) + FROM ( SELECT $$a$$ || x AS b, y AS c, + ARRAY[ROW(x.*,ARRAY[1,2,3]), + ROW(y.*,ARRAY[4,5,6])] AS z + FROM generate_series(1,2) x, + generate_series(4,5) y) q; + json_agg +----------------------------------------------------------------------- + [{"b":"a1","c":4,"z":[{"f1":1,"f2":[1,2,3]},{"f1":4,"f2":[4,5,6]}]}, + + {"b":"a1","c":5,"z":[{"f1":1,"f2":[1,2,3]},{"f1":5,"f2":[4,5,6]}]}, + + {"b":"a2","c":4,"z":[{"f1":2,"f2":[1,2,3]},{"f1":4,"f2":[4,5,6]}]}, + + {"b":"a2","c":5,"z":[{"f1":2,"f2":[1,2,3]},{"f1":5,"f2":[4,5,6]}]}] +(1 row) + +SELECT json_agg(q) + FROM rows q; + json_agg +----------------------- + [{"x":1,"y":"txt1"}, + + {"x":2,"y":"txt2"}, + + {"x":3,"y":"txt3"}] +(1 row) + -- non-numeric output SELECT row_to_json(q) FROM (SELECT 'NaN'::float8 AS "float8field") q; diff --git a/src/test/regress/sql/json.sql b/src/test/regress/sql/json.sql index 52be0cf7eb7..5583d653075 100644 --- a/src/test/regress/sql/json.sql +++ b/src/test/regress/sql/json.sql @@ -100,6 +100,18 @@ FROM rows q; SELECT row_to_json(row((select array_agg(x) as d from generate_series(5,10) x)),false); +--json_agg + +SELECT json_agg(q) + FROM ( SELECT $$a$$ || x AS b, y AS c, + ARRAY[ROW(x.*,ARRAY[1,2,3]), + ROW(y.*,ARRAY[4,5,6])] AS z + FROM generate_series(1,2) x, + generate_series(4,5) y) q; + +SELECT json_agg(q) + FROM rows q; + -- non-numeric output SELECT row_to_json(q) FROM (SELECT 'NaN'::float8 AS "float8field") q; |