diff --git a/doc/src/sgml/datatype.sgml b/doc/src/sgml/datatype.sgml index 152ef2f037..58e46b5870 100644 --- a/doc/src/sgml/datatype.sgml +++ b/doc/src/sgml/datatype.sgml @@ -269,6 +269,12 @@ XML data + + + json + + JSON data + @@ -4169,6 +4175,32 @@ SET xmloption TO { DOCUMENT | CONTENT }; + + <acronym>JSON</> Type + + + JSON + + + + The json data type can be used to store JSON data. Such + data can also be stored as text, but the + json data type has the advantage of checking that each + stored value is a valid JSON value. + + + + PostgreSQL allows only one server encoding + per database. It is therefore not possible for JSON to conform rigidly + to the specification unless the server encoding is UTF-8. Attempts to + directly include characters which cannot be represented in the server + encoding will fail; conversely, characters which can be represented in + the server encoding but not in UTF-8 will be allowed. + \uXXXX escapes are allowed regardless of the server + encoding, and are checked only for syntactic correctness. + + + &array; &rowtypes; diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index e297e9cfb9..e35ac59030 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -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; } diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile index 5f968b011f..c635c38f5b 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 0000000000..cbb81d1bf3 --- /dev/null +++ b/src/backend/utils/adt/json.c @@ -0,0 +1,665 @@ +/*------------------------------------------------------------------------- + * + * 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 "lib/stringinfo.h" +#include "libpq/pqformat.h" +#include "mb/pg_wchar.h" +#include "utils/builtins.h" +#include "utils/json.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; + +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); + +extern Datum json_in(PG_FUNCTION_ARGS); + +/* + * 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))); + } + } + + /* 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; +} diff --git a/src/include/catalog/pg_proc.h b/src/include/catalog/pg_proc.h index 96551ccae9..5dc6d05478 100644 --- a/src/include/catalog/pg_proc.h +++ b/src/include/catalog/pg_proc.h @@ -4022,6 +4022,16 @@ 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"); + /* 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 d9855a79f8..e4bca55297 100644 --- a/src/include/catalog/pg_type.h +++ b/src/include/catalog/pg_type.h @@ -352,10 +352,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 0000000000..ee87fd61d7 --- /dev/null +++ b/src/include/utils/json.h @@ -0,0 +1,24 @@ +/*------------------------------------------------------------------------- + * + * 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); + +#endif /* XML_H */ diff --git a/src/test/regress/expected/json.out b/src/test/regress/expected/json.out new file mode 100644 index 0000000000..5ef65f7c97 --- /dev/null +++ b/src/test/regress/expected/json.out @@ -0,0 +1,258 @@ +-- 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. diff --git a/src/test/regress/parallel_schedule b/src/test/regress/parallel_schedule index 3bedad07c7..d6105f8567 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 0b64569849..90726ce97b 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 0000000000..440398b9b9 --- /dev/null +++ b/src/test/regress/sql/json.sql @@ -0,0 +1,56 @@ +-- 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