On 01/14/2012 03:06 PM, Andrew Dunstan wrote:
OK, here's a patch that does both query_to_json and array_to_json,
along with docs and regression tests. It include Robert's original
patch, although I can produce a differential patch if required. It can
also be pulled from <https://bitbucket.org/adunstan/pgdevel>
Here's an update that adds row_to_json, plus a bit more cleanup. Example:
andrew=# SELECT row_to_json(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;
row_to_json
--------------------------------------------------------------------
{"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]}]}
(4 rows)
(This might be more to Robert's taste than query_to_json() :-) )
cheers
andrew
diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml
index 152ef2f..f45b10b 100644
--- a/doc/src/sgml/datatype.sgml
+++ b/doc/src/sgml/datatype.sgml
@@ -269,6 +269,12 @@
<entry></entry>
<entry>XML data</entry>
</row>
+
+ <row>
+ <entry><type>json</type></entry>
+ <entry></entry>
+ <entry>JSON data</entry>
+ </row>
</tbody>
</tgroup>
</table>
@@ -4169,6 +4175,21 @@ SET xmloption TO { DOCUMENT | CONTENT };
</sect2>
</sect1>
+ <sect1 id="datatype-json">
+ <title><acronym>JSON</> Type</title>
+
+ <indexterm zone="datatype-json">
+ <primary>JSON</primary>
+ </indexterm>
+
+ <para>
+ The <type>json</type> data type can be used to store JSON data. Such
+ data can also be stored as <type>text</type>, but the
+ <type>json</type> data type has the advantage of checking that each
+ stored value is a valid JSON value.
+ </para>
+ </sect1>
+
&array;
&rowtypes;
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 2e06346..9368739 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -9615,6 +9615,77 @@ table2-mapping
</sect2>
</sect1>
+ <sect1 id="functions-json">
+ <title>JSON functions</title>
+
+ <indexterm zone="datatype-json">
+ <primary>JSON</primary>
+ <secondary>Functions and operators</secondary>
+ </indexterm>
+
+ <para>
+ This section descripbes the functions that are available for creating
+ JSON (see <xref linkend="datatype-json">) data.
+ </para>
+
+ <table id="functions-json-table">
+ <title>JSON Support Functions</title>
+ <tgroup cols="4">
+ <thead>
+ <row>
+ <entry>Function</entry>
+ <entry>Description</entry>
+ <entry>Example</entry>
+ <entry>Example Result</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry>
+ <indexterm>
+ <primary>query_to_json</primary>
+ </indexterm>
+ <literal>query_to_json(text, boolean)</literal>
+ </entry>
+ <entry>
+ Returns the result of running the query as JSON. If the
+ second parameter is true, there will be a line feed between records.
+ </entry>
+ <entry><literal>query_to_json('select 1 as a, $$foo$$ as b', false)</literal></entry>
+ <entry><literal>[{"a":1,"b":"foo"}]</literal></entry>
+ </row>
+ <row>
+ <entry>
+ <indexterm>
+ <primary>array_to_json</primary>
+ </indexterm>
+ <literal>array_to_json(anyarray)</literal>
+ </entry>
+ <entry>
+ Returns the array as JSON. A Postgres multi-dimensional array becomes a JSON
+ array of arrays.
+ </entry>
+ <entry><literal>array_to_json('{{1,5},{99,100}}'::int[])</literal></entry>
+ <entry><literal>[[1,5],[99,100]]</literal></entry>
+ </row>
+ <row>
+ <entry>
+ <indexterm>
+ <primary>row_to_json</primary>
+ </indexterm>
+ <literal>row_to_json(record)</literal>
+ </entry>
+ <entry>
+ Returns the row as JSON.
+ </entry>
+ <entry><literal>row_to_json(row(1,'foo'))</literal></entry>
+ <entry><literal>{"f1":1,"f2":"foo"}</literal></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ </sect1>
<sect1 id="functions-sequence">
<title>Sequence Manipulation Functions</title>
diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c
index 8b48105..ddb2784 100644
--- a/src/backend/commands/explain.c
+++ b/src/backend/commands/explain.c
@@ -24,6 +24,7 @@
#include "rewrite/rewriteHandler.h"
#include "tcop/tcopprot.h"
#include "utils/builtins.h"
+#include "utils/json.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"
@@ -99,7 +100,6 @@ static void ExplainDummyGroup(const char *objtype, const char *labelname,
static void ExplainXMLTag(const char *tagname, int flags, ExplainState *es);
static void ExplainJSONLineEnding(ExplainState *es);
static void ExplainYAMLLineStarting(ExplainState *es);
-static void escape_json(StringInfo buf, const char *str);
static void escape_yaml(StringInfo buf, const char *str);
@@ -242,7 +242,7 @@ ExplainResultDesc(ExplainStmt *stmt)
{
TupleDesc tupdesc;
ListCell *lc;
- bool xml = false;
+ Oid result_type = TEXTOID;
/* Check for XML format option */
foreach(lc, stmt->options)
@@ -253,7 +253,12 @@ ExplainResultDesc(ExplainStmt *stmt)
{
char *p = defGetString(opt);
- xml = (strcmp(p, "xml") == 0);
+ if (strcmp(p, "xml") == 0)
+ result_type = XMLOID;
+ else if (strcmp(p, "json") == 0)
+ result_type = JSONOID;
+ else
+ result_type = TEXTOID;
/* don't "break", as ExplainQuery will use the last value */
}
}
@@ -261,7 +266,7 @@ ExplainResultDesc(ExplainStmt *stmt)
/* Need a tuple descriptor representing a single TEXT or XML column */
tupdesc = CreateTemplateTupleDesc(1, false);
TupleDescInitEntry(tupdesc, (AttrNumber) 1, "QUERY PLAN",
- xml ? XMLOID : TEXTOID, -1, 0);
+ result_type, -1, 0);
return tupdesc;
}
@@ -2311,51 +2316,6 @@ ExplainYAMLLineStarting(ExplainState *es)
}
/*
- * Produce a JSON string literal, properly escaping characters in the text.
- */
-static 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, '\"');
-}
-
-/*
* YAML is a superset of JSON; unfortuantely, the YAML quoting rules are
* ridiculously complicated -- as documented in sections 5.3 and 7.3.3 of
* http://yaml.org/spec/1.2/spec.html -- so we chose to just quote everything.
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 5f968b0..c635c38 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -18,7 +18,7 @@ endif
OBJS = acl.o arrayfuncs.o array_userfuncs.o arrayutils.o bool.o \
cash.o char.o date.o datetime.o datum.o domains.o \
enum.o float.o format_type.o \
- geo_ops.o geo_selfuncs.o int.o int8.o like.o lockfuncs.o \
+ geo_ops.o geo_selfuncs.o int.o int8.o json.o like.o lockfuncs.o \
misc.o nabstime.o name.o numeric.o numutils.o \
oid.o oracle_compat.o pseudotypes.o rangetypes.o rangetypes_gist.o \
rowtypes.o regexp.o regproc.o ruleutils.o selfuncs.o \
diff --git a/src/backend/utils/adt/json.c b/src/backend/utils/adt/json.c
new file mode 100644
index 0000000..ac6bc60
--- /dev/null
+++ b/src/backend/utils/adt/json.c
@@ -0,0 +1,1067 @@
+/*-------------------------------------------------------------------------
+ *
+ * json.c
+ * JSON data type support.
+ *
+ * Portions Copyright (c) 1996-2012, 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_type.h"
+#include "executor/spi.h"
+#include "lib/stringinfo.h"
+#include "libpq/pqformat.h"
+#include "mb/pg_wchar.h"
+#include "parser/parse_coerce.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/json.h"
+#include "utils/typcache.h"
+
+typedef enum
+{
+ JSON_VALUE_INVALID,
+ JSON_VALUE_STRING,
+ JSON_VALUE_NUMBER,
+ JSON_VALUE_OBJECT,
+ JSON_VALUE_ARRAY,
+ JSON_VALUE_TRUE,
+ JSON_VALUE_FALSE,
+ JSON_VALUE_NULL
+} JsonValueType;
+
+typedef struct
+{
+ char *input;
+ char *token_start;
+ char *token_terminator;
+ JsonValueType token_type;
+ int line_number;
+ char *line_start;
+} JsonLexContext;
+
+typedef enum
+{
+ JSON_PARSE_VALUE, /* expecting a value */
+ JSON_PARSE_ARRAY_START, /* saw '[', expecting value or ']' */
+ JSON_PARSE_ARRAY_NEXT, /* saw array element, expecting ',' or ']' */
+ JSON_PARSE_OBJECT_START, /* saw '{', expecting label or '}' */
+ JSON_PARSE_OBJECT_LABEL, /* saw object label, expecting ':' */
+ JSON_PARSE_OBJECT_NEXT, /* saw object value, expecting ',' or '}' */
+ JSON_PARSE_OBJECT_COMMA /* saw object ',', expecting next label */
+} JsonParseState;
+
+typedef struct JsonParseStack
+{
+ JsonParseState state;
+} JsonParseStack;
+
+typedef enum
+{
+ JSON_STACKOP_NONE,
+ JSON_STACKOP_PUSH,
+ JSON_STACKOP_PUSH_WITH_PUSHBACK,
+ JSON_STACKOP_POP
+} JsonStackOp;
+
+typedef struct
+{
+ char *colname;
+ Oid typid;
+ TYPCATEGORY tcategory;
+ Oid toutputfunc;
+ bool tisvarlena;
+
+} result_metadata;
+
+static void json_validate_cstring(char *input);
+static void json_lex(JsonLexContext *lex);
+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 char *extract_mb_char(char *s);
+static void composite_to_json(Datum composite, StringInfo result);
+static void array_dim_to_json(StringInfo result, int dim, int ndims,int * dims,
+ Datum *vals, int * valcount, TYPCATEGORY tcategory,
+ Oid typoutputfunc);
+static void array_to_json_internal(Datum array, StringInfo result);
+
+static void rowset_row_to_json(int rownum, StringInfo result, result_metadata *meta);
+
+/*
+ * Input.
+ */
+Datum
+json_in(PG_FUNCTION_ARGS)
+{
+ char *text = PG_GETARG_CSTRING(0);
+
+ json_validate_cstring(text);
+
+ PG_RETURN_TEXT_P(cstring_to_text(text));
+}
+
+/*
+ * Output.
+ */
+Datum
+json_out(PG_FUNCTION_ARGS)
+{
+ Datum txt = PG_GETARG_DATUM(0);
+
+ PG_RETURN_CSTRING(TextDatumGetCString(txt));
+}
+
+/*
+ * Binary send.
+ */
+Datum
+json_send(PG_FUNCTION_ARGS)
+{
+ StringInfoData buf;
+ text *t = PG_GETARG_TEXT_PP(0);
+
+ 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);
+ text *result;
+ char *str;
+ int nbytes;
+
+ str = pq_getmsgtext(buf, buf->len - buf->cursor, &nbytes);
+
+ /*
+ * We need a null-terminated string to pass to json_validate_cstring().
+ * Rather than make a separate copy, make the temporary result one byte
+ * bigger than it needs to be.
+ */
+ result = palloc(nbytes + 1 + VARHDRSZ);
+ SET_VARSIZE(result, nbytes + VARHDRSZ);
+ memcpy(VARDATA(result), str, nbytes);
+ str = VARDATA(result);
+ str[nbytes] = '\0';
+
+ /* Validate it. */
+ json_validate_cstring(str);
+
+ PG_RETURN_TEXT_P(result);
+}
+
+/*
+ * Check whether supplied input is valid JSON.
+ */
+static void
+json_validate_cstring(char *input)
+{
+ JsonLexContext lex;
+ JsonParseStack *stack,
+ *stacktop;
+ int stacksize;
+
+ /* Set up lexing context. */
+ lex.input = input;
+ lex.token_terminator = lex.input;
+ lex.line_number = 1;
+ lex.line_start = input;
+
+ /* Set up parse stack. */
+ stacksize = 32;
+ stacktop = palloc(sizeof(JsonParseStack) * stacksize);
+ stack = stacktop;
+ stack->state = JSON_PARSE_VALUE;
+
+ /* Main parsing loop. */
+ for (;;)
+ {
+ JsonStackOp op;
+
+ /* Fetch next token. */
+ json_lex(&lex);
+
+ /* Check for unexpected end of input. */
+ if (lex.token_start == NULL)
+ report_parse_error(stack, &lex);
+
+redo:
+ /* Figure out what to do with this token. */
+ op = JSON_STACKOP_NONE;
+ switch (stack->state)
+ {
+ case JSON_PARSE_VALUE:
+ if (lex.token_type != JSON_VALUE_INVALID)
+ op = JSON_STACKOP_POP;
+ else if (lex.token_start[0] == '[')
+ stack->state = JSON_PARSE_ARRAY_START;
+ else if (lex.token_start[0] == '{')
+ stack->state = JSON_PARSE_OBJECT_START;
+ else
+ report_parse_error(stack, &lex);
+ break;
+ case JSON_PARSE_ARRAY_START:
+ if (lex.token_type != JSON_VALUE_INVALID)
+ stack->state = JSON_PARSE_ARRAY_NEXT;
+ else if (lex.token_start[0] == ']')
+ op = JSON_STACKOP_POP;
+ else if (lex.token_start[0] == '['
+ || lex.token_start[0] == '{')
+ {
+ stack->state = JSON_PARSE_ARRAY_NEXT;
+ op = JSON_STACKOP_PUSH_WITH_PUSHBACK;
+ }
+ else
+ report_parse_error(stack, &lex);
+ break;
+ case JSON_PARSE_ARRAY_NEXT:
+ if (lex.token_type != JSON_VALUE_INVALID)
+ report_parse_error(stack, &lex);
+ else if (lex.token_start[0] == ']')
+ op = JSON_STACKOP_POP;
+ else if (lex.token_start[0] == ',')
+ op = JSON_STACKOP_PUSH;
+ else
+ report_parse_error(stack, &lex);
+ break;
+ case JSON_PARSE_OBJECT_START:
+ if (lex.token_type == JSON_VALUE_STRING)
+ stack->state = JSON_PARSE_OBJECT_LABEL;
+ else if (lex.token_type == JSON_VALUE_INVALID
+ && lex.token_start[0] == '}')
+ op = JSON_STACKOP_POP;
+ else
+ report_parse_error(stack, &lex);
+ break;
+ case JSON_PARSE_OBJECT_LABEL:
+ if (lex.token_type == JSON_VALUE_INVALID
+ && lex.token_start[0] == ':')
+ {
+ stack->state = JSON_PARSE_OBJECT_NEXT;
+ op = JSON_STACKOP_PUSH;
+ }
+ else
+ report_parse_error(stack, &lex);
+ break;
+ case JSON_PARSE_OBJECT_NEXT:
+ if (lex.token_type != JSON_VALUE_INVALID)
+ report_parse_error(stack, &lex);
+ else if (lex.token_start[0] == '}')
+ op = JSON_STACKOP_POP;
+ else if (lex.token_start[0] == ',')
+ stack->state = JSON_PARSE_OBJECT_COMMA;
+ else
+ report_parse_error(stack, &lex);
+ break;
+ case JSON_PARSE_OBJECT_COMMA:
+ if (lex.token_type == JSON_VALUE_STRING)
+ stack->state = JSON_PARSE_OBJECT_LABEL;
+ else
+ report_parse_error(stack, &lex);
+ break;
+ default:
+ elog(ERROR, "unexpected json parse state: %d",
+ (int) stack->state);
+ }
+
+ /* Push or pop the stack, if needed. */
+ switch (op)
+ {
+ case JSON_STACKOP_PUSH:
+ case JSON_STACKOP_PUSH_WITH_PUSHBACK:
+ ++stack;
+ if (stack >= &stacktop[stacksize])
+ {
+ int stackoffset = stack - stacktop;
+ stacksize = stacksize + 32;
+ stacktop = repalloc(stacktop,
+ sizeof(JsonParseStack) * stacksize);
+ stack = stacktop + stackoffset;
+ }
+ stack->state = JSON_PARSE_VALUE;
+ if (op == JSON_STACKOP_PUSH_WITH_PUSHBACK)
+ goto redo;
+ break;
+ case JSON_STACKOP_POP:
+ if (stack == stacktop)
+ {
+ /* Expect end of input. */
+ json_lex(&lex);
+ if (lex.token_start != NULL)
+ report_parse_error(NULL, &lex);
+ return;
+ }
+ --stack;
+ break;
+ case JSON_STACKOP_NONE:
+ /* nothing to do */
+ break;
+ }
+ }
+}
+
+/*
+ * Lex one token from the input stream.
+ */
+static void
+json_lex(JsonLexContext *lex)
+{
+ char *s;
+
+ /* Skip leading whitespace. */
+ s = lex->token_terminator;
+ while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r')
+ {
+ if (*s == '\n')
+ ++lex->line_number;
+ ++s;
+ }
+ lex->token_start = s;
+
+ /* Determine token type. */
+ if (strchr("{}[],:", s[0]))
+ {
+ /* strchr() doesn't return false on a NUL input. */
+ if (s[0] == '\0')
+ {
+ /* End of string. */
+ lex->token_start = NULL;
+ lex->token_terminator = NULL;
+ }
+ else
+ {
+ /* Single-character token, some kind of punctuation mark. */
+ lex->token_terminator = s + 1;
+ }
+ lex->token_type = JSON_VALUE_INVALID;
+ }
+ else if (*s == '"')
+ {
+ /* String. */
+ json_lex_string(lex);
+ lex->token_type = JSON_VALUE_STRING;
+ }
+ else if (*s == '-')
+ {
+ /* Negative number. */
+ json_lex_number(lex, s + 1);
+ lex->token_type = JSON_VALUE_NUMBER;
+ }
+ else if (*s >= '0' && *s <= '9')
+ {
+ /* Positive number. */
+ json_lex_number(lex, s);
+ lex->token_type = JSON_VALUE_NUMBER;
+ }
+ else
+ {
+ char *p;
+
+ /*
+ * We're not dealing with a string, number, legal punctuation mark,
+ * or end of string. The only legal tokens we might find here are
+ * true, false, and null, but for error reporting purposes we scan
+ * until we see a non-alphanumeric character. That way, we can report
+ * the whole word as an unexpected token, rather than just some
+ * unintuitive prefix thereof.
+ */
+ for (p = s; (*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')
+ || (*p >= '0' && *p <= '9') || *p == '_' || IS_HIGHBIT_SET(*p);
+ ++p)
+ ;
+
+ /*
+ * We got some sort of unexpected punctuation or an otherwise
+ * unexpected character, so just complain about that one character.
+ */
+ if (p == s)
+ {
+ lex->token_terminator = s + 1;
+ report_invalid_token(lex);
+ }
+
+ /*
+ * We've got a real alphanumeric token here. If it happens to be
+ * true, false, or null, all is well. If not, error out.
+ */
+ lex->token_terminator = p;
+ if (p - s == 4)
+ {
+ if (memcmp(s, "true", 4) == 0)
+ lex->token_type = JSON_VALUE_TRUE;
+ else if (memcmp(s, "null", 4) == 0)
+ lex->token_type = JSON_VALUE_NULL;
+ else
+ report_invalid_token(lex);
+ }
+ else if (p - s == 5 && memcmp(s, "false", 5) == 0)
+ lex->token_type = JSON_VALUE_FALSE;
+ else
+ report_invalid_token(lex);
+ }
+}
+
+/*
+ * The next token in the input stream is known to be a string; lex it.
+ */
+static void
+json_lex_string(JsonLexContext *lex)
+{
+ char *s = lex->token_start + 1;
+
+ for (s = lex->token_start + 1; *s != '"'; ++s)
+ {
+ /* Per RFC4627, these characters MUST be escaped. */
+ if (*s < 32)
+ {
+ /* A NUL byte marks the (premature) end of the string. */
+ if (*s == '\0')
+ {
+ lex->token_terminator = s;
+ report_invalid_token(lex);
+ }
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid input syntax for type json"),
+ errdetail_internal("line %d: Character \"%c\" must be escaped.",
+ lex->line_number, *s)));
+ }
+ else if (*s == '\\')
+ {
+ /* OK, we have an escape character. */
+ ++s;
+ if (*s == '\0')
+ {
+ lex->token_terminator = s;
+ report_invalid_token(lex);
+ }
+ else if (*s == 'u')
+ {
+ int i;
+ int ch = 0;
+
+ for (i = 1; i <= 4; ++i)
+ {
+ if (s[i] == '\0')
+ {
+ lex->token_terminator = s + i;
+ report_invalid_token(lex);
+ }
+ else if (s[i] >= '0' && s[i] <= '9')
+ ch = (ch * 16) + (s[i] - '0');
+ else if (s[i] >= 'a' && s[i] <= 'f')
+ ch = (ch * 16) + (s[i] - 'a') + 10;
+ else if (s[i] >= 'A' && s[i] <= 'F')
+ ch = (ch * 16) + (s[i] - 'A') + 10;
+ else
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid input syntax for type json"),
+ errdetail_internal("line %d: \"\\u\" must be followed by four hexadecimal digits.",
+ lex->line_number)));
+ }
+ }
+
+ /*
+ * If the database encoding is not UTF-8, we support \uXXXX
+ * escapes only for characters 0-127.
+ */
+ if (GetDatabaseEncoding() != PG_UTF8)
+ {
+ if (ch > 127)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid input syntax for type json"),
+ errdetail_internal("line %d: \\uXXXX escapes are supported for non-ASCII characters only under UTF-8.",
+ lex->line_number)));
+ }
+
+ /* Account for the four additional bytes we just parsed. */
+ s += 4;
+ }
+ else if (!strchr("\"\\/bfnrt", *s))
+ {
+ /* Error out. */
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid input syntax for type json"),
+ errdetail_internal("line %d: Invalid escape \"\\%s\".",
+ lex->line_number, extract_mb_char(s))));
+ }
+ }
+ }
+
+ /* Hooray, we found the end of the string! */
+ lex->token_terminator = s + 1;
+}
+
+/*-------------------------------------------------------------------------
+ * The next token in the input stream is known to be a number; lex it.
+ *
+ * In JSON, a number consists of four parts:
+ *
+ * (1) An optional minus sign ('-').
+ *
+ * (2) Either a single '0', or a string of one or more digits that does not
+ * begin with a '0'.
+ *
+ * (3) An optional decimal part, consisting of a period ('.') followed by
+ * one or more digits. (Note: While this part can be omitted
+ * completely, it's not OK to have only the decimal point without
+ * any digits afterwards.)
+ *
+ * (4) An optional exponent part, consisting of 'e' or 'E', optionally
+ * followed by '+' or '-', followed by one or more digits. (Note:
+ * As with the decimal part, if 'e' or 'E' is present, it must be
+ * followed by at least one digit.)
+ *
+ * The 's' argument to this function points to the ostensible beginning
+ * of part 2 - i.e. the character after any optional minus sign, and the
+ * first character of the string if there is none.
+ *
+ *-------------------------------------------------------------------------
+ */
+static void
+json_lex_number(JsonLexContext *lex, char *s)
+{
+ bool error = false;
+ char *p;
+
+ /* Part (1): leading sign indicator. */
+ /* Caller already did this for us; so do nothing. */
+
+ /* Part (2): parse main digit string. */
+ if (*s == '0')
+ ++s;
+ else if (*s >= '1' && *s <= '9')
+ {
+ do
+ {
+ ++s;
+ } while (*s >= '0' && *s <= '9');
+ }
+ else
+ error = true;
+
+ /* Part (3): parse optional decimal portion. */
+ if (*s == '.')
+ {
+ ++s;
+ if (*s < '0' && *s > '9')
+ error = true;
+ else
+ {
+ do
+ {
+ ++s;
+ } while (*s >= '0' && *s <= '9');
+ }
+ }
+
+ /* Part (4): parse optional exponent. */
+ if (*s == 'e' || *s == 'E')
+ {
+ ++s;
+ if (*s == '+' || *s == '-')
+ ++s;
+ if (*s < '0' && *s > '9')
+ error = true;
+ else
+ {
+ do
+ {
+ ++s;
+ } while (*s >= '0' && *s <= '9');
+ }
+ }
+
+ /* Check for trailing garbage. */
+ for (p = s; (*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z')
+ || (*p >= '0' && *p <= '9') || *p == '_' || IS_HIGHBIT_SET(*p); ++p)
+ ;
+ lex->token_terminator = p;
+ if (p > s || error)
+ report_invalid_token(lex);
+}
+
+/*
+ * Report a parse error.
+ */
+static void
+report_parse_error(JsonParseStack *stack, JsonLexContext *lex)
+{
+ char *detail = NULL;
+ char *token = NULL;
+ int toklen;
+
+ /* Handle case where the input ended prematurely. */
+ if (lex->token_start == NULL)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid input syntax for type json: \"%s\"",
+ lex->input),
+ errdetail_internal("The input string ended unexpectedly.")));
+
+ /* Work out the offending token. */
+ toklen = lex->token_terminator - lex->token_start;
+ token = palloc(toklen + 1);
+ memcpy(token, lex->token_start, toklen);
+ token[toklen] = '\0';
+
+ /* Select correct detail message. */
+ if (stack == NULL)
+ detail = "line %d: Expected end of input, but found \"%s\".";
+ else
+ {
+ switch (stack->state)
+ {
+ case JSON_PARSE_VALUE:
+ detail = "line %d: Expected string, number, object, array, true, false, or null, but found \"%s\".";
+ break;
+ case JSON_PARSE_ARRAY_START:
+ detail = "line %d: Expected array element or \"]\", but found \"%s\".";
+ break;
+ case JSON_PARSE_ARRAY_NEXT:
+ detail = "line %d: Expected \",\" or \"]\", but found \"%s\".";
+ break;
+ case JSON_PARSE_OBJECT_START:
+ detail = "line %d: Expected string or \"}\", but found \"%s\".";
+ break;
+ case JSON_PARSE_OBJECT_LABEL:
+ detail = "line %d: Expected \":\", but found \"%s\".";
+ break;
+ case JSON_PARSE_OBJECT_NEXT:
+ detail = "line %d: Expected \",\" or \"}\", but found \"%s\".";
+ break;
+ case JSON_PARSE_OBJECT_COMMA:
+ detail = "line %d: Expected string, but found \"%s\".";
+ break;
+ }
+ }
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid input syntax for type json: \"%s\"",
+ lex->input),
+ errdetail_internal(detail, lex->line_number, token)));
+}
+
+/*
+ * Report an invalid input token.
+ */
+static void
+report_invalid_token(JsonLexContext *lex)
+{
+ char *token;
+ int toklen;
+
+ toklen = lex->token_terminator - lex->token_start;
+ token = palloc(toklen + 1);
+ memcpy(token, lex->token_start, toklen);
+ token[toklen] = '\0';
+
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
+ errmsg("invalid input syntax for type json"),
+ errdetail_internal("line %d: Token \"%s\" is invalid.",
+ lex->line_number, token)));
+}
+
+/*
+ * Extract a single, possibly multi-byte char from the input string.
+ */
+static char *
+extract_mb_char(char *s)
+{
+ char *res;
+ int len;
+
+ len = pg_mblen(s);
+ res = palloc(len + 1);
+ memcpy(res, s, len);
+ res[len] = '\0';
+
+ return res;
+}
+
+static inline void
+datum_to_json(Datum val, StringInfo result, TYPCATEGORY tcategory, Oid typoutputfunc)
+{
+
+ char *outputstr;
+
+ if (val == (Datum) NULL)
+ {
+ appendStringInfoString(result,"null");
+ return;
+ }
+
+ switch (tcategory)
+ {
+ case TYPCATEGORY_ARRAY:
+ array_to_json_internal(val, result);
+ break;
+ case TYPCATEGORY_COMPOSITE:
+ composite_to_json(val, result);
+ break;
+ case TYPCATEGORY_BOOLEAN:
+ if (DatumGetBool(val))
+ appendStringInfoString(result,"true");
+ else
+ appendStringInfoString(result,"false");
+ break;
+ case TYPCATEGORY_NUMERIC:
+ outputstr = OidOutputFunctionCall(typoutputfunc, val);
+ appendStringInfoString(result, outputstr);
+ pfree(outputstr);
+ break;
+ default:
+ outputstr = OidOutputFunctionCall(typoutputfunc, val);
+ escape_json(result, outputstr);
+ pfree(outputstr);
+ }
+}
+
+static void
+array_dim_to_json(StringInfo result, int dim, int ndims,int * dims, Datum *vals, int * valcount, TYPCATEGORY tcategory, Oid typoutputfunc)
+{
+
+ int i;
+
+ Assert(dim < ndims);
+
+ appendStringInfoChar(result, '[');
+
+ for (i = 1; i <= dims[dim]; i++)
+ {
+ if (i > 1)
+ appendStringInfoChar(result,',');
+
+ if (dim + 1 == ndims)
+ {
+ datum_to_json(vals[*valcount],result,tcategory,typoutputfunc);
+ (*valcount)++;
+ }
+ else
+ {
+ array_dim_to_json(result,dim+1,ndims,dims,vals,valcount,tcategory,typoutputfunc);
+ }
+ }
+
+ appendStringInfoChar(result, ']');
+}
+
+
+static void
+array_to_json_internal(Datum array, StringInfo result)
+{
+ 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,
+ typdelim;
+ Oid typioparam;
+ Oid typoutputfunc;
+ TYPCATEGORY tcategory;
+
+ ndim = ARR_NDIM(v);
+ dim = ARR_DIMS(v);
+ nitems = ArrayGetNItems(ndim, dim);
+
+ if (nitems <= 0)
+ {
+ appendStringInfoString(result,"[]");
+ return;
+ }
+
+ get_type_io_data(element_type, IOFunc_output,
+ &typlen, &typbyval, &typalign,
+ &typdelim, &typioparam, &typoutputfunc);
+
+
+ deconstruct_array(v, element_type, typlen, typbyval,
+ typalign, &elements, &nulls,
+ &nitems);
+
+ /* can't have an array of arrays, so this is the only special case here */
+ if (element_type == RECORDOID)
+ tcategory = TYPCATEGORY_COMPOSITE;
+ else
+ tcategory = TypeCategory(element_type);
+
+ array_dim_to_json(result,0,ndim,dim,elements,&count,tcategory, typoutputfunc);
+
+ pfree(elements);
+ pfree(nulls);
+}
+
+
+static void
+composite_to_json(Datum composite, StringInfo result)
+{
+ HeapTupleHeader td;
+ Oid tupType;
+ int32 tupTypmod;
+ TupleDesc tupdesc;
+ HeapTupleData tmptup, *tuple;
+ int i;
+ bool needsep = false;
+
+ 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, origval;
+ bool isnull;
+ char *attname;
+ TYPCATEGORY tcategory;
+ Oid typoutput;
+ bool typisvarlena;
+
+ if (tupdesc->attrs[i]->attisdropped)
+ continue;
+
+ if (needsep)
+ appendStringInfoChar(result,',');
+ needsep = true;
+
+ attname = NameStr(tupdesc->attrs[i]->attname);
+ escape_json(result,attname);
+ appendStringInfoChar(result,':');
+
+ origval = heap_getattr(tuple, i + 1, tupdesc, &isnull);
+
+ if (tupdesc->attrs[i]->atttypid == RECORDARRAYOID)
+ tcategory = TYPCATEGORY_ARRAY;
+ else if (tupdesc->attrs[i]->atttypid == RECORDOID)
+ tcategory = TYPCATEGORY_COMPOSITE;
+ else
+ tcategory = TypeCategory(tupdesc->attrs[i]->atttypid);
+
+ /* XXX TODO: cache this info */
+ 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.
+ */
+ if (typisvarlena && ! isnull)
+ val = PointerGetDatum(PG_DETOAST_DATUM(origval));
+ else
+ val = origval;
+
+ datum_to_json(val, result, tcategory, typoutput);
+
+ /* Clean up detoasted copy, if any */
+ if (val != origval)
+ pfree(DatumGetPointer(val));
+
+ }
+
+ appendStringInfoChar(result,'}');
+ ReleaseTupleDesc(tupdesc);
+
+
+}
+
+static void
+rowset_row_to_json(int rownum, StringInfo result, result_metadata *meta)
+{
+ int i;
+
+ appendStringInfoChar(result, '{');
+
+ for (i = 1; i <= SPI_tuptable->tupdesc->natts; i++)
+ {
+ Datum colval;
+ bool isnull;
+
+ if (i > 1)
+ appendStringInfoChar(result, ',');
+
+ escape_json(result,meta[i-1].colname);
+ appendStringInfoChar(result,':');
+
+ /* XXX should we get a detoasted copy of this? */
+
+ colval = SPI_getbinval(SPI_tuptable->vals[rownum],
+ SPI_tuptable->tupdesc,
+ i,
+ &isnull);
+
+ datum_to_json(colval, result, meta[i-1].tcategory, meta[i-1].toutputfunc);
+ }
+
+ appendStringInfoChar(result, '}');
+
+
+}
+
+extern Datum
+query_to_json(PG_FUNCTION_ARGS)
+{
+ text *qtext = PG_GETARG_TEXT_PP(0);
+ bool lf_between_records = PG_GETARG_BOOL(1);
+ char *query = text_to_cstring(qtext);
+ StringInfo result;
+ result_metadata *meta;
+ int i;
+ char *sep;
+
+ sep = lf_between_records ? ",\n " : ",";
+
+ result = makeStringInfo();
+
+ appendStringInfoChar(result,'[');
+
+ SPI_connect();
+
+ if (SPI_execute(query, true, 0) != SPI_OK_SELECT)
+ ereport(ERROR,
+ (errcode(ERRCODE_DATA_EXCEPTION),
+ errmsg("invalid query")));
+
+ meta = palloc(SPI_tuptable->tupdesc->natts * sizeof(result_metadata));
+
+ for (i = 1; i <= SPI_tuptable->tupdesc->natts; i++)
+ {
+ result_metadata *m = &(meta[i-1]);
+
+ m->colname = SPI_fname(SPI_tuptable->tupdesc, i);
+ m->typid = SPI_gettypeid(SPI_tuptable->tupdesc, i);
+ /*
+ * for our purposes RECORDARRAYOID is an ARRAY
+ * and RECORDOID is a composite
+ */
+ if (m->typid == RECORDARRAYOID)
+ m->tcategory = TYPCATEGORY_ARRAY;
+ else if (m->typid == RECORDOID)
+ m->tcategory = TYPCATEGORY_COMPOSITE;
+ else
+ m->tcategory = TypeCategory(m->typid);
+ getTypeOutputInfo(m->typid, &(m->toutputfunc), &(m->tisvarlena));
+ }
+
+ for (i = 0; i < SPI_processed; i++)
+ {
+ if (i > 0)
+ appendStringInfoString(result,sep);
+
+ rowset_row_to_json(i, result, meta);
+ }
+
+ SPI_finish();
+
+ appendStringInfoChar(result,']');
+
+ PG_RETURN_TEXT_P(cstring_to_text(result->data));
+
+}
+
+extern Datum
+array_to_json(PG_FUNCTION_ARGS)
+{
+ Datum array = PG_GETARG_DATUM(0);
+ StringInfo result;
+
+ result = makeStringInfo();
+
+ array_to_json_internal(array, result);
+
+ PG_RETURN_TEXT_P(cstring_to_text(result->data));
+};
+
+extern Datum
+row_to_json(PG_FUNCTION_ARGS)
+{
+ Datum array = PG_GETARG_DATUM(0);
+ StringInfo result;
+
+ result = makeStringInfo();
+
+ composite_to_json(array, result);
+
+ PG_RETURN_TEXT_P(cstring_to_text(result->data));
+};
+
+/*
+ * 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, '\"');
+}
+
diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h
index 355c61a..6841e61 100644
--- a/src/include/catalog/pg_proc.h
+++ b/src/include/catalog/pg_proc.h
@@ -4006,6 +4006,22 @@ DESCR("determine if a string is well formed XML document");
DATA(insert OID = 3053 ( xml_is_well_formed_content PGNSP PGUID 12 1 0 0 0 f f f t f i 1 0 16 "25" _null_ _null_ _null_ _null_ xml_is_well_formed_content _null_ _null_ _null_ ));
DESCR("determine if a string is well formed XML content");
+/* json */
+DATA(insert OID = 321 ( json_in PGNSP PGUID 12 1 0 0 0 f f f t f s 1 0 114 "2275" _null_ _null_ _null_ _null_ json_in _null_ _null_ _null_ ));
+DESCR("I/O");
+DATA(insert OID = 322 ( json_out PGNSP PGUID 12 1 0 0 0 f f f t f i 1 0 2275 "114" _null_ _null_ _null_ _null_ json_out _null_ _null_ _null_ ));
+DESCR("I/O");
+DATA(insert OID = 323 ( json_recv PGNSP PGUID 12 1 0 0 0 f f f t f s 1 0 114 "2281" _null_ _null_ _null_ _null_ json_recv _null_ _null_ _null_ ));
+DESCR("I/O");
+DATA(insert OID = 324 ( json_send PGNSP PGUID 12 1 0 0 0 f f f t f s 1 0 17 "114" _null_ _null_ _null_ _null_ json_send _null_ _null_ _null_ ));
+DESCR("I/O");
+DATA(insert OID = 3144 ( query_to_json PGNSP PGUID 12 1 0 0 0 f f f t f s 2 0 114 "25 16" _null_ _null_ _null_ _null_ query_to_json _null_ _null_ _null_ ));
+DESCR("I/O");
+DATA(insert OID = 3145 ( array_to_json PGNSP PGUID 12 1 0 0 0 f f f t f s 1 0 114 "2277" _null_ _null_ _null_ _null_ array_to_json _null_ _null_ _null_ ));
+DESCR("I/O");
+DATA(insert OID = 3146 ( row_to_json PGNSP PGUID 12 1 0 0 0 f f f t f s 1 0 114 "2249" _null_ _null_ _null_ _null_ row_to_json _null_ _null_ _null_ ));
+DESCR("I/O");
+
/* uuid */
DATA(insert OID = 2952 ( uuid_in PGNSP PGUID 12 1 0 0 0 f f f t f i 1 0 2950 "2275" _null_ _null_ _null_ _null_ uuid_in _null_ _null_ _null_ ));
DESCR("I/O");
diff --git a/src/include/catalog/pg_type.h b/src/include/catalog/pg_type.h
index e12efe4..2719bc4 100644
--- a/src/include/catalog/pg_type.h
+++ b/src/include/catalog/pg_type.h
@@ -350,10 +350,13 @@ DATA(insert OID = 81 ( pg_proc PGNSP PGUID -1 f c C f t \054 1255 0 0 record_i
DATA(insert OID = 83 ( pg_class PGNSP PGUID -1 f c C f t \054 1259 0 0 record_in record_out record_recv record_send - - - d x f 0 -1 0 0 _null_ _null_ _null_ ));
/* OIDS 100 - 199 */
+DATA(insert OID = 114 ( json PGNSP PGUID -1 f b U f t \054 0 0 199 json_in json_out json_recv json_send - - - i x f 0 -1 0 0 _null_ _null_ _null_ ));
+#define JSONOID 114
DATA(insert OID = 142 ( xml PGNSP PGUID -1 f b U f t \054 0 0 143 xml_in xml_out xml_recv xml_send - - - i x f 0 -1 0 0 _null_ _null_ _null_ ));
DESCR("XML content");
#define XMLOID 142
DATA(insert OID = 143 ( _xml PGNSP PGUID -1 f b A f t \054 0 142 0 array_in array_out array_recv array_send - - - i x f 0 -1 0 0 _null_ _null_ _null_ ));
+DATA(insert OID = 199 ( _json PGNSP PGUID -1 f b A f t \054 0 114 0 array_in array_out array_recv array_send - - - i x f 0 -1 0 0 _null_ _null_ _null_ ));
DATA(insert OID = 194 ( pg_node_tree PGNSP PGUID -1 f b S f t \054 0 0 0 pg_node_tree_in pg_node_tree_out pg_node_tree_recv pg_node_tree_send - - - i x f 0 -1 0 100 _null_ _null_ _null_ ));
DESCR("string representing an internal node tree");
diff --git a/src/include/utils/json.h b/src/include/utils/json.h
new file mode 100644
index 0000000..5d150eb
--- /dev/null
+++ b/src/include/utils/json.h
@@ -0,0 +1,28 @@
+/*-------------------------------------------------------------------------
+ *
+ * json.h
+ * Declarations for JSON data type support.
+ *
+ * Portions Copyright (c) 1996-2012, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/json.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef JSON_H
+#define JSON_H
+
+#include "fmgr.h"
+
+extern Datum json_in(PG_FUNCTION_ARGS);
+extern Datum json_out(PG_FUNCTION_ARGS);
+extern Datum json_recv(PG_FUNCTION_ARGS);
+extern Datum json_send(PG_FUNCTION_ARGS);
+extern Datum query_to_json(PG_FUNCTION_ARGS);
+extern Datum array_to_json(PG_FUNCTION_ARGS);
+extern Datum row_to_json(PG_FUNCTION_ARGS);
+extern void escape_json(StringInfo buf, const char *str);
+
+#endif /* XML_H */
diff --git a/src/test/regress/expected/json.out b/src/test/regress/expected/json.out
new file mode 100644
index 0000000..b975d72
--- /dev/null
+++ b/src/test/regress/expected/json.out
@@ -0,0 +1,360 @@
+-- Strings.
+SELECT '""'::json; -- OK.
+ json
+------
+ ""
+(1 row)
+
+SELECT $$''$$::json; -- ERROR, single quotes are not allowed
+ERROR: invalid input syntax for type json
+LINE 1: SELECT $$''$$::json;
+ ^
+DETAIL: line 1: Token "'" is invalid.
+SELECT '"abc"'::json; -- OK
+ json
+-------
+ "abc"
+(1 row)
+
+SELECT '"abc'::json; -- ERROR, quotes not closed
+ERROR: invalid input syntax for type json
+LINE 1: SELECT '"abc'::json;
+ ^
+DETAIL: line 1: Token ""abc" is invalid.
+SELECT '"abc
+def"'::json; -- ERROR, unescaped newline in string constant
+ERROR: invalid input syntax for type json
+LINE 1: SELECT '"abc
+ ^
+DETAIL: line 1: Character "
+" must be escaped.
+SELECT '"\n\"\\"'::json; -- OK, legal escapes
+ json
+----------
+ "\n\"\\"
+(1 row)
+
+SELECT '"\v"'::json; -- ERROR, not a valid JSON escape
+ERROR: invalid input syntax for type json
+LINE 1: SELECT '"\v"'::json;
+ ^
+DETAIL: line 1: Invalid escape "\v".
+SELECT '"\u"'::json; -- ERROR, incomplete escape
+ERROR: invalid input syntax for type json
+LINE 1: SELECT '"\u"'::json;
+ ^
+DETAIL: line 1: "\u" must be followed by four hexadecimal digits.
+SELECT '"\u00"'::json; -- ERROR, incomplete escape
+ERROR: invalid input syntax for type json
+LINE 1: SELECT '"\u00"'::json;
+ ^
+DETAIL: line 1: "\u" must be followed by four hexadecimal digits.
+SELECT '"\u000g"'::json; -- ERROR, g is not a hex digit
+ERROR: invalid input syntax for type json
+LINE 1: SELECT '"\u000g"'::json;
+ ^
+DETAIL: line 1: "\u" must be followed by four hexadecimal digits.
+SELECT '"\u0000"'::json; -- OK, legal escape
+ json
+----------
+ "\u0000"
+(1 row)
+
+SELECT '"\uaBcD"'::json; -- OK, uppercase and lower case both OK
+ json
+----------
+ "\uaBcD"
+(1 row)
+
+-- Numbers.
+SELECT '1'::json; -- OK
+ json
+------
+ 1
+(1 row)
+
+SELECT '0'::json; -- OK
+ json
+------
+ 0
+(1 row)
+
+SELECT '01'::json; -- ERROR, not valid according to JSON spec
+ERROR: invalid input syntax for type json
+LINE 1: SELECT '01'::json;
+ ^
+DETAIL: line 1: Token "01" is invalid.
+SELECT '0.1'::json; -- OK
+ json
+------
+ 0.1
+(1 row)
+
+SELECT '9223372036854775808'::json; -- OK, even though it's too large for int8
+ json
+---------------------
+ 9223372036854775808
+(1 row)
+
+SELECT '1e100'::json; -- OK
+ json
+-------
+ 1e100
+(1 row)
+
+SELECT '1.3e100'::json; -- OK
+ json
+---------
+ 1.3e100
+(1 row)
+
+SELECT '1f2'::json; -- ERROR
+ERROR: invalid input syntax for type json
+LINE 1: SELECT '1f2'::json;
+ ^
+DETAIL: line 1: Token "1f2" is invalid.
+-- Arrays.
+SELECT '[]'::json; -- OK
+ json
+------
+ []
+(1 row)
+
+SELECT '[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]'::json; -- OK
+ json
+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
+(1 row)
+
+SELECT '[1,2]'::json; -- OK
+ json
+-------
+ [1,2]
+(1 row)
+
+SELECT '[1,2,]'::json; -- ERROR, trailing comma
+ERROR: invalid input syntax for type json: "[1,2,]"
+LINE 1: SELECT '[1,2,]'::json;
+ ^
+DETAIL: line 1: Expected string, number, object, array, true, false, or null, but found "]".
+SELECT '[1,2'::json; -- ERROR, no closing bracket
+ERROR: invalid input syntax for type json: "[1,2"
+LINE 1: SELECT '[1,2'::json;
+ ^
+DETAIL: The input string ended unexpectedly.
+SELECT '[1,[2]'::json; -- ERROR, no closing bracket
+ERROR: invalid input syntax for type json: "[1,[2]"
+LINE 1: SELECT '[1,[2]'::json;
+ ^
+DETAIL: The input string ended unexpectedly.
+-- Objects.
+SELECT '{}'::json; -- OK
+ json
+------
+ {}
+(1 row)
+
+SELECT '{"abc"}'::json; -- ERROR, no value
+ERROR: invalid input syntax for type json: "{"abc"}"
+LINE 1: SELECT '{"abc"}'::json;
+ ^
+DETAIL: line 1: Expected ":", but found "}".
+SELECT '{"abc":1}'::json; -- OK
+ json
+-----------
+ {"abc":1}
+(1 row)
+
+SELECT '{1:"abc"}'::json; -- ERROR, keys must be strings
+ERROR: invalid input syntax for type json: "{1:"abc"}"
+LINE 1: SELECT '{1:"abc"}'::json;
+ ^
+DETAIL: line 1: Expected string or "}", but found "1".
+SELECT '{"abc",1}'::json; -- ERROR, wrong separator
+ERROR: invalid input syntax for type json: "{"abc",1}"
+LINE 1: SELECT '{"abc",1}'::json;
+ ^
+DETAIL: line 1: Expected ":", but found ",".
+SELECT '{"abc"=1}'::json; -- ERROR, totally wrong separator
+ERROR: invalid input syntax for type json
+LINE 1: SELECT '{"abc"=1}'::json;
+ ^
+DETAIL: line 1: Token "=" is invalid.
+SELECT '{"abc"::1}'::json; -- ERROR, another wrong separator
+ERROR: invalid input syntax for type json: "{"abc"::1}"
+LINE 1: SELECT '{"abc"::1}'::json;
+ ^
+DETAIL: line 1: Expected string, number, object, array, true, false, or null, but found ":".
+SELECT '{"abc":1,"def":2,"ghi":[3,4],"hij":{"klm":5,"nop":[6]}}'::json; -- OK
+ json
+---------------------------------------------------------
+ {"abc":1,"def":2,"ghi":[3,4],"hij":{"klm":5,"nop":[6]}}
+(1 row)
+
+SELECT '{"abc":1:2}'::json; -- ERROR, colon in wrong spot
+ERROR: invalid input syntax for type json: "{"abc":1:2}"
+LINE 1: SELECT '{"abc":1:2}'::json;
+ ^
+DETAIL: line 1: Expected "," or "}", but found ":".
+SELECT '{"abc":1,3}'::json; -- ERROR, no value
+ERROR: invalid input syntax for type json: "{"abc":1,3}"
+LINE 1: SELECT '{"abc":1,3}'::json;
+ ^
+DETAIL: line 1: Expected string, but found "3".
+-- Miscellaneous stuff.
+SELECT 'true'::json; -- OK
+ json
+------
+ true
+(1 row)
+
+SELECT 'false'::json; -- OK
+ json
+-------
+ false
+(1 row)
+
+SELECT 'null'::json; -- OK
+ json
+------
+ null
+(1 row)
+
+SELECT ' true '::json; -- OK, even with extra whitespace
+ json
+--------
+ true
+(1 row)
+
+SELECT 'true false'::json; -- ERROR, too many values
+ERROR: invalid input syntax for type json: "true false"
+LINE 1: SELECT 'true false'::json;
+ ^
+DETAIL: line 1: Expected end of input, but found "false".
+SELECT 'true, false'::json; -- ERROR, too many values
+ERROR: invalid input syntax for type json: "true, false"
+LINE 1: SELECT 'true, false'::json;
+ ^
+DETAIL: line 1: Expected end of input, but found ",".
+SELECT 'truf'::json; -- ERROR, not a keyword
+ERROR: invalid input syntax for type json
+LINE 1: SELECT 'truf'::json;
+ ^
+DETAIL: line 1: Token "truf" is invalid.
+SELECT 'trues'::json; -- ERROR, not a keyword
+ERROR: invalid input syntax for type json
+LINE 1: SELECT 'trues'::json;
+ ^
+DETAIL: line 1: Token "trues" is invalid.
+SELECT ''::json; -- ERROR, no value
+ERROR: invalid input syntax for type json: ""
+LINE 1: SELECT ''::json;
+ ^
+DETAIL: The input string ended unexpectedly.
+SELECT ' '::json; -- ERROR, no value
+ERROR: invalid input syntax for type json: " "
+LINE 1: SELECT ' '::json;
+ ^
+DETAIL: The input string ended unexpectedly.
+-- query_to_json
+SELECT query_to_json('select 1 as a',false);
+ query_to_json
+---------------
+ [{"a":1}]
+(1 row)
+
+SELECT query_to_json('select x as b, x * 2 as c from generate_series(1,3) x',false);
+ query_to_json
+---------------------------------------------
+ [{"b":1,"c":2},{"b":2,"c":4},{"b":3,"c":6}]
+(1 row)
+
+SELECT query_to_json('select x as b, x * 2 as c from generate_series(1,3) x',true);
+ query_to_json
+-----------------
+ [{"b":1,"c":2},+
+ {"b":2,"c":4},+
+ {"b":3,"c":6}]
+(1 row)
+
+SELECT query_to_json('
+ 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',true);
+ query_to_json
+----------------------------------------------------------------------
+ [{"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 query_to_json('select array_agg(x) as d from generate_series(5,10) x',false);
+ query_to_json
+------------------------
+ [{"d":[5,6,7,8,9,10]}]
+(1 row)
+
+-- array_to_json
+SELECT array_to_json(array_agg(x))
+FROM generate_series(1,10) x;
+ array_to_json
+------------------------
+ [1,2,3,4,5,6,7,8,9,10]
+(1 row)
+
+SELECT array_to_json(array_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;
+ array_to_json
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ [{"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 array_to_json('{{1,5},{99,100}}'::int[]);
+ array_to_json
+------------------
+ [[1,5],[99,100]]
+(1 row)
+
+-- row_to_json
+SELECT row_to_json(row(1,'foo'));
+ row_to_json
+---------------------
+ {"f1":1,"f2":"foo"}
+(1 row)
+
+SELECT row_to_json(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;
+ row_to_json
+--------------------------------------------------------------------
+ {"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]}]}
+(4 rows)
+
+CREATE TEMP TABLE rows AS
+SELECT x, 'txt' || x as y
+FROM generate_series(1,3) AS x;
+SELECT row_to_json(q)
+FROM rows q;
+ row_to_json
+--------------------
+ {"x":1,"y":"txt1"}
+ {"x":2,"y":"txt2"}
+ {"x":3,"y":"txt3"}
+(3 rows)
+
diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule
index 3bedad0..d6105f8 100644
--- a/src/test/regress/parallel_schedule
+++ b/src/test/regress/parallel_schedule
@@ -92,7 +92,7 @@ test: rules
# ----------
# Another group of parallel tests
# ----------
-test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock
+test: select_views portals_p2 foreign_key cluster dependency guc bitmapops combocid tsearch tsdicts foreign_data window xmlmap functional_deps advisory_lock json
# ----------
# Another group of parallel tests
diff --git a/src/test/regress/serial_schedule b/src/test/regress/serial_schedule
index 0b64569..90726ce 100644
--- a/src/test/regress/serial_schedule
+++ b/src/test/regress/serial_schedule
@@ -109,6 +109,7 @@ test: window
test: xmlmap
test: functional_deps
test: advisory_lock
+test: json
test: plancache
test: limit
test: plpgsql
diff --git a/src/test/regress/sql/json.sql b/src/test/regress/sql/json.sql
new file mode 100644
index 0000000..8f85ff4
--- /dev/null
+++ b/src/test/regress/sql/json.sql
@@ -0,0 +1,102 @@
+-- Strings.
+SELECT '""'::json; -- OK.
+SELECT $$''$$::json; -- ERROR, single quotes are not allowed
+SELECT '"abc"'::json; -- OK
+SELECT '"abc'::json; -- ERROR, quotes not closed
+SELECT '"abc
+def"'::json; -- ERROR, unescaped newline in string constant
+SELECT '"\n\"\\"'::json; -- OK, legal escapes
+SELECT '"\v"'::json; -- ERROR, not a valid JSON escape
+SELECT '"\u"'::json; -- ERROR, incomplete escape
+SELECT '"\u00"'::json; -- ERROR, incomplete escape
+SELECT '"\u000g"'::json; -- ERROR, g is not a hex digit
+SELECT '"\u0000"'::json; -- OK, legal escape
+SELECT '"\uaBcD"'::json; -- OK, uppercase and lower case both OK
+
+-- Numbers.
+SELECT '1'::json; -- OK
+SELECT '0'::json; -- OK
+SELECT '01'::json; -- ERROR, not valid according to JSON spec
+SELECT '0.1'::json; -- OK
+SELECT '9223372036854775808'::json; -- OK, even though it's too large for int8
+SELECT '1e100'::json; -- OK
+SELECT '1.3e100'::json; -- OK
+SELECT '1f2'::json; -- ERROR
+
+-- Arrays.
+SELECT '[]'::json; -- OK
+SELECT '[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]'::json; -- OK
+SELECT '[1,2]'::json; -- OK
+SELECT '[1,2,]'::json; -- ERROR, trailing comma
+SELECT '[1,2'::json; -- ERROR, no closing bracket
+SELECT '[1,[2]'::json; -- ERROR, no closing bracket
+
+-- Objects.
+SELECT '{}'::json; -- OK
+SELECT '{"abc"}'::json; -- ERROR, no value
+SELECT '{"abc":1}'::json; -- OK
+SELECT '{1:"abc"}'::json; -- ERROR, keys must be strings
+SELECT '{"abc",1}'::json; -- ERROR, wrong separator
+SELECT '{"abc"=1}'::json; -- ERROR, totally wrong separator
+SELECT '{"abc"::1}'::json; -- ERROR, another wrong separator
+SELECT '{"abc":1,"def":2,"ghi":[3,4],"hij":{"klm":5,"nop":[6]}}'::json; -- OK
+SELECT '{"abc":1:2}'::json; -- ERROR, colon in wrong spot
+SELECT '{"abc":1,3}'::json; -- ERROR, no value
+
+-- Miscellaneous stuff.
+SELECT 'true'::json; -- OK
+SELECT 'false'::json; -- OK
+SELECT 'null'::json; -- OK
+SELECT ' true '::json; -- OK, even with extra whitespace
+SELECT 'true false'::json; -- ERROR, too many values
+SELECT 'true, false'::json; -- ERROR, too many values
+SELECT 'truf'::json; -- ERROR, not a keyword
+SELECT 'trues'::json; -- ERROR, not a keyword
+SELECT ''::json; -- ERROR, no value
+SELECT ' '::json; -- ERROR, no value
+
+-- query_to_json
+SELECT query_to_json('select 1 as a',false);
+SELECT query_to_json('select x as b, x * 2 as c from generate_series(1,3) x',false);
+SELECT query_to_json('select x as b, x * 2 as c from generate_series(1,3) x',true);
+SELECT query_to_json('
+ 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',true);
+SELECT query_to_json('select array_agg(x) as d from generate_series(5,10) x',false);
+
+-- array_to_json
+SELECT array_to_json(array_agg(x))
+FROM generate_series(1,10) x;
+
+
+SELECT array_to_json(array_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 array_to_json('{{1,5},{99,100}}'::int[]);
+
+-- row_to_json
+SELECT row_to_json(row(1,'foo'));
+
+SELECT row_to_json(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;
+
+CREATE TEMP TABLE rows AS
+SELECT x, 'txt' || x as y
+FROM generate_series(1,3) AS x;
+
+SELECT row_to_json(q)
+FROM rows q;
--
Sent via pgsql-hackers mailing list ([email protected])
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers