diff --git a/contrib/hstore/expected/hstore.out b/contrib/hstore/expected/hstore.out index 813b9c04784ad97b9b0bc2d4ad111bb8686e3988..21141430229a4edcf410dc8f514d7521902ed0a0 100644 --- a/contrib/hstore/expected/hstore.out +++ b/contrib/hstore/expected/hstore.out @@ -1453,3 +1453,39 @@ select count(*) from testhstore where h = 'pos=>98, line=>371, node=>CBA, indexe 1 (1 row) +-- json +select hstore_to_json('"a key" =>1, b => t, c => null, d=> 12345, e => 012345, f=> 1.234, g=> 2.345e+4'); + hstore_to_json +------------------------------------------------------------------------------------------------- + {"b": "t", "c": null, "d": "12345", "e": "012345", "f": "1.234", "g": "2.345e+4", "a key": "1"} +(1 row) + +select cast( hstore '"a key" =>1, b => t, c => null, d=> 12345, e => 012345, f=> 1.234, g=> 2.345e+4' as json); + json +------------------------------------------------------------------------------------------------- + {"b": "t", "c": null, "d": "12345", "e": "012345", "f": "1.234", "g": "2.345e+4", "a key": "1"} +(1 row) + +select hstore_to_json_loose('"a key" =>1, b => t, c => null, d=> 12345, e => 012345, f=> 1.234, g=> 2.345e+4'); + hstore_to_json_loose +------------------------------------------------------------------------------------------ + {"b": true, "c": null, "d": 12345, "e": "012345", "f": 1.234, "g": 2.345e+4, "a key": 1} +(1 row) + +create table test_json_agg (f1 text, f2 hstore); +insert into test_json_agg values ('rec1','"a key" =>1, b => t, c => null, d=> 12345, e => 012345, f=> 1.234, g=> 2.345e+4'), + ('rec2','"a key" =>2, b => f, c => "null", d=> -12345, e => 012345.6, f=> -1.234, g=> 0.345e-4'); +select json_agg(q) from test_json_agg q; + json_agg +---------------------------------------------------------------------------------------------------------------------------- + [{"f1":"rec1","f2":{"b": "t", "c": null, "d": "12345", "e": "012345", "f": "1.234", "g": "2.345e+4", "a key": "1"}}, + + {"f1":"rec2","f2":{"b": "f", "c": "null", "d": "-12345", "e": "012345.6", "f": "-1.234", "g": "0.345e-4", "a key": "2"}}] +(1 row) + +select json_agg(q) from (select f1, hstore_to_json_loose(f2) as f2 from test_json_agg) q; + json_agg +---------------------------------------------------------------------------------------------------------------------- + [{"f1":"rec1","f2":{"b": true, "c": null, "d": 12345, "e": "012345", "f": 1.234, "g": 2.345e+4, "a key": 1}}, + + {"f1":"rec2","f2":{"b": false, "c": "null", "d": -12345, "e": "012345.6", "f": -1.234, "g": 0.345e-4, "a key": 2}}] +(1 row) + diff --git a/contrib/hstore/hstore--1.1.sql b/contrib/hstore/hstore--1.1.sql index e95ad328aacb2829fe7f5e6379480aa021a72368..f415a7230b440d1dd70124a971dcf364cc0e926f 100644 --- a/contrib/hstore/hstore--1.1.sql +++ b/contrib/hstore/hstore--1.1.sql @@ -234,6 +234,19 @@ LANGUAGE C IMMUTABLE STRICT; CREATE CAST (text[] AS hstore) WITH FUNCTION hstore(text[]); +CREATE FUNCTION hstore_to_json(hstore) +RETURNS json +AS 'MODULE_PATHNAME', 'hstore_to_json' +LANGUAGE C IMMUTABLE STRICT; + +CREATE CAST (hstore AS json) + WITH FUNCTION hstore_to_json(hstore); + +CREATE FUNCTION hstore_to_json_loose(hstore) +RETURNS json +AS 'MODULE_PATHNAME', 'hstore_to_json_loose' +LANGUAGE C IMMUTABLE STRICT; + CREATE FUNCTION hstore(record) RETURNS hstore AS 'MODULE_PATHNAME', 'hstore_from_record' diff --git a/contrib/hstore/hstore_io.c b/contrib/hstore/hstore_io.c index e84cdd69051c9205683e16b90234f379998a23c0..07b60e621b2fe0d4364d477909076b5777215567 100644 --- a/contrib/hstore/hstore_io.c +++ b/contrib/hstore/hstore_io.c @@ -8,7 +8,10 @@ #include "access/htup_details.h" #include "catalog/pg_type.h" #include "funcapi.h" +#include "lib/stringinfo.h" #include "libpq/pqformat.h" +#include "utils/builtins.h" +#include "utils/json.h" #include "utils/lsyscache.h" #include "utils/typcache.h" @@ -1209,3 +1212,217 @@ hstore_send(PG_FUNCTION_ARGS) PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); } + + +/* + * hstore_to_json_loose + * + * This is a heuristic conversion to json which treats + * 't' and 'f' as booleans and strings that look like numbers as numbers, + * as long as they don't start with a leading zero followed by another digit + * (think zip codes or phone numbers starting with 0). + */ +PG_FUNCTION_INFO_V1(hstore_to_json_loose); +Datum hstore_to_json_loose(PG_FUNCTION_ARGS); +Datum +hstore_to_json_loose(PG_FUNCTION_ARGS) +{ + HStore *in = PG_GETARG_HS(0); + int buflen, + i; + int count = HS_COUNT(in); + char *out, + *ptr; + char *base = STRPTR(in); + HEntry *entries = ARRPTR(in); + bool is_number; + StringInfo src, + dst; + + if (count == 0) + { + out = palloc(1); + *out = '\0'; + PG_RETURN_TEXT_P(cstring_to_text(out)); + } + + buflen = 3; + + /* + * Formula adjusted slightly from the logic in hstore_out. We have to take + * account of out treatment of booleans to be a bit more pessimistic about + * the length of values. + */ + + for (i = 0; i < count; i++) + { + /* include "" and colon-space and comma-space */ + buflen += 6 + 2 * HS_KEYLEN(entries, i); + /* include "" only if nonnull */ + buflen += 3 + (HS_VALISNULL(entries, i) + ? 1 + : 2 * HS_VALLEN(entries, i)); + } + + out = ptr = palloc(buflen); + + src = makeStringInfo(); + dst = makeStringInfo(); + + *ptr++ = '{'; + + for (i = 0; i < count; i++) + { + resetStringInfo(src); + resetStringInfo(dst); + appendBinaryStringInfo(src, HS_KEY(entries, base, i), HS_KEYLEN(entries, i)); + escape_json(dst, src->data); + strncpy(ptr, dst->data, dst->len); + ptr += dst->len; + *ptr++ = ':'; + *ptr++ = ' '; + resetStringInfo(dst); + if (HS_VALISNULL(entries, i)) + appendStringInfoString(dst, "null"); + /* guess that values of 't' or 'f' are booleans */ + else if (HS_VALLEN(entries, i) == 1 && *(HS_VAL(entries, base, i)) == 't') + appendStringInfoString(dst, "true"); + else if (HS_VALLEN(entries, i) == 1 && *(HS_VAL(entries, base, i)) == 'f') + appendStringInfoString(dst, "false"); + else + { + is_number = false; + resetStringInfo(src); + appendBinaryStringInfo(src, HS_VAL(entries, base, i), HS_VALLEN(entries, i)); + + /* + * don't treat something with a leading zero followed by another + * digit as numeric - could be a zip code or similar + */ + if (src->len > 0 && (src->data[0] != '0' || !isdigit(src->data[1])) && + strspn(src->data, "+-0123456789Ee.") == src->len) + { + /* + * might be a number. See if we can input it as a numeric + * value + */ + char *endptr = "junk"; + + (void) (strtol(src->data, &endptr, 10) + 1); + if (*endptr == '\0') + { + /* + * strol man page says this means the whole string is + * valid + */ + is_number = true; + } + else + { + /* not an int - try a double */ + (void) (strtod(src->data, &endptr) + 1.0); + if (*endptr == '\0') + is_number = true; + } + } + if (is_number) + appendBinaryStringInfo(dst, src->data, src->len); + else + escape_json(dst, src->data); + } + strncpy(ptr, dst->data, dst->len); + ptr += dst->len; + + if (i + 1 != count) + { + *ptr++ = ','; + *ptr++ = ' '; + } + } + *ptr++ = '}'; + *ptr = '\0'; + + PG_RETURN_TEXT_P(cstring_to_text(out)); +} + +PG_FUNCTION_INFO_V1(hstore_to_json); +Datum hstore_to_json(PG_FUNCTION_ARGS); +Datum +hstore_to_json(PG_FUNCTION_ARGS) +{ + HStore *in = PG_GETARG_HS(0); + int buflen, + i; + int count = HS_COUNT(in); + char *out, + *ptr; + char *base = STRPTR(in); + HEntry *entries = ARRPTR(in); + StringInfo src, + dst; + + if (count == 0) + { + out = palloc(1); + *out = '\0'; + PG_RETURN_TEXT_P(cstring_to_text(out)); + } + + buflen = 3; + + /* + * Formula adjusted slightly from the logic in hstore_out. We have to take + * account of out treatment of booleans to be a bit more pessimistic about + * the length of values. + */ + + for (i = 0; i < count; i++) + { + /* include "" and colon-space and comma-space */ + buflen += 6 + 2 * HS_KEYLEN(entries, i); + /* include "" only if nonnull */ + buflen += 3 + (HS_VALISNULL(entries, i) + ? 1 + : 2 * HS_VALLEN(entries, i)); + } + + out = ptr = palloc(buflen); + + src = makeStringInfo(); + dst = makeStringInfo(); + + *ptr++ = '{'; + + for (i = 0; i < count; i++) + { + resetStringInfo(src); + resetStringInfo(dst); + appendBinaryStringInfo(src, HS_KEY(entries, base, i), HS_KEYLEN(entries, i)); + escape_json(dst, src->data); + strncpy(ptr, dst->data, dst->len); + ptr += dst->len; + *ptr++ = ':'; + *ptr++ = ' '; + resetStringInfo(dst); + if (HS_VALISNULL(entries, i)) + appendStringInfoString(dst, "null"); + else + { + resetStringInfo(src); + appendBinaryStringInfo(src, HS_VAL(entries, base, i), HS_VALLEN(entries, i)); + escape_json(dst, src->data); + } + strncpy(ptr, dst->data, dst->len); + ptr += dst->len; + + if (i + 1 != count) + { + *ptr++ = ','; + *ptr++ = ' '; + } + } + *ptr++ = '}'; + *ptr = '\0'; + + PG_RETURN_TEXT_P(cstring_to_text(out)); +} diff --git a/contrib/hstore/sql/hstore.sql b/contrib/hstore/sql/hstore.sql index d046a7f3840630d419c589c564ca253dde2164e3..68b74bfcff5fd0cac5ea33e9ef98bf30be9fccd8 100644 --- a/contrib/hstore/sql/hstore.sql +++ b/contrib/hstore/sql/hstore.sql @@ -330,3 +330,15 @@ set enable_seqscan=off; select count(*) from testhstore where h #># 'p=>1'; select count(*) from testhstore where h = 'pos=>98, line=>371, node=>CBA, indexed=>t'; + +-- json +select hstore_to_json('"a key" =>1, b => t, c => null, d=> 12345, e => 012345, f=> 1.234, g=> 2.345e+4'); +select cast( hstore '"a key" =>1, b => t, c => null, d=> 12345, e => 012345, f=> 1.234, g=> 2.345e+4' as json); +select hstore_to_json_loose('"a key" =>1, b => t, c => null, d=> 12345, e => 012345, f=> 1.234, g=> 2.345e+4'); + +create table test_json_agg (f1 text, f2 hstore); +insert into test_json_agg values ('rec1','"a key" =>1, b => t, c => null, d=> 12345, e => 012345, f=> 1.234, g=> 2.345e+4'), + ('rec2','"a key" =>2, b => f, c => "null", d=> -12345, e => 012345.6, f=> -1.234, g=> 0.345e-4'); +select json_agg(q) from test_json_agg q; +select json_agg(q) from (select f1, hstore_to_json_loose(f2) as f2 from test_json_agg) q; + diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 9b7e9677581c1c379ac7dd67c57789b60e4654f6..372e2b65751c45c581838f651d2ddac6c382d387 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -9685,10 +9685,41 @@ table2-mapping <entry><literal>row_to_json(row(1,'foo'))</literal></entry> <entry><literal>{"f1":1,"f2":"foo"}</literal></entry> </row> + <row> + <entry> + <indexterm> + <primary>to_json</primary> + </indexterm> + <literal>to_json(anyelement)</literal> + </entry> + <entry> + Returns the value as JSON. If the data type is not builtin, and there + is a cast from the type to json, the cast function will be used to + perform the conversion. Otherwise, for any value other than a number, + a boolean or NULL, the text representation will be used, escaped and + quoted so that it is legal JSON. + </entry> + <entry><literal>to_json('Fred said "Hi."'</literal></entry> + <entry><literal>"Fred said \"Hi.\""</literal></entry> + </row> </tbody> </tgroup> </table> + <note> + <para> + The <xref linkend="hstore"> extension has a cast from hstore to + json, so that converted hstore values are represented as json objects, + not as string values. + </para> + </note> + + <para> + See also <xref linkend="functions-aggregate"> about the aggregate + function <function>json_agg</function> which aggregates record + values as json efficiently. + </para> + </sect1> <sect1 id="functions-sequence"> @@ -11059,6 +11090,22 @@ SELECT NULLIF(value, '(none)') ... <entry>equivalent to <function>bool_and</function></entry> </row> + <row> + <entry> + <indexterm> + <primary>json_agg</primary> + </indexterm> + <function>json_agg(<replaceable class="parameter">record</replaceable>)</function> + </entry> + <entry> + <type>record</type> + </entry> + <entry> + <type>json</type> + </entry> + <entry>aggregates records as a json array of objects</entry> + </row> + <row> <entry> <indexterm> @@ -11204,6 +11251,7 @@ SELECT count(*) FROM sometable; <para> The aggregate functions <function>array_agg</function>, + <function>json_agg</function>, <function>string_agg</function>, and <function>xmlagg</function>, as well as similar user-defined aggregate functions, produce meaningfully different result values diff --git a/doc/src/sgml/hstore.sgml b/doc/src/sgml/hstore.sgml index a03f926f0b755ee920a425ef2f1ef6dd0433c9f6..85e7efcedc17e07a1b6208effc38833c0cba70ca 100644 --- a/doc/src/sgml/hstore.sgml +++ b/doc/src/sgml/hstore.sgml @@ -322,6 +322,22 @@ b <entry><literal>{{a,1},{b,2}}</literal></entry> </row> + <row> + <entry><function>hstore_to_json(hstore)</function></entry> + <entry><type>json</type></entry> + <entry>get <type>hstore</type> as a json value</entry> + <entry><literal>hstore_to_json('"a key"=>1, b=>t, c=>null, d=>12345, e=>012345, f=>1.234, g=>2.345e+4')</literal></entry> + <entry><literal>{"a key": "1", "b": "t", "c": null, "d": "12345", "e": "012345", "f": "1.234", "g": "2.345e+4"}</literal></entry> + </row> + + <row> + <entry><function>hstore_to_json_loose(hstore)</function></entry> + <entry><type>json</type></entry> + <entry>get <type>hstore</type> as a json value, but attempting to distinguish numerical and boolean values so they are unquoted in the json</entry> + <entry><literal>hstore_to_json('"a key"=>1, b=>t, c=>null, d=>12345, e=>012345, f=>1.234, g=>2.345e+4')</literal></entry> + <entry><literal>{"a key": 1, "b": true, "c": null, "d": 12345, "e": "012345", "f": 1.234, "g": 2.345e+4}</literal></entry> + </row> + <row> <entry><function>slice(hstore, text[])</function></entry> <entry><type>hstore</type></entry> @@ -396,6 +412,13 @@ b </tgroup> </table> + <note> + <para> + The function <function>hstore_to_json</function> is used when an <type>hstore</type> + value is cast to <type>json</type>. + </para> + </note> + <note> <para> The function <function>populate_record</function> is actually declared diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c index f330fcad6ee196325e9ec824c61ef478c8fb715d..82be9a1b6693bd918d31fd146008bf8502cb40aa 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. @@ -1121,6 +1182,222 @@ row_to_json_pretty(PG_FUNCTION_ARGS) PG_RETURN_TEXT_P(cstring_to_text(result->data)); } +/* + * 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. */ diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 4c36cbee9e9a2c3fdaef6db73a8c56b974726c80..9c63fa18b5bd70333de7c5ba8dcf5f048128c600 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 f19f734ce137118f1a9581e471496c219d94d8ed..6fb10a945672e455506773e2db1bc274552c070d 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 0e26ebf219ff6c85a04edbaea9442420763a4de4..c97056e167399207c3278ca6bf72d01290f888ab 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 34f37d7b8b1f6cd748968ddf21ccfba1b0c3b5c5..caaa769449a0e81a07978659ed52aba92392dd57 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 2dfe7bb0eec87c2793340fed1a1b4ce1a9604e98..7ae18f1cb8ccaa4799f32f67d0119fc1c4a775d8 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 52be0cf7eb76364724946e6fdba358a3c576568f..5583d65307588680aa81b3b093c46e225274ace3 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;