On Fri, Oct 4, 2024 at 7:33 AM jian he <jian.universal...@gmail.com> wrote: > > On Thu, Sep 26, 2024 at 11:45 PM Alexandra Wang > <alexandra.wang....@gmail.com> wrote: > > > > Hi, > > > > I didn’t run pgindent earlier, so here’s the updated version with the > > correct indentation. Hope this helps! > > > > the attached patch solves the domain type issue, Andrew mentioned in the > thread. > > I also added a test case: composite over jsonb domain type, > > > it still works. for example: > create domain json_d as jsonb; > create type test as (a int, b json_d); > create table t1(a test); > insert into t1 select $$(1,"{""a"": 3, ""key1"": {""c"": ""42""}, > ""key2"": [11, 12]}") $$; > insert into t1 select $$ (1,"{""a"": 3, ""key1"": {""c"": ""42""}, > ""key2"": [11, 12, {""x"": [31, 42]}]}") $$; > > select (t1.a).b.key2[2].x[1] from t1; > select (t1.a).b.key1.c from t1;
Thank you so much, Jian, for reviewing the patch and providing a fix! I’ve integrated your fix into the attached v5 patch. Inspired by your test case, I discovered another issue with domains over JSON: top-level JSON array access to a domain over JSON when the domain is a field of a composite type. Here’s an example: create domain json_d as json; create type test as (a int, b json_d); create table t1(a test); insert into t1 select $$ (1,"[{""a"": 3}, {""key1"": {""c"": ""42""}}, {""key2"": [11, 12]}]") $$; select (t1.a).b[0] from t1; The v5 patch includes the following updates: - Fixed the aforementioned issue and added more tests covering composite types with domains, nested domains, and arrays of domains over JSON/JSONB. - Refactored the logic for parsing JSON/JSONB object fields by moving it from ParseFuncOrColumn() to transformIndirection() for improved readability. The ParseFuncOrColumn() function is already handling both single-argument function calls and composite types, and it has other callers besides transformIndirection(). Best, Alex
From 44d71c9364fb8e9e8c57a5390ea5c4a3f7e371e0 Mon Sep 17 00:00:00 2001 From: Alexandra Wang <alexandra.wang.oss@gmail.com> Date: Thu, 7 Nov 2024 15:14:50 -0600 Subject: [PATCH v5] Add JSON/JSONB simplified accessor This patch implements JSON/JSONB member accessor and JSON/JSONB array accessor as specified in SQL 2023. Specifically, the following sytaxes are added: 1. Simple dot-notation access to JSON and JSONB object fields 2. Subscripting for indexed access to JSON array elements Examples: -- Setup create table t(x int, y json); insert into t select 1, '{"a": 1, "b": 42}'::json; insert into t select 1, '{"a": 2, "b": {"c": 42}}'::json; insert into t select 1, '{"a": 3, "b": {"c": "42"}, "d":[11, 12]}'::json; -- Existing syntax predates the SQL standard: select (t.y)->'b' from t; select (t.y)->'b'->'c' from t; select (t.y)->'d'->0 from t; -- JSON simplified accessor specified by the SQL standard: select (t.y).b from t; select (t.y).b.c from t; select (t.y).d[0] from t; The SQL standard states that simplified access is equivalent to: JSON_QUERY (VEP, 'lax $.JC' WITH CONDITIONAL ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR) where VEP is the <value expression primary> and JC is the <JSON simplified accessor op chain>. For example, the JSON_QUERY equalalence of the above queries is: select json_query(y, 'lax $.b' WITH CONDITIONAL ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR) from t; select json_query(y, 'lax $.b.c' WITH CONDITIONAL ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR) from t; select json_query(y, 'lax $.d[0]' WITH CONDITIONAL ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR) from t; This implementation enables dot-notation access to JSON/JSONB object by making a syntatic sugar for the json_object_field "->" operator in ParseFuncOrColumn() for arg of JSON/JSONB type. Similarly, JSON array access via subscripting is enabled by creating an OpExpr for the existing "->" operator. Note that the JSON subscripting implementation is different from the JSONB subscripting counterpart, as the former leverages the "->" operator directly, while the latter uses the more generic SubscriptingRef interface. Domains over JSON/JSONB types are also suppported. --- src/backend/parser/parse_expr.c | 37 ++++- src/backend/parser/parse_func.c | 85 +++++++++- src/include/catalog/pg_operator.dat | 6 +- src/include/parser/parse_func.h | 4 + src/test/regress/expected/json.out | 246 ++++++++++++++++++++++++++++ src/test/regress/expected/jsonb.out | 228 ++++++++++++++++++++++++++ src/test/regress/sql/json.sql | 86 ++++++++++ src/test/regress/sql/jsonb.sql | 83 ++++++++++ 8 files changed, 763 insertions(+), 12 deletions(-) diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c index c2806297aa..9a0ef59b4c 100644 --- a/src/backend/parser/parse_expr.c +++ b/src/backend/parser/parse_expr.c @@ -451,7 +451,16 @@ transformIndirection(ParseState *pstate, A_Indirection *ind) Node *n = lfirst(i); if (IsA(n, A_Indices)) - subscripts = lappend(subscripts, n); + { + if (exprType(result) == JSONOID || getBaseType(exprType(result)) == JSONOID) + result = ParseJsonSimplifiedAccessorArrayElement(pstate, + castNode(A_Indices, n), + result, + location); + else + subscripts = lappend(subscripts, n); + } + else if (IsA(n, A_Star)) { ereport(ERROR, @@ -462,6 +471,8 @@ transformIndirection(ParseState *pstate, A_Indirection *ind) else { Node *newresult; + Oid result_typid; + Oid result_basetypid; Assert(IsA(n, String)); @@ -475,13 +486,23 @@ transformIndirection(ParseState *pstate, A_Indirection *ind) false); subscripts = NIL; - newresult = ParseFuncOrColumn(pstate, - list_make1(n), - list_make1(result), - last_srf, - NULL, - false, - location); + result_typid = exprType(result); + result_basetypid = (result_typid == JSONOID || result_typid == JSONBOID) ? + result_typid : getBaseType(result_typid); + + if (result_basetypid == JSONOID || result_basetypid == JSONBOID) + newresult = ParseJsonSimplifiedAccessorObjectField(pstate, + strVal(n), + result, + location, result_basetypid); + else + newresult = ParseFuncOrColumn(pstate, + list_make1(n), + list_make1(result), + last_srf, + NULL, + false, + location); if (newresult == NULL) unknown_attribute(pstate, result, strVal(n), location); result = newresult; diff --git a/src/backend/parser/parse_func.c b/src/backend/parser/parse_func.c index 9b23344a3b..1f53736ce0 100644 --- a/src/backend/parser/parse_func.c +++ b/src/backend/parser/parse_func.c @@ -33,6 +33,8 @@ #include "utils/builtins.h" #include "utils/lsyscache.h" #include "utils/syscache.h" +#include "parser/parse_oper.h" +#include "catalog/pg_operator_d.h" /* Possible error codes from LookupFuncNameInternal */ @@ -53,7 +55,6 @@ static Oid LookupFuncNameInternal(ObjectType objtype, List *funcname, bool include_out_arguments, bool missing_ok, FuncLookupError *lookupError); - /* * Parse a function call * @@ -1902,6 +1903,88 @@ FuncNameAsType(List *funcname) return result; } +/* + * ParseJsonSimplifiedAccessorArrayElement - + * transform json subscript into json_array_element operator. + */ +Node * +ParseJsonSimplifiedAccessorArrayElement(ParseState *pstate, A_Indices *subscript, + Node *first_arg, int location) +{ + OpExpr *result; + Node *index; + + if (exprType(first_arg) != JSONOID && getBaseType(exprType(first_arg)) != JSONOID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("json subscript does not support type oid: %u", + exprType(first_arg))), + parser_errposition(pstate, location)); + + if (subscript->is_slice) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("json subscript does not support slices")), + parser_errposition(pstate, location)); + + index = transformExpr(pstate, subscript->uidx, pstate->p_expr_kind); + if (!IsA(index, Const) || + castNode(Const, index)->consttype != INT4OID) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("json subscript must be coercible to integer")), + parser_errposition(pstate, exprLocation(index))); + + result = makeNode(OpExpr); + result->opno = OID_JSON_ARRAY_ELEMENT_OP; + result->opresulttype = JSONOID; + result->opfuncid = get_opcode(result->opno); + result->args = list_make2(first_arg, index); + result->location = exprLocation(index); + + return (Node *) result; +} + +/* + * ParseJsonSimplifiedAccessorObjectField - + * handles function calls with a single argument that is of json type. + * If the function call is actually a column projection, return a suitably + * transformed expression tree. If not, return NULL. + */ +Node * +ParseJsonSimplifiedAccessorObjectField(ParseState *pstate, const char *funcname, + Node *first_arg, int location, Oid basetypid) +{ + OpExpr *result; + Node *rexpr; + + result = makeNode(OpExpr); + result->opresulttype = basetypid; + switch (basetypid) + { + case JSONOID: + result->opno = OID_JSON_OBJECT_FIELD_OP; + break; + case JSONBOID: + result->opno = OID_JSONB_OBJECT_FIELD_OP; + break; + default: + elog(ERROR, "unsupported type OID: %u", basetypid); + } + result->opfuncid = get_opcode(result->opno); + rexpr = (Node *) makeConst( + TEXTOID, + -1, + InvalidOid, + -1, + CStringGetTextDatum(funcname), + false, + false); + result->args = list_make2(first_arg, rexpr); + result->location = location; + return (Node *) result; +} + /* * ParseComplexProjection - * handles function calls with a single argument that is of complex type. diff --git a/src/include/catalog/pg_operator.dat b/src/include/catalog/pg_operator.dat index 0e7511dde1..e375c49252 100644 --- a/src/include/catalog/pg_operator.dat +++ b/src/include/catalog/pg_operator.dat @@ -3154,13 +3154,13 @@ oprname => '*', oprleft => 'anyrange', oprright => 'anyrange', oprresult => 'anyrange', oprcom => '*(anyrange,anyrange)', oprcode => 'range_intersect' }, -{ oid => '3962', descr => 'get json object field', +{ oid => '3962', oid_symbol => 'OID_JSON_OBJECT_FIELD_OP', descr => 'get json object field', oprname => '->', oprleft => 'json', oprright => 'text', oprresult => 'json', oprcode => 'json_object_field' }, { oid => '3963', descr => 'get json object field as text', oprname => '->>', oprleft => 'json', oprright => 'text', oprresult => 'text', oprcode => 'json_object_field_text' }, -{ oid => '3964', descr => 'get json array element', +{ oid => '3964', oid_symbol => 'OID_JSON_ARRAY_ELEMENT_OP', descr => 'get json array element', oprname => '->', oprleft => 'json', oprright => 'int4', oprresult => 'json', oprcode => 'json_array_element' }, { oid => '3965', descr => 'get json array element as text', @@ -3172,7 +3172,7 @@ { oid => '3967', descr => 'get value from json as text with path elements', oprname => '#>>', oprleft => 'json', oprright => '_text', oprresult => 'text', oprcode => 'json_extract_path_text' }, -{ oid => '3211', descr => 'get jsonb object field', +{ oid => '3211', oid_symbol => 'OID_JSONB_OBJECT_FIELD_OP', descr => 'get jsonb object field', oprname => '->', oprleft => 'jsonb', oprright => 'text', oprresult => 'jsonb', oprcode => 'jsonb_object_field' }, { oid => '3477', descr => 'get jsonb object field as text', diff --git a/src/include/parser/parse_func.h b/src/include/parser/parse_func.h index c7ba99dee7..64a600079c 100644 --- a/src/include/parser/parse_func.h +++ b/src/include/parser/parse_func.h @@ -34,6 +34,10 @@ typedef enum extern Node *ParseFuncOrColumn(ParseState *pstate, List *funcname, List *fargs, Node *last_srf, FuncCall *fn, bool proc_call, int location); +extern Node *ParseJsonSimplifiedAccessorObjectField(ParseState *pstate, const char *funcname, + Node *first_arg, int location, Oid basetypid); +extern Node *ParseJsonSimplifiedAccessorArrayElement(ParseState *pstate, A_Indices *subscript, + Node *first_arg, int location); extern FuncDetailCode func_get_detail(List *funcname, List *fargs, List *fargnames, diff --git a/src/test/regress/expected/json.out b/src/test/regress/expected/json.out index 96c40911cb..9da7dba068 100644 --- a/src/test/regress/expected/json.out +++ b/src/test/regress/expected/json.out @@ -2716,3 +2716,249 @@ select ts_headline('[]'::json, tsquery('aaa & bbb')); [] (1 row) +-- simple dot notation +drop table if exists test_json_dot; +NOTICE: table "test_json_dot" does not exist, skipping +create table test_json_dot(id int, test_json json); +insert into test_json_dot select 1, '{"a": 1, "b": 42}'::json; +insert into test_json_dot select 1, '{"a": 2, "b": {"c": 42}}'::json; +insert into test_json_dot select 1, '{"a": 3, "b": {"c": "42"}, "d":[11, 12]}'::json; +insert into test_json_dot select 1, '{"a": 3, "b": {"c": "42"}, "d":[{"x": [11, 12]}, {"y": [21, 22]}]}'::json; +-- member object access +select (test_json_dot.test_json).b, json_query(test_json, 'lax $.b' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; + b | expected +-------------+------------- + 42 | 42 + {"c": 42} | {"c": 42} + {"c": "42"} | {"c": "42"} + {"c": "42"} | {"c": "42"} +(4 rows) + +select (test_json_dot.test_json).b.c, json_query(test_json, 'lax $.b.c' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; + c | expected +------+---------- + | + 42 | 42 + "42" | "42" + "42" | "42" +(4 rows) + +select (test_json_dot.test_json).d, json_query(test_json, 'lax $.d' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; + d | expected +------------------------------------+------------------------------------ + | + | + [11, 12] | [11, 12] + [{"x": [11, 12]}, {"y": [21, 22]}] | [{"x": [11, 12]}, {"y": [21, 22]}] +(4 rows) + +select (test_json_dot.test_json)."d", json_query(test_json, 'lax $.d' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; + d | expected +------------------------------------+------------------------------------ + | + | + [11, 12] | [11, 12] + [{"x": [11, 12]}, {"y": [21, 22]}] | [{"x": [11, 12]}, {"y": [21, 22]}] +(4 rows) + +select (test_json_dot.test_json).'d' from test_json_dot; +ERROR: syntax error at or near "'d'" +LINE 1: select (test_json_dot.test_json).'d' from test_json_dot; + ^ +select (test_json_dot.test_json)['d'] from test_json_dot; +ERROR: json subscript must be coercible to integer +LINE 1: select (test_json_dot.test_json)['d'] from test_json_dot; + ^ +-- wildcard access is not supported +select (test_json_dot.test_json).* from test_json_dot; +ERROR: type json is not composite +-- array element access +select (test_json_dot.test_json).d->0 from test_json_dot; + ?column? +----------------- + + + 11 + {"x": [11, 12]} +(4 rows) + +select (test_json_dot.test_json).d[0], json_query(test_json, 'lax $.d[0]' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; + d | expected +-----------------+----------------- + | + | + 11 | 11 + {"x": [11, 12]} | {"x": [11, 12]} +(4 rows) + +select (test_json_dot.test_json).d[1], json_query(test_json, 'lax $.d[1]' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; + d | expected +-----------------+----------------- + | + | + 12 | 12 + {"y": [21, 22]} | {"y": [21, 22]} +(4 rows) + +select (test_json_dot.test_json).d[0:] from test_json_dot; +ERROR: json subscript does not support slices +LINE 1: select (test_json_dot.test_json).d[0:] from test_json_dot; + ^ +select (test_json_dot.test_json).d[0::int] from test_json_dot; + d +----------------- + + + 11 + {"x": [11, 12]} +(4 rows) + +select (test_json_dot.test_json).d[0::float] from test_json_dot; +ERROR: json subscript must be coercible to integer +LINE 1: select (test_json_dot.test_json).d[0::float] from test_json_... + ^ +select (test_json_dot.test_json).d[0].x[1], json_query(test_json, 'lax $.d[0].x[1]' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; + x | expected +----+---------- + | + | + | + 12 | 12 +(4 rows) + +-- complex type with domain over json +create domain json_d as json; +create type comp_json as (a int, b json_d); +create table test_json_domain_dot(a comp_json); +insert into test_json_domain_dot select $$ (1,"{""a"": 3, ""key1"": {""c"": ""42""}, ""key2"": [11, 12]}") $$; +insert into test_json_domain_dot select $$ (1,"{""a"": 3, ""key1"": {""c"": ""42""}, ""key2"": [11, 12, {""x"": [31, 42]}]}") $$; +--object access +select (test_json_domain_dot.a).b.key1.c from test_json_domain_dot; + c +------ + "42" + "42" +(2 rows) + +select (test_json_domain_dot.a).b.key2 from test_json_domain_dot; + key2 +--------------------------- + [11, 12] + [11, 12, {"x": [31, 42]}] +(2 rows) + +select (test_json_domain_dot.a).b.key2[0] from test_json_domain_dot; + key2 +------ + 11 + 11 +(2 rows) + +select (test_json_domain_dot.a).b.key2[0::text] from test_json_domain_dot; +ERROR: json subscript must be coercible to integer +LINE 1: select (test_json_domain_dot.a).b.key2[0::text] from test_js... + ^ +select (test_json_domain_dot.a).b.key2[2].x[1] from test_json_domain_dot; + x +---- + + 42 +(2 rows) + +-- array access +insert into test_json_domain_dot select $$ (1,"[{""a"": 3}, {""key1"": {""c"": ""42""}}, {""key2"": [11, 12]}]") $$; +select (test_json_domain_dot.a).b[0] from test_json_domain_dot; + b +---------- + + + {"a": 3} +(3 rows) + +select (test_json_domain_dot.a).b[0:] from test_json_domain_dot; +ERROR: json subscript does not support slices +LINE 1: select (test_json_domain_dot.a).b[0:] from test_json_domain_... + ^ +drop table test_json_domain_dot cascade; +drop type comp_json cascade; +drop domain json_d cascade; +-- nested domains over json +CREATE DOMAIN json_with_name AS json + CHECK ( + -- check that JSON has a "name" field and that it is a string + json_typeof(VALUE->'name') = 'string' + ); +CREATE DOMAIN json_with_name_and_email AS json_with_name + CHECK ( + -- ensure that if "email" exists, it follows a simple email format + VALUE->'email' IS NULL OR (VALUE->>'email' ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') + ); +CREATE DOMAIN json_user_profile AS json_with_name_and_email + CHECK ( + -- ensure that if "phone" exists, it follows a basic phone format + VALUE->'phone' IS NULL OR (VALUE->>'phone' ~ '^\+\d{1,3}-\d{3}-\d{3}-\d{4}$') + ); +CREATE TABLE json_users (id SERIAL PRIMARY KEY, profile json_user_profile); +INSERT INTO json_users (profile) VALUES ('{"name": "Alice", "email": "alice@example.com", "phone": "+1-123-456-7890"}'); +INSERT INTO json_users (profile) VALUES ('{"name": "Bob", "email": "bob@example.com", "phone": "+9-876-543-3210", "address": [123, "1st street", "New York", "New York", 12345]}'); +SELECT (json_users.profile).name from json_users; + name +--------- + "Alice" + "Bob" +(2 rows) + +SELECT (json_users.profile).email from json_users; + email +--------------------- + "alice@example.com" + "bob@example.com" +(2 rows) + +SELECT (json_users.profile).phone from json_users; + phone +------------------- + "+1-123-456-7890" + "+9-876-543-3210" +(2 rows) + +SELECT (json_users.profile).address from json_users; + address +---------------------------------------------------- + + [123, "1st street", "New York", "New York", 12345] +(2 rows) + +SELECT (json_users.profile).address[3] from json_users; + address +------------ + + "New York" +(2 rows) + +-- array of nested domains over json +CREATE TABLE json_user_arrs (id SERIAL PRIMARY KEY, profiles json_user_profile[]); +INSERT INTO json_user_arrs (profiles) VALUES (ARRAY['{"name": "Alice", "email": "alice@example.com", "phone": "+1-123-456-7890"}'::json_user_profile, '{"name": "Bob", "email": "bob@example.com", "phone": "+9-876-543-3210", "address": [123, "1st street", "New York", "New York", 12345]}'::json_user_profile]); +SELECT json_user_arrs.profiles[1] from json_user_arrs; + profiles +----------------------------------------------------------------------------- + {"name": "Alice", "email": "alice@example.com", "phone": "+1-123-456-7890"} +(1 row) + +SELECT json_user_arrs.profiles[2] from json_user_arrs; + profiles +---------------------------------------------------------------------------------------------------------------------------------------- + {"name": "Bob", "email": "bob@example.com", "phone": "+9-876-543-3210", "address": [123, "1st street", "New York", "New York", 12345]} +(1 row) + +SELECT json_user_arrs.profiles[2].address[0] from json_user_arrs; + address +--------- + 123 +(1 row) + +drop table json_users; +drop table json_user_arrs; +drop domain json_user_profile; +drop domain json_with_name_and_email; +drop domain json_with_name; diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index 7d163a156e..357b2f4356 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -5715,3 +5715,231 @@ select '12345.0000000000000000000000000000000000000000000005'::jsonb::int8; 12345 (1 row) +-- simple dot notation +drop table if exists test_jsonb_dot; +NOTICE: table "test_jsonb_dot" does not exist, skipping +create table test_jsonb_dot(id int, test_jsonb jsonb); +insert into test_jsonb_dot select 1, '{"a": 1, "b": 42}'::json; +insert into test_jsonb_dot select 1, '{"a": 2, "b": {"c": 42}}'::json; +insert into test_jsonb_dot select 1, '{"a": 3, "b": {"c": "42"}, "d":[11, 12]}'::json; +-- member object access +select (test_jsonb_dot.test_jsonb).b from test_jsonb_dot; + b +------------- + 42 + {"c": 42} + {"c": "42"} +(3 rows) + +select (test_jsonb_dot.test_jsonb).b.c from test_jsonb_dot; + c +------ + + 42 + "42" +(3 rows) + +select (test_jsonb_dot.test_jsonb).d from test_jsonb_dot; + d +---------- + + + [11, 12] +(3 rows) + +select (test_jsonb_dot.test_jsonb)."d" from test_jsonb_dot; + d +---------- + + + [11, 12] +(3 rows) + +select (test_jsonb_dot.test_jsonb).'d' from test_jsonb_dot; +ERROR: syntax error at or near "'d'" +LINE 1: select (test_jsonb_dot.test_jsonb).'d' from test_jsonb_dot; + ^ +select (test_jsonb_dot.test_jsonb)['d'] from test_jsonb_dot; + test_jsonb +------------ + + + [11, 12] +(3 rows) + +-- array element access +select (test_jsonb_dot.test_jsonb).d[0] from test_jsonb_dot; + d +---- + + + 11 +(3 rows) + +select (test_jsonb_dot.test_jsonb).d[0:] from test_jsonb_dot; +ERROR: jsonb subscript does not support slices +LINE 1: select (test_jsonb_dot.test_jsonb).d[0:] from test_jsonb_dot... + ^ +select (test_jsonb_dot.test_jsonb).d[0::int] from test_jsonb_dot; + d +---- + + + 11 +(3 rows) + +select (test_jsonb_dot.test_jsonb).d[0::float] from test_jsonb_dot; +ERROR: subscript type double precision is not supported +LINE 1: select (test_jsonb_dot.test_jsonb).d[0::float] from test_jso... + ^ +HINT: jsonb subscript must be coercible to either integer or text. +select (test_jsonb_dot.test_jsonb).d[0].x[1] from test_jsonb_dot; + x +--- + + + +(3 rows) + +-- wildcard access is not supported +select (test_jsonb_dot.test_jsonb).* from test_jsonb_dot; +ERROR: type jsonb is not composite +-- complex type with domain over jsonb +create domain jsonb_d as jsonb; +create type comp_jsonb as (a int, b jsonb_d); +create table test_jsonb_domain_dot(a comp_jsonb); +insert into test_jsonb_domain_dot select $$ (1,"{""a"": 3, ""key1"": {""c"": ""42""}, ""key2"": [11, 12]}") $$; +insert into test_jsonb_domain_dot select $$ (1,"{""a"": 3, ""key1"": {""c"": ""42""}, ""key2"": [11, 12, {""x"": [31, 42]}]}") $$; +-- object access +select (test_jsonb_domain_dot.a).b.key1.c from test_jsonb_domain_dot; + c +------ + "42" + "42" +(2 rows) + +select (test_jsonb_domain_dot.a).b.key2 from test_jsonb_domain_dot; + key2 +--------------------------- + [11, 12] + [11, 12, {"x": [31, 42]}] +(2 rows) + +select (test_jsonb_domain_dot.a).b.key2[0] from test_jsonb_domain_dot; + key2 +------ + 11 + 11 +(2 rows) + +select (test_jsonb_domain_dot.a).b.key2[0::text] from test_jsonb_domain_dot; + key2 +------ + 11 + 11 +(2 rows) + +select (test_jsonb_domain_dot.a).b.key2[2].x[1] from test_jsonb_domain_dot; + x +---- + + 42 +(2 rows) + +-- array access +insert into test_jsonb_domain_dot select $$ (1,"[{""a"": 3}, {""key1"": {""c"": ""42""}}, {""key2"": [11, 12]}]") $$; +select (test_jsonb_domain_dot.a).b[0] from test_jsonb_domain_dot; + b +---------- + + + {"a": 3} +(3 rows) + +select (test_jsonb_domain_dot.a).b[0:] from test_jsonb_domain_dot; +ERROR: jsonb subscript does not support slices +LINE 1: select (test_jsonb_domain_dot.a).b[0:] from test_jsonb_domai... + ^ +drop table test_jsonb_domain_dot cascade; +drop type comp_jsonb cascade; +drop domain jsonb_d cascade; +-- nested domains over jsonb +CREATE DOMAIN jsonb_with_name AS JSONB + CHECK ( + -- check that JSON has a "name" field and that it is a string + VALUE ? 'name' AND jsonb_typeof(VALUE->'name') = 'string' + ); +CREATE DOMAIN jsonb_with_name_and_email AS jsonb_with_name + CHECK ( + -- ensure that if "email" exists, it follows a simple email format + NOT VALUE ? 'email' OR (VALUE->>'email' ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') + ); +CREATE DOMAIN jsonb_user_profile AS jsonb_with_name_and_email + CHECK ( + -- ensure that if "phone" exists, it follows a basic phone format + NOT VALUE ? 'phone' OR (VALUE->>'phone' ~ '^\+\d{1,3}-\d{3}-\d{3}-\d{4}$') + ); +CREATE TABLE jsonb_users (id SERIAL PRIMARY KEY, profile jsonb_user_profile); +INSERT INTO jsonb_users (profile) VALUES ('{"name": "Alice", "email": "alice@example.com", "phone": "+1-123-456-7890"}'); +INSERT INTO jsonb_users (profile) VALUES ('{"name": "Bob", "email": "bob@example.com", "phone": "+9-876-543-3210", "address": [123, "1st street", "New York", "New York", 12345]}'); +SELECT (jsonb_users.profile).name from jsonb_users; + name +--------- + "Alice" + "Bob" +(2 rows) + +SELECT (jsonb_users.profile).email from jsonb_users; + email +--------------------- + "alice@example.com" + "bob@example.com" +(2 rows) + +SELECT (jsonb_users.profile).phone from jsonb_users; + phone +------------------- + "+1-123-456-7890" + "+9-876-543-3210" +(2 rows) + +SELECT (jsonb_users.profile).address from jsonb_users; + address +---------------------------------------------------- + + [123, "1st street", "New York", "New York", 12345] +(2 rows) + +SELECT (jsonb_users.profile).address[3] from jsonb_users; + address +------------ + + "New York" +(2 rows) + +-- array of nested domains over jsonb +CREATE TABLE jsonb_user_arrs (id SERIAL PRIMARY KEY, profiles jsonb_user_profile[]); +INSERT INTO jsonb_user_arrs (profiles) VALUES (ARRAY['{"name": "Alice", "email": "alice@example.com", "phone": "+1-123-456-7890"}'::jsonb_user_profile, '{"name": "Bob", "email": "bob@example.com", "phone": "+9-876-543-3210", "address": [123, "1st street", "New York", "New York", 12345]}'::jsonb_user_profile]); +SELECT jsonb_user_arrs.profiles[1] from jsonb_user_arrs; + profiles +----------------------------------------------------------------------------- + {"name": "Alice", "email": "alice@example.com", "phone": "+1-123-456-7890"} +(1 row) + +SELECT jsonb_user_arrs.profiles[2] from jsonb_user_arrs; + profiles +---------------------------------------------------------------------------------------------------------------------------------------- + {"name": "Bob", "email": "bob@example.com", "phone": "+9-876-543-3210", "address": [123, "1st street", "New York", "New York", 12345]} +(1 row) + +SELECT jsonb_user_arrs.profiles[2].address[0] from jsonb_user_arrs; + address +--------- + 123 +(1 row) + +drop table jsonb_users; +drop table jsonb_user_arrs; +drop domain jsonb_user_profile; +drop domain jsonb_with_name_and_email; +drop domain jsonb_with_name; diff --git a/src/test/regress/sql/json.sql b/src/test/regress/sql/json.sql index 8251f4f400..25fb1259ab 100644 --- a/src/test/regress/sql/json.sql +++ b/src/test/regress/sql/json.sql @@ -869,3 +869,89 @@ select ts_headline('english', '{"a": "aaa bbb", "b": {"c": "ccc ddd fff", "c1": select ts_headline('null'::json, tsquery('aaa & bbb')); select ts_headline('{}'::json, tsquery('aaa & bbb')); select ts_headline('[]'::json, tsquery('aaa & bbb')); + +-- simple dot notation +drop table if exists test_json_dot; +create table test_json_dot(id int, test_json json); +insert into test_json_dot select 1, '{"a": 1, "b": 42}'::json; +insert into test_json_dot select 1, '{"a": 2, "b": {"c": 42}}'::json; +insert into test_json_dot select 1, '{"a": 3, "b": {"c": "42"}, "d":[11, 12]}'::json; +insert into test_json_dot select 1, '{"a": 3, "b": {"c": "42"}, "d":[{"x": [11, 12]}, {"y": [21, 22]}]}'::json; + +-- member object access +select (test_json_dot.test_json).b, json_query(test_json, 'lax $.b' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; +select (test_json_dot.test_json).b.c, json_query(test_json, 'lax $.b.c' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; +select (test_json_dot.test_json).d, json_query(test_json, 'lax $.d' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; +select (test_json_dot.test_json)."d", json_query(test_json, 'lax $.d' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; +select (test_json_dot.test_json).'d' from test_json_dot; +select (test_json_dot.test_json)['d'] from test_json_dot; + +-- wildcard access is not supported +select (test_json_dot.test_json).* from test_json_dot; + +-- array element access +select (test_json_dot.test_json).d->0 from test_json_dot; +select (test_json_dot.test_json).d[0], json_query(test_json, 'lax $.d[0]' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; +select (test_json_dot.test_json).d[1], json_query(test_json, 'lax $.d[1]' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; +select (test_json_dot.test_json).d[0:] from test_json_dot; +select (test_json_dot.test_json).d[0::int] from test_json_dot; +select (test_json_dot.test_json).d[0::float] from test_json_dot; +select (test_json_dot.test_json).d[0].x[1], json_query(test_json, 'lax $.d[0].x[1]' WITH CONDITIONAL WRAPPER NULL ON EMPTY NULL ON ERROR) as expected from test_json_dot; + +-- complex type with domain over json +create domain json_d as json; +create type comp_json as (a int, b json_d); +create table test_json_domain_dot(a comp_json); +insert into test_json_domain_dot select $$ (1,"{""a"": 3, ""key1"": {""c"": ""42""}, ""key2"": [11, 12]}") $$; +insert into test_json_domain_dot select $$ (1,"{""a"": 3, ""key1"": {""c"": ""42""}, ""key2"": [11, 12, {""x"": [31, 42]}]}") $$; +--object access +select (test_json_domain_dot.a).b.key1.c from test_json_domain_dot; +select (test_json_domain_dot.a).b.key2 from test_json_domain_dot; +select (test_json_domain_dot.a).b.key2[0] from test_json_domain_dot; +select (test_json_domain_dot.a).b.key2[0::text] from test_json_domain_dot; +select (test_json_domain_dot.a).b.key2[2].x[1] from test_json_domain_dot; +-- array access +insert into test_json_domain_dot select $$ (1,"[{""a"": 3}, {""key1"": {""c"": ""42""}}, {""key2"": [11, 12]}]") $$; +select (test_json_domain_dot.a).b[0] from test_json_domain_dot; +select (test_json_domain_dot.a).b[0:] from test_json_domain_dot; +drop table test_json_domain_dot cascade; +drop type comp_json cascade; +drop domain json_d cascade; + +-- nested domains over json +CREATE DOMAIN json_with_name AS json + CHECK ( + -- check that JSON has a "name" field and that it is a string + json_typeof(VALUE->'name') = 'string' + ); +CREATE DOMAIN json_with_name_and_email AS json_with_name + CHECK ( + -- ensure that if "email" exists, it follows a simple email format + VALUE->'email' IS NULL OR (VALUE->>'email' ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') + ); +CREATE DOMAIN json_user_profile AS json_with_name_and_email + CHECK ( + -- ensure that if "phone" exists, it follows a basic phone format + VALUE->'phone' IS NULL OR (VALUE->>'phone' ~ '^\+\d{1,3}-\d{3}-\d{3}-\d{4}$') + ); +CREATE TABLE json_users (id SERIAL PRIMARY KEY, profile json_user_profile); +INSERT INTO json_users (profile) VALUES ('{"name": "Alice", "email": "alice@example.com", "phone": "+1-123-456-7890"}'); +INSERT INTO json_users (profile) VALUES ('{"name": "Bob", "email": "bob@example.com", "phone": "+9-876-543-3210", "address": [123, "1st street", "New York", "New York", 12345]}'); +SELECT (json_users.profile).name from json_users; +SELECT (json_users.profile).email from json_users; +SELECT (json_users.profile).phone from json_users; +SELECT (json_users.profile).address from json_users; +SELECT (json_users.profile).address[3] from json_users; + +-- array of nested domains over json +CREATE TABLE json_user_arrs (id SERIAL PRIMARY KEY, profiles json_user_profile[]); +INSERT INTO json_user_arrs (profiles) VALUES (ARRAY['{"name": "Alice", "email": "alice@example.com", "phone": "+1-123-456-7890"}'::json_user_profile, '{"name": "Bob", "email": "bob@example.com", "phone": "+9-876-543-3210", "address": [123, "1st street", "New York", "New York", 12345]}'::json_user_profile]); +SELECT json_user_arrs.profiles[1] from json_user_arrs; +SELECT json_user_arrs.profiles[2] from json_user_arrs; +SELECT json_user_arrs.profiles[2].address[0] from json_user_arrs; + +drop table json_users; +drop table json_user_arrs; +drop domain json_user_profile; +drop domain json_with_name_and_email; +drop domain json_with_name; diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 5f0190d5a2..b6096021bc 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1559,3 +1559,86 @@ select '12345.0000000000000000000000000000000000000000000005'::jsonb::float8; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int2; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int4; select '12345.0000000000000000000000000000000000000000000005'::jsonb::int8; + +-- simple dot notation +drop table if exists test_jsonb_dot; +create table test_jsonb_dot(id int, test_jsonb jsonb); +insert into test_jsonb_dot select 1, '{"a": 1, "b": 42}'::json; +insert into test_jsonb_dot select 1, '{"a": 2, "b": {"c": 42}}'::json; +insert into test_jsonb_dot select 1, '{"a": 3, "b": {"c": "42"}, "d":[11, 12]}'::json; + +-- member object access +select (test_jsonb_dot.test_jsonb).b from test_jsonb_dot; +select (test_jsonb_dot.test_jsonb).b.c from test_jsonb_dot; +select (test_jsonb_dot.test_jsonb).d from test_jsonb_dot; +select (test_jsonb_dot.test_jsonb)."d" from test_jsonb_dot; +select (test_jsonb_dot.test_jsonb).'d' from test_jsonb_dot; +select (test_jsonb_dot.test_jsonb)['d'] from test_jsonb_dot; + +-- array element access +select (test_jsonb_dot.test_jsonb).d[0] from test_jsonb_dot; +select (test_jsonb_dot.test_jsonb).d[0:] from test_jsonb_dot; +select (test_jsonb_dot.test_jsonb).d[0::int] from test_jsonb_dot; +select (test_jsonb_dot.test_jsonb).d[0::float] from test_jsonb_dot; +select (test_jsonb_dot.test_jsonb).d[0].x[1] from test_jsonb_dot; + +-- wildcard access is not supported +select (test_jsonb_dot.test_jsonb).* from test_jsonb_dot; + +-- complex type with domain over jsonb +create domain jsonb_d as jsonb; +create type comp_jsonb as (a int, b jsonb_d); +create table test_jsonb_domain_dot(a comp_jsonb); +insert into test_jsonb_domain_dot select $$ (1,"{""a"": 3, ""key1"": {""c"": ""42""}, ""key2"": [11, 12]}") $$; +insert into test_jsonb_domain_dot select $$ (1,"{""a"": 3, ""key1"": {""c"": ""42""}, ""key2"": [11, 12, {""x"": [31, 42]}]}") $$; +-- object access +select (test_jsonb_domain_dot.a).b.key1.c from test_jsonb_domain_dot; +select (test_jsonb_domain_dot.a).b.key2 from test_jsonb_domain_dot; +select (test_jsonb_domain_dot.a).b.key2[0] from test_jsonb_domain_dot; +select (test_jsonb_domain_dot.a).b.key2[0::text] from test_jsonb_domain_dot; +select (test_jsonb_domain_dot.a).b.key2[2].x[1] from test_jsonb_domain_dot; +-- array access +insert into test_jsonb_domain_dot select $$ (1,"[{""a"": 3}, {""key1"": {""c"": ""42""}}, {""key2"": [11, 12]}]") $$; +select (test_jsonb_domain_dot.a).b[0] from test_jsonb_domain_dot; +select (test_jsonb_domain_dot.a).b[0:] from test_jsonb_domain_dot; +drop table test_jsonb_domain_dot cascade; +drop type comp_jsonb cascade; +drop domain jsonb_d cascade; + +-- nested domains over jsonb +CREATE DOMAIN jsonb_with_name AS JSONB + CHECK ( + -- check that JSON has a "name" field and that it is a string + VALUE ? 'name' AND jsonb_typeof(VALUE->'name') = 'string' + ); +CREATE DOMAIN jsonb_with_name_and_email AS jsonb_with_name + CHECK ( + -- ensure that if "email" exists, it follows a simple email format + NOT VALUE ? 'email' OR (VALUE->>'email' ~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$') + ); +CREATE DOMAIN jsonb_user_profile AS jsonb_with_name_and_email + CHECK ( + -- ensure that if "phone" exists, it follows a basic phone format + NOT VALUE ? 'phone' OR (VALUE->>'phone' ~ '^\+\d{1,3}-\d{3}-\d{3}-\d{4}$') + ); +CREATE TABLE jsonb_users (id SERIAL PRIMARY KEY, profile jsonb_user_profile); +INSERT INTO jsonb_users (profile) VALUES ('{"name": "Alice", "email": "alice@example.com", "phone": "+1-123-456-7890"}'); +INSERT INTO jsonb_users (profile) VALUES ('{"name": "Bob", "email": "bob@example.com", "phone": "+9-876-543-3210", "address": [123, "1st street", "New York", "New York", 12345]}'); +SELECT (jsonb_users.profile).name from jsonb_users; +SELECT (jsonb_users.profile).email from jsonb_users; +SELECT (jsonb_users.profile).phone from jsonb_users; +SELECT (jsonb_users.profile).address from jsonb_users; +SELECT (jsonb_users.profile).address[3] from jsonb_users; + +-- array of nested domains over jsonb +CREATE TABLE jsonb_user_arrs (id SERIAL PRIMARY KEY, profiles jsonb_user_profile[]); +INSERT INTO jsonb_user_arrs (profiles) VALUES (ARRAY['{"name": "Alice", "email": "alice@example.com", "phone": "+1-123-456-7890"}'::jsonb_user_profile, '{"name": "Bob", "email": "bob@example.com", "phone": "+9-876-543-3210", "address": [123, "1st street", "New York", "New York", 12345]}'::jsonb_user_profile]); +SELECT jsonb_user_arrs.profiles[1] from jsonb_user_arrs; +SELECT jsonb_user_arrs.profiles[2] from jsonb_user_arrs; +SELECT jsonb_user_arrs.profiles[2].address[0] from jsonb_user_arrs; + +drop table jsonb_users; +drop table jsonb_user_arrs; +drop domain jsonb_user_profile; +drop domain jsonb_with_name_and_email; +drop domain jsonb_with_name; -- 2.39.5 (Apple Git-154)