From be9683f89f8fb484112f738b77fbe72bfe625a64 Mon Sep 17 00:00:00 2001 From: Alexandra Wang Date: Tue, 8 Jul 2025 22:18:07 -0700 Subject: [PATCH v22 5/5] Implement read-only dot notation for jsonb This patch introduces JSONB member access using dot notation that aligns with the JSON simplified accessor specified in SQL:2023. Examples: -- Setup create table t(x int, y jsonb); insert into t select 1, '{"a": 1, "b": 42}'::jsonb; insert into t select 1, '{"a": 2, "b": {"c": 42}}'::jsonb; insert into t select 1, '{"a": 3, "b": {"c": "42"}, "d":[11, 12]}'::jsonb; -- Existing syntax in PostgreSQL that 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 = JC = For example, the JSON_QUERY equivalents of the above queries are: 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; Implementation details: This patch extends the existing container subscripting interface to support container-specific information, namely a JSONPath expression for jsonb. During query transformation, if dot-notation is present, a JSONPath expression is constructed to represent the access chain. Then during execution, if a JSONPath expression is present in JsonbSubWorkspace, executes it via JsonPathQuery(). Note that we cannot simply rewrite the accessors into JSON_QUERY() during transformation, because the original query structure must be preserved for EXPLAIN and CREATE VIEW. Co-authored-by: Nikita Glukhov Co-authored-by: Alexandra Wang Reviewed-by: Andrew Dunstan Reviewed-by: Matheus Alcantara Reviewed-by: Mark Dilger Reviewed-by: Jian He Reviewed-by: Vik Fearing Reviewed-by: Nikita Malakhov Reviewed-by: Peter Eisentraut Reviewed-by: Chao Li Tested-by: Jelte Fennema-Nio --- doc/src/sgml/json.sgml | 301 ++++++++++++ src/backend/catalog/sql_features.txt | 4 +- src/backend/executor/execExpr.c | 81 ++-- src/backend/nodes/nodeFuncs.c | 14 + src/backend/utils/adt/jsonbsubs.c | 302 +++++++++++- src/backend/utils/adt/ruleutils.c | 43 +- src/include/nodes/primnodes.h | 54 ++- .../ecpg/test/expected/sql-sqljson.c | 112 ++++- .../ecpg/test/expected/sql-sqljson.stderr | 100 ++++ .../ecpg/test/expected/sql-sqljson.stdout | 7 + src/interfaces/ecpg/test/sql/sqljson.pgc | 33 ++ src/test/regress/expected/jsonb.out | 450 +++++++++++++++++- src/test/regress/sql/jsonb.sql | 115 +++++ src/tools/pgindent/typedefs.list | 1 + 14 files changed, 1535 insertions(+), 82 deletions(-) diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 206eadb8f7b..4405570d66e 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -712,6 +712,307 @@ UPDATE table_name SET jsonb_field[1]['a'] = '1'; + + JSON Simplified Accessor + + PostgreSQL implements the JSON simplified accessor as specified in SQL:2023. + The SQL standard defines the simplified accessor as a chain of operations + that can include JSON member accessors (dot notation for object fields) + and JSON array accessors (integer subscripts for array elements). + This provides a standardized way to access JSON data that complements + PostgreSQL's pre-standard subscripting and operator-based access methods. + + + + The SQL:2023 simplified accessor syntax includes: + + + + JSON member accessor: jsonb_column.field_name + for accessing object fields + + + + + JSON array accessor: jsonb_column[index] + for accessing array elements by integer index + + + + These can be chained together: jsonb_column.field_name[0].nested_field. + When dot notation is present in the accessor chain, the entire chain follows + SQL:2023 semantics with lax mode behavior and conditional array wrapper. + + + + The JSON simplified accessor is semantically equivalent to using + JSON_QUERY with the lax mode and + WITH CONDITIONAL ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR + options. For example, json_col.field is equivalent to + JSON_QUERY(json_col, 'lax $.field' WITH CONDITIONAL ARRAY WRAPPER NULL ON EMPTY NULL ON ERROR). + The conditional array wrapper wraps multiple results in an array + but leaves a single result as-is without wrapping. + + + + Examples of JSON simplified accessor syntax: + + + +-- Basic field access +SELECT ('{"color": "red", "rgb": [255, 0, 0]}'::jsonb).color; + +-- Nested field access +SELECT ('{"user": {"profile": {"settings": {"theme": "dark"}}}}'::jsonb).user.profile.settings.theme; + +-- JSON member accessor followed by JSON array accessor (both part of simplified accessor) +SELECT ('{"repertoire": [{"title": "Swan Lake"}, {"title": "The Nutcracker"}]}'::jsonb).repertoire[1].title; + +-- In WHERE clauses +SELECT * FROM users WHERE profile.preferences.theme = '"dark"'; + +-- Comparison with other access methods (NOT equivalent - different semantics): +SELECT json_col['address']['city']; -- Subscripting +SELECT json_col->'address'->'city'; -- Operator +SELECT json_col.address.city; -- Simplified accessor (different behavior) + + + + + Comparison of JSON Access Methods + + PostgreSQL provides three different approaches for accessing JSON data, each with + distinct semantics and behaviors: + + + + SQL:2023 JSON Simplified Accessor: + + + + Syntax: json_col.field_name (member accessor) and + json_col[index] (array accessor when used with dot notation) + + + + + Standard SQL:2023 behavior with lax mode semantics + + + + + Automatic array wrapping/unwrapping as specified in the SQL standard + + + + + When accessing a field from an array, operates on each array element and wraps results in an array; + when accessing an array index from a non-array, wraps the value as an array first + + + + + Triggered when dot notation is present anywhere in the accessor chain + + + + + Read-only access + + + + + + + Pre-standard JSONB Subscripting: + + + + Syntax: json_col['field_name'] (text-based) and + json_col[index] (integer-based when no dot notation present) + + + + + PostgreSQL's original JSONB subscripting behavior (available since version 14) + + + + + Direct object field and array element access without array wrapping/unwrapping + + + + + Supports both read and write operations + + + + + + + Arrow Operators: + + + + Syntax: json_col->'field_name' and json_col->>'field_name' + + + + + PostgreSQL's JSON operators that work with both json and jsonb types + + + + + Direct object field and array element access without array wrapping/unwrapping + + + + + -> returns jsonb, ->> returns text + + + + + Read-only access + + + + + + + Key Semantic Differences: The most important distinctions are + how these methods handle member access from arrays and array access from non-array values. + + + + Member Access from Arrays: + +-- Setup data +INSERT INTO test_table VALUES + ('{"brightness": 80}'), -- Object case + ('[{"brightness": 45}, {"brightness": 90}]'); -- Array case + +-- Different behaviors: +SELECT data.brightness FROM test_table; -- Simplified accessor +-- Results: 80, [45, 90] (array elements unwrapped, results wrapped) + +SELECT data['brightness'] FROM test_table; -- Pre-standard subscripting +-- Results: 80, NULL (no array handling) + +SELECT data->'brightness' FROM test_table; -- Arrow operator +-- Results: 80, NULL (no array handling) + + + In the array case, the simplified accessor applies the field access to each array element + (unwrapping) and conditionally wraps the results in an array, while subscripting and arrow operators + attempt direct field access on the array itself (which returns NULL since + arrays don't have named fields). + + + + Array Access from Objects (Lax Mode Behavior): + +-- Setup data +INSERT INTO test_table VALUES ('{"weather": "sunny", "temperature": "72F"}'); + +-- Different behaviors when accessing [0] on a non-array value: +SELECT data[0] FROM test_table; -- Simplified accessor (lax mode, if dots present elsewhere) +-- Result: {"weather": "sunny", "temperature": "72F"} (object wrapped as array, [0] returns entire object) + +SELECT data[0] FROM test_table; -- Pre-standard subscripting (strict mode, no dots) +-- Result: NULL (no wrapping, direct array access on object fails) + +SELECT data->0 FROM test_table; -- Arrow operator (strict mode) +-- Result: NULL (no wrapping, direct array access on object fails) + + + In lax mode (simplified accessor), when an array operation is performed on a non-array value, + the value is first wrapped in an array, then the operation proceeds. In strict mode + (pre-standard methods), the operation fails and returns NULL. + + + + When to Use Each Method: + + + + Use SQL:2023 simplified accessor for standard compliance and when you want lax mode and conditional array wrapper + + + + + Use pre-standard subscripting for write operations or when you need direct field access without array processing + + + + + Use arrow operators when you need text output (->>) or when working with both json and jsonb types + + + + + + + + Best Practices: Avoid Mixing Access Methods + + Important: Do not mix SQL:2023 simplified accessor syntax + with pre-standard subscripting syntax in the same accessor chain. These + methods have subtly different semantics and are not interchangeable aliases. + Mixing them can lead to confusion and code that is difficult to understand. + + + + Recommended - Consistent simplified accessor: + +-- All parts use simplified accessor (standard behavior) +SELECT data.location.coordinates.latitude FROM table; -- Good +SELECT data.repertoire[0].title FROM table; -- Good +SELECT data.users[1].profile.email FROM table; -- Good + + + + + Recommended - Consistent pre-standard subscripting: + +-- All parts use pre-standard subscripting +SELECT data['location']['coordinates']['latitude'] FROM table; -- Good +SELECT data[0]['title'] FROM table; -- Good (when no dots present) +SELECT data['users'][1]['profile']['email'] FROM table; -- Good + + + + + Not recommended - Mixed syntax: + +-- Mixing simplified accessor with pre-standard subscripting +SELECT data.location['latitude'] FROM table; -- Avoid +SELECT data['repertoire'][0].title FROM table; -- Avoid + + While these mixed forms work as designed, they can be very confusing + because the simplified accessor cannot handle text-based subscripts like `['field']`. + This forces a fallback to pre-standard semantics for those specific parts, + creating a chain that switches between lax mode (for dot notation) and + strict mode (for text subscripts) within the same accessor expression. + + + + Choose one approach consistently throughout your accessor chain to ensure + predictable and maintainable code. + + + + Current Implementation: The current implementation supports + JSON member accessors (dot notation) and JSON array accessors (integer subscripts) + as defined in the SQL:2023 simplified accessor specification. + Advanced features from the SQL:2023 specification, such as wildcard member + accessors and item method accessors, are not yet implemented. + + + + Transforms diff --git a/src/backend/catalog/sql_features.txt b/src/backend/catalog/sql_features.txt index ebe85337c28..457e993305e 100644 --- a/src/backend/catalog/sql_features.txt +++ b/src/backend/catalog/sql_features.txt @@ -568,8 +568,8 @@ T838 JSON_TABLE: PLAN DEFAULT clause NO T839 Formatted cast of datetimes to/from character strings NO T840 Hex integer literals in SQL/JSON path language YES T851 SQL/JSON: optional keywords for default syntax YES -T860 SQL/JSON simplified accessor: column reference only NO -T861 SQL/JSON simplified accessor: case-sensitive JSON member accessor NO +T860 SQL/JSON simplified accessor: column reference only YES +T861 SQL/JSON simplified accessor: case-sensitive JSON member accessor YES T862 SQL/JSON simplified accessor: wildcard member accessor NO T863 SQL/JSON simplified accessor: single-quoted string literal as member accessor NO T864 SQL/JSON simplified accessor NO diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c index f1569879b52..385c8d0cefe 100644 --- a/src/backend/executor/execExpr.c +++ b/src/backend/executor/execExpr.c @@ -3320,50 +3320,59 @@ ExecInitSubscriptingRef(ExprEvalStep *scratch, SubscriptingRef *sbsref, state->steps_len - 1); } - /* Evaluate upper subscripts */ - i = 0; - foreach(lc, sbsref->refupperindexpr) + /* Evaluate upper subscripts, unless refjsonbpath is used for execution */ + if (!sbsref->refjsonbpath) { - Expr *e = (Expr *) lfirst(lc); - - /* When slicing, individual subscript bounds can be omitted */ - if (!e) - { - sbsrefstate->upperprovided[i] = false; - sbsrefstate->upperindexnull[i] = true; - } - else + i = 0; + foreach(lc, sbsref->refupperindexpr) { - sbsrefstate->upperprovided[i] = true; - /* Each subscript is evaluated into appropriate array entry */ - ExecInitExprRec(e, state, - &sbsrefstate->upperindex[i], - &sbsrefstate->upperindexnull[i]); + Expr *e = (Expr *) lfirst(lc); + + /* When slicing, individual subscript bounds can be omitted */ + if (!e) + { + sbsrefstate->upperprovided[i] = false; + sbsrefstate->upperindexnull[i] = true; + } + else + { + sbsrefstate->upperprovided[i] = true; + /* Each subscript is evaluated into appropriate array entry */ + ExecInitExprRec(e, state, + &sbsrefstate->upperindex[i], + &sbsrefstate->upperindexnull[i]); + } + i++; } - i++; } - /* Evaluate lower subscripts similarly */ - i = 0; - foreach(lc, sbsref->reflowerindexpr) + /* + * Evaluate lower subscripts similarly, unless refjsonbpath is used for + * execution + */ + if (!sbsref->refjsonbpath) { - Expr *e = (Expr *) lfirst(lc); - - /* When slicing, individual subscript bounds can be omitted */ - if (!e) - { - sbsrefstate->lowerprovided[i] = false; - sbsrefstate->lowerindexnull[i] = true; - } - else + i = 0; + foreach(lc, sbsref->reflowerindexpr) { - sbsrefstate->lowerprovided[i] = true; - /* Each subscript is evaluated into appropriate array entry */ - ExecInitExprRec(e, state, - &sbsrefstate->lowerindex[i], - &sbsrefstate->lowerindexnull[i]); + Expr *e = (Expr *) lfirst(lc); + + /* When slicing, individual subscript bounds can be omitted */ + if (!e) + { + sbsrefstate->lowerprovided[i] = false; + sbsrefstate->lowerindexnull[i] = true; + } + else + { + sbsrefstate->lowerprovided[i] = true; + /* Each subscript is evaluated into appropriate array entry */ + ExecInitExprRec(e, state, + &sbsrefstate->lowerindex[i], + &sbsrefstate->lowerindexnull[i]); + } + i++; } - i++; } /* SBSREF_SUBSCRIPTS checks and converts all the subscripts at once */ diff --git a/src/backend/nodes/nodeFuncs.c b/src/backend/nodes/nodeFuncs.c index 7bc823507f1..cab5d8d9f32 100644 --- a/src/backend/nodes/nodeFuncs.c +++ b/src/backend/nodes/nodeFuncs.c @@ -284,6 +284,15 @@ exprType(const Node *expr) case T_PlaceHolderVar: type = exprType((Node *) ((const PlaceHolderVar *) expr)->phexpr); break; + case T_FieldAccessorExpr: + + /* + * FieldAccessorExpr is not evaluable. Treat it as TEXT for + * collation, deparsing, and similar purposes, since it represents + * a JSON field name. + */ + type = TEXTOID; + break; default: elog(ERROR, "unrecognized node type: %d", (int) nodeTag(expr)); type = InvalidOid; /* keep compiler quiet */ @@ -1146,6 +1155,9 @@ exprSetCollation(Node *expr, Oid collation) case T_MergeSupportFunc: ((MergeSupportFunc *) expr)->msfcollid = collation; break; + case T_FieldAccessorExpr: + ((FieldAccessorExpr *) expr)->faecollid = collation; + break; case T_SubscriptingRef: ((SubscriptingRef *) expr)->refcollid = collation; break; @@ -2129,6 +2141,7 @@ expression_tree_walker_impl(Node *node, case T_SortGroupClause: case T_CTESearchClause: case T_MergeSupportFunc: + case T_FieldAccessorExpr: /* primitive node types with no expression subnodes */ break; case T_WithCheckOption: @@ -3008,6 +3021,7 @@ expression_tree_mutator_impl(Node *node, case T_SortGroupClause: case T_CTESearchClause: case T_MergeSupportFunc: + case T_FieldAccessorExpr: return copyObject(node); case T_WithCheckOption: { diff --git a/src/backend/utils/adt/jsonbsubs.c b/src/backend/utils/adt/jsonbsubs.c index 5757334f4eb..308e66e8d4f 100644 --- a/src/backend/utils/adt/jsonbsubs.c +++ b/src/backend/utils/adt/jsonbsubs.c @@ -15,21 +15,30 @@ #include "postgres.h" #include "executor/execExpr.h" +#include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" #include "nodes/subscripting.h" #include "parser/parse_coerce.h" #include "parser/parse_expr.h" #include "utils/builtins.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" -/* SubscriptingRefState.workspace for jsonb subscripting execution */ +/* + * SubscriptingRefState.workspace for generic jsonb subscripting execution. + * + * Stores state for both jsonb simple subscripting and dot notation access. + * Dot notation additionally uses `jsonpath` for JsonPath evaluation. + */ typedef struct JsonbSubWorkspace { bool expectArray; /* jsonb root is expected to be an array */ Oid *indexOid; /* OID of coerced subscript expression, could * be only integer or text */ Datum *index; /* Subscript values in Datum format */ + JsonPath *jsonpath; /* JsonPath for dot notation execution via + * JsonPathQuery() */ } JsonbSubWorkspace; static Node * @@ -96,6 +105,233 @@ coerce_jsonpath_subscript_to_int4_or_text(ParseState *pstate, Node *subExpr) return subExpr; } +/* + * During transformation, determine whether to build a JsonPath + * for JsonPathQuery() execution. + * + * JsonPath is needed if the indirection list includes: + * - String-based access (dot notation) + * - Slice-based subscripting (when isSlice is true) + * + * Otherwise, simple jsonb subscripting is enough. + */ +static bool +jsonb_check_jsonpath_needed(List *indirection) +{ + ListCell *lc; + + foreach(lc, indirection) + { + Node *accessor = lfirst(lc); + + if (IsA(accessor, String)) + return true; + else + Assert(IsA(accessor, A_Indices)); + } + + return false; +} + +/* + * Helper functions for constructing JsonPath expressions. + * + * The make_jsonpath_item_* functions create various types of JsonPathParseItem + * nodes, which are used to build JsonPath expressions for jsonb simplified + * accessor. + */ + +static JsonPathParseItem * +make_jsonpath_item(JsonPathItemType type) +{ + JsonPathParseItem *v = palloc(sizeof(*v)); + + v->type = type; + v->next = NULL; + + return v; +} + +/* + * Convert a constant integer expression into a JsonPathParseItem. + * + * The input expression must be a non-null constant of type INT4. Returns NULL otherwise. + * This function constructs a jpiNumeric item for use in JsonPath and appends a matching + * Const(INT4) node to the given expression list for use in EXPLAIN, views, etc. + * + * Parameters: + * - pstate: parse state context + * - expr: input expression node + * - exprs: list of expression nodes (updated in place) + */ +static JsonPathParseItem * +make_jsonpath_item_expr(ParseState *pstate, Node *expr, List **exprs) +{ + Const *cnst; + + expr = transformExpr(pstate, expr, pstate->p_expr_kind); + + if (IsA(expr, Const)) + { + cnst = (Const *) expr; + if (cnst->consttype == INT4OID && !(cnst->constisnull)) + { + JsonPathParseItem *jpi = make_jsonpath_item(jpiNumeric); + + jpi->value.numeric = + DatumGetNumeric(DirectFunctionCall1(int4_numeric, Int32GetDatum(cnst->constvalue))); + + *exprs = lappend(*exprs, makeConst(INT4OID, -1, InvalidOid, 4, + Int32GetDatum(cnst->constvalue), false, true)); + + return jpi; + } + } + + return NULL; +} + +/* + * Constructs a JsonPath expression from a list of indirections. + * This function is used when jsonb subscripting involves dot notation, + * requiring JsonPath-based evaluation. + * + * The function modifies the indirection list in place, removing processed + * elements as it converts them into JsonPath components, as follows: + * - String keys (dot notation) -> jpiKey items. + * - Array indices -> jpiIndexArray items. + * + * In addition to building the JsonPath expression, this function populates + * the following fields of the given SubscriptingRef: + * - refjsonbpath: the generated JsonPath + * - refupperindexpr: upper index expressions (object keys or array indexes) + * - reflowerindexpr: lower index expressions, remains NIL as slices are not supported. + * + * Parameters: + * - pstate: Parse state context. + * - indirection: List of subscripting expressions (modified in-place). + * - sbsref: SubscriptingRef node to update + */ +static int +jsonb_subscript_make_jsonpath(ParseState *pstate, List *indirection, SubscriptingRef *sbsref) +{ + JsonPathParseResult jpres; + JsonPathParseItem *path = make_jsonpath_item(jpiRoot); + ListCell *lc; + Datum jsp; + int pathlen = 0; + + sbsref->refupperindexpr = NIL; + sbsref->reflowerindexpr = NIL; + sbsref->refjsonbpath = NULL; + + jpres.expr = path; + jpres.lax = true; + + foreach(lc, indirection) + { + Node *accessor = lfirst(lc); + JsonPathParseItem *jpi; + + if (IsA(accessor, String)) + { + char *field = strVal(accessor); + FieldAccessorExpr *accessor_expr; + + jpi = make_jsonpath_item(jpiKey); + jpi->value.string.val = field; + jpi->value.string.len = strlen(field); + + accessor_expr = makeNode(FieldAccessorExpr); + accessor_expr->type = T_FieldAccessorExpr; + accessor_expr->fieldname = field; + + sbsref->refupperindexpr = lappend(sbsref->refupperindexpr, accessor_expr); + } + else if (IsA(accessor, A_Indices)) + { + A_Indices *ai = castNode(A_Indices, accessor); + + if (!ai->is_slice) + { + JsonPathParseItem *jpi_from = NULL; + + Assert(ai->uidx && !ai->lidx); + jpi_from = make_jsonpath_item_expr(pstate, ai->uidx, &sbsref->refupperindexpr); + if (jpi_from == NULL) + { + /* + * Break out of the loop if the subscript is not a + * non-null integer constant, so that we can fall back to + * jsonb subscripting logic. + * + * This is needed to handle cases with mixed usage of SQL + * standard json simplified accessor syntax and PostgreSQL + * jsonb subscripting syntax, e.g: + * + * select (jb).a['b'].c from jsonb_table; + * + * where dot-notation (.a and .c) is the SQL standard json + * simplified accessor syntax, and the ['b'] subscript is + * the PostgreSQL jsonb subscripting syntax, because 'b' + * is not a non-null constant integer and cannot be used + * for json array access. + * + * In this case, we cannot create a JsonPath item, so we + * break out of the loop and let + * jsonb_subscript_transform() handle this indirection as + * a PostgreSQL jsonb subscript. + */ + break; + } + + jpi = make_jsonpath_item(jpiIndexArray); + jpi->value.array.nelems = 1; + jpi->value.array.elems = palloc(sizeof(jpi->value.array.elems[0])); + + jpi->value.array.elems[0].from = jpi_from; + jpi->value.array.elems[0].to = NULL; + } + else + { + Node *expr = ai->uidx ? ai->uidx : ai->lidx; + + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("jsonb subscript does not support slices"), + parser_errposition(pstate, exprLocation(expr)))); + } + } + else + { + /* + * Unexpected node type in indirection list. This should not + * happen with current grammar, but we handle it defensively by + * breaking out of the loop rather than crashing. In case of + * future grammar changes that might introduce new node types, + * this allows us to create a jsonpath from as many indirection + * elements as we can and let transformIndirection() fallback to + * alternative logic to handle the remaining indirection elements. + */ + Assert(false); /* not reachable */ + break; + } + + /* append path item */ + path->next = jpi; + path = jpi; + pathlen++; + } + + if (pathlen != 0) + { + jsp = jsonPathFromParseResult(&jpres, 0, NULL); + sbsref->refjsonbpath = (Node *) makeConst(JSONPATHOID, -1, InvalidOid, -1, jsp, false, false); + } + + return pathlen; +} + /* * Finish parse analysis of a SubscriptingRef expression for a jsonb. * @@ -114,9 +350,29 @@ jsonb_subscript_transform(SubscriptingRef *sbsref, List *upperIndexpr = NIL; ListCell *idx; + /* Determine the result type of the subscripting operation; always jsonb */ + sbsref->refrestype = JSONBOID; + sbsref->reftypmod = -1; + + if (jsonb_check_jsonpath_needed(indirection)) + { + int pathlen; + + pathlen = jsonb_subscript_make_jsonpath(pstate, indirection, sbsref); + if (sbsref->refjsonbpath) + return pathlen; + } + /* - * Transform and convert the subscript expressions. Jsonb subscripting - * does not support slices, look only at the upper index. + * We reach here only in two cases: (a) the JSON simplified accessor is + * not needed at all (for example, a plain array subscript like [1] or + * object key access like ['a']), or (b) jsonb_subscript_make_jsonpath() + * was called but could not complete the JsonPath construction (for + * example, when mixing dot notation with non-integer subscripts like + * (jb)['a'].b where 'a' is not a constant integer). + * + * In both cases we fall back to pre-standard jsonb subscripting, coercing + * each subscript to array index or object key as needed. */ foreach(idx, indirection) { @@ -163,10 +419,6 @@ jsonb_subscript_transform(SubscriptingRef *sbsref, sbsref->refupperindexpr = upperIndexpr; sbsref->reflowerindexpr = NIL; - /* Determine the result type of the subscripting operation; always jsonb */ - sbsref->refrestype = JSONBOID; - sbsref->reftypmod = -1; - return list_length(upperIndexpr); } @@ -219,7 +471,7 @@ jsonb_subscript_check_subscripts(ExprState *state, * For jsonb fetch and assign functions we need to provide path in * text format. Convert if it's not already text. */ - if (workspace->indexOid[i] == INT4OID) + if (!workspace->jsonpath && workspace->indexOid[i] == INT4OID) { Datum datum = sbsrefstate->upperindex[i]; char *cs = DatumGetCString(DirectFunctionCall1(int4out, datum)); @@ -247,17 +499,32 @@ jsonb_subscript_fetch(ExprState *state, { SubscriptingRefState *sbsrefstate = op->d.sbsref.state; JsonbSubWorkspace *workspace = (JsonbSubWorkspace *) sbsrefstate->workspace; - Jsonb *jsonbSource; /* Should not get here if source jsonb (or any subscript) is null */ Assert(!(*op->resnull)); - jsonbSource = DatumGetJsonbP(*op->resvalue); - *op->resvalue = jsonb_get_element(jsonbSource, - workspace->index, - sbsrefstate->numupper, - op->resnull, - false); + if (workspace->jsonpath) + { + bool empty = false; + bool error = false; + + *op->resvalue = JsonPathQuery(*op->resvalue, workspace->jsonpath, + JSW_CONDITIONAL, + &empty, &error, NULL, + NULL); + + *op->resnull = empty || error; + } + else + { + Jsonb *jsonbSource = DatumGetJsonbP(*op->resvalue); + + *op->resvalue = jsonb_get_element(jsonbSource, + workspace->index, + sbsrefstate->numupper, + op->resnull, + false); + } } /* @@ -365,7 +632,7 @@ jsonb_exec_setup(const SubscriptingRef *sbsref, { JsonbSubWorkspace *workspace; ListCell *lc; - int nupper = sbsref->refupperindexpr->length; + int nupper = list_length(sbsref->refupperindexpr); char *ptr; /* Allocate type-specific workspace with space for per-subscript data */ @@ -374,6 +641,9 @@ jsonb_exec_setup(const SubscriptingRef *sbsref, workspace->expectArray = false; ptr = ((char *) workspace) + MAXALIGN(sizeof(JsonbSubWorkspace)); + if (sbsref->refjsonbpath) + workspace->jsonpath = DatumGetJsonPathP(castNode(Const, sbsref->refjsonbpath)->constvalue); + /* * This coding assumes sizeof(Datum) >= sizeof(Oid), else we might * misalign the indexOid pointer diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 0408a95941d..761918f899e 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -9329,10 +9329,13 @@ get_rule_expr(Node *node, deparse_context *context, * Parenthesize the argument unless it's a simple Var or a * FieldSelect. (In particular, if it's another * SubscriptingRef, we *must* parenthesize to avoid - * confusion.) + * confusion.) Always add parenthesis if JSON simplified + * accessor is used, for now. */ - need_parens = !IsA(sbsref->refexpr, Var) && - !IsA(sbsref->refexpr, FieldSelect); + need_parens = (!IsA(sbsref->refexpr, Var) && + !IsA(sbsref->refexpr, FieldSelect)) || + sbsref->refjsonbpath; + if (need_parens) appendStringInfoChar(buf, '('); get_rule_expr((Node *) sbsref->refexpr, context, showimplicit); @@ -13005,17 +13008,35 @@ printSubscripts(SubscriptingRef *sbsref, deparse_context *context) lowlist_item = list_head(sbsref->reflowerindexpr); /* could be NULL */ foreach(uplist_item, sbsref->refupperindexpr) { - appendStringInfoChar(buf, '['); - if (lowlist_item) + Node *upper = (Node *) lfirst(uplist_item); + + if (upper && IsA(upper, FieldAccessorExpr)) { + FieldAccessorExpr *fae = (FieldAccessorExpr *) upper; + + /* Use dot-notation for field access */ + appendStringInfoChar(buf, '.'); + appendStringInfoString(buf, quote_identifier(fae->fieldname)); + + /* Skip matching low index — field access doesn't use slices */ + if (lowlist_item) + lowlist_item = lnext(sbsref->reflowerindexpr, lowlist_item); + } + else + { + /* Use JSONB array subscripting */ + appendStringInfoChar(buf, '['); + if (lowlist_item) + { + /* If subexpression is NULL, get_rule_expr prints nothing */ + get_rule_expr((Node *) lfirst(lowlist_item), context, false); + appendStringInfoChar(buf, ':'); + lowlist_item = lnext(sbsref->reflowerindexpr, lowlist_item); + } /* If subexpression is NULL, get_rule_expr prints nothing */ - get_rule_expr((Node *) lfirst(lowlist_item), context, false); - appendStringInfoChar(buf, ':'); - lowlist_item = lnext(sbsref->reflowerindexpr, lowlist_item); + get_rule_expr(upper, context, false); + appendStringInfoChar(buf, ']'); } - /* If subexpression is NULL, get_rule_expr prints nothing */ - get_rule_expr((Node *) lfirst(uplist_item), context, false); - appendStringInfoChar(buf, ']'); } } diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h index 6dfca3cb35b..4f519f5031f 100644 --- a/src/include/nodes/primnodes.h +++ b/src/include/nodes/primnodes.h @@ -708,18 +708,30 @@ typedef struct SubscriptingRef int32 reftypmod pg_node_attr(query_jumble_ignore); /* collation of result, or InvalidOid if none */ Oid refcollid pg_node_attr(query_jumble_ignore); - /* expressions that evaluate to upper container indexes */ + + /* + * expressions that evaluate to upper container indexes or expressions + * that are collected but not evaluated when refjsonbpath is set. + */ List *refupperindexpr; /* - * expressions that evaluate to lower container indexes, or NIL for single - * container element. + * expressions that evaluate to lower container indexes, or NIL for a + * single container element, or expressions that are collected but not + * evaluated when refjsonbpath is set. */ List *reflowerindexpr; /* the expression that evaluates to a container value */ Expr *refexpr; /* expression for the source value, or NULL if fetch */ Expr *refassgnexpr; + + /* + * container-specific extra information, currently used only by jsonb. + * stores a JsonPath expression when jsonb dot notation is used. NULL for + * simple subscripting. + */ + Node *refjsonbpath; } SubscriptingRef; /* @@ -2371,4 +2383,40 @@ typedef struct OnConflictExpr List *exclRelTlist; /* tlist of the EXCLUDED pseudo relation */ } OnConflictExpr; +/* + * FieldAccessorExpr - represents a single object member access using dot-notation + * in JSON simplified accessor syntax (e.g., jsonb_col.a). + * + * These nodes appear as list elements in SubscriptingRef.refupperindexpr to + * indicate JSON object key access. They are not evaluable expressions by + * themselves but serve as placeholders to preserve source-level syntax for + * rule rewriting and deparsing (e.g., in EXPLAIN and view definitions). + * Execution is handled by the enclosing SubscriptingRef. + * + * If dot-notation is used in a SubscriptingRef, the JSON path is represented + * as a flat list of FieldAccessorExpr nodes (for object field access), Const + * nodes (for array indexes), and NULLs (for omitted slice bounds), rather than + * through nested expression trees. + * + * Note: The flat representation avoids nested FieldAccessorExpr chains, + * simplifying evaluation and enabling standard-compliant behavior such as + * conditional array wrapping. This avoids the need for position-aware + * wrapping/unwrapping logic during execution. + * + * For example, in the expression: + * ('{"a": [{"b": 1}]}'::jsonb).a[0].b + * the SubscriptingRef will contain: + * - refexpr: the base expression (the jsonb value) + * - refupperindexpr: [FieldAccessorExpr("a"), Const(0), + * FieldAccessorExpr("b")] + * - reflowerindexpr: [NULL, NULL, NULL] (slice lower bounds not used here) + */ +typedef struct FieldAccessorExpr +{ + NodeTag type; + char *fieldname; /* name of the JSONB object field accessed via + * dot notation */ + Oid faecollid pg_node_attr(query_jumble_ignore); +} FieldAccessorExpr; + #endif /* PRIMNODES_H */ diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.c b/src/interfaces/ecpg/test/expected/sql-sqljson.c index 39221f9ea5d..e6a7ece6dab 100644 --- a/src/interfaces/ecpg/test/expected/sql-sqljson.c +++ b/src/interfaces/ecpg/test/expected/sql-sqljson.c @@ -417,12 +417,122 @@ if (sqlca.sqlcode < 0) sqlprint();} for (int i = 0; i < sizeof(is_json); i++) printf("Found is_json[%d]: %s\n", i, is_json[i] ? "true" : "false"); - { ECPGdisconnect(__LINE__, "CURRENT"); + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}}' :: jsonb ) . \"a\" )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); #line 118 "sqljson.pgc" if (sqlca.sqlcode < 0) sqlprint();} #line 118 "sqljson.pgc" + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( '{\"a\": {\"b\": 1, \"c\": 2}}' :: jsonb . \"a\" )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 121 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 121 "sqljson.pgc" + + // error + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}}' :: jsonb ) . a )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 124 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 124 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}}' :: jsonb ) . a . b )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 127 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 127 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( coalesce ( json ( ( '{\"a\": {\"b\": 1, \"c\": 2}}' :: jsonb ) . c ) , 'null' ) )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 130 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 130 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}, \"b\": [{\"x\": 1}, {\"x\": [12, {\"y\":1}]}]}' :: jsonb ) . b [ 0 ] )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 133 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 133 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}, \"b\": [{\"x\": 1}, {\"x\": [12, {\"y\":1}]}]}' :: jsonb ) . b . x )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 136 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 136 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}, \"b\": [{\"x\": 1}, {\"x\": [12, {\"y\":1}]}]}' :: jsonb ) [ 'b' ] [ 0 ] )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 139 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 139 "sqljson.pgc" + + printf("Found json=%s\n", json); + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}, \"b\": [{\"x\": 1}, {\"x\": [12, {\"y\":1}]}]}' :: jsonb ) . b [ 1 ] . x [ 0 : ] )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 142 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 142 "sqljson.pgc" + + // error + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}, \"b\": [{\"x\": 1}, {\"x\": [12, {\"y\":1}]}]}' :: jsonb ) . * )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 145 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 145 "sqljson.pgc" + + // error + + { ECPGdo(__LINE__, 0, 1, NULL, 0, ECPGst_normal, "select json ( ( '{\"a\": {\"b\": 1, \"c\": 2}, \"b\": [{\"x\": 1}, {\"x\": [12, {\"y\":1}]}]}' :: jsonb ) [ 'b' ] [ 0 : ] )", ECPGt_EOIT, + ECPGt_char,(json),(long)1024,(long)1,(1024)*sizeof(char), + ECPGt_NO_INDICATOR, NULL , 0L, 0L, 0L, ECPGt_EORT); +#line 148 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 148 "sqljson.pgc" + + // error + + { ECPGdisconnect(__LINE__, "CURRENT"); +#line 151 "sqljson.pgc" + +if (sqlca.sqlcode < 0) sqlprint();} +#line 151 "sqljson.pgc" + return 0; } diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.stderr b/src/interfaces/ecpg/test/expected/sql-sqljson.stderr index e55a95dd711..19f8c58af06 100644 --- a/src/interfaces/ecpg/test/expected/sql-sqljson.stderr +++ b/src/interfaces/ecpg/test/expected/sql-sqljson.stderr @@ -268,5 +268,105 @@ SQL error: cannot use type jsonb in RETURNING clause of JSON_SERIALIZE() on line [NO_PID]: sqlca: code: 0, state: 00000 [NO_PID]: ecpg_get_data on line 102: RESULT: f offset: -1; array: no [NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 118: query: select json ( ( '{"a": {"b": 1, "c": 2}}' :: jsonb ) . "a" ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 118: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 118: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 118: RESULT: {"b": 1, "c": 2} offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 121: query: select json ( '{"a": {"b": 1, "c": 2}}' :: jsonb . "a" ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 121: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_check_PQresult on line 121: bad response - ERROR: schema "jsonb" does not exist +LINE 1: select json ( '{"a": {"b": 1, "c": 2}}' :: jsonb . "a" ) + ^ +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlstate 3F000 (sqlcode -400): schema "jsonb" does not exist on line 121 +[NO_PID]: sqlca: code: -400, state: 3F000 +SQL error: schema "jsonb" does not exist on line 121 +[NO_PID]: ecpg_execute on line 124: query: select json ( ( '{"a": {"b": 1, "c": 2}}' :: jsonb ) . a ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 124: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 124: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 124: RESULT: {"b": 1, "c": 2} offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 127: query: select json ( ( '{"a": {"b": 1, "c": 2}}' :: jsonb ) . a . b ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 127: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 127: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 127: RESULT: 1 offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 130: query: select json ( coalesce ( json ( ( '{"a": {"b": 1, "c": 2}}' :: jsonb ) . c ) , 'null' ) ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 130: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 130: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 130: RESULT: null offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 133: query: select json ( ( '{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}' :: jsonb ) . b [ 0 ] ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 133: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 133: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 133: RESULT: {"x": 1} offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 136: query: select json ( ( '{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}' :: jsonb ) . b . x ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 136: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 136: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 136: RESULT: [1, [12, {"y": 1}]] offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 139: query: select json ( ( '{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}' :: jsonb ) [ 'b' ] [ 0 ] ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 139: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_process_output on line 139: correctly got 1 tuples with 1 fields +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_get_data on line 139: RESULT: {"x": 1} offset: -1; array: no +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 142: query: select json ( ( '{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}' :: jsonb ) . b [ 1 ] . x [ 0 : ] ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 142: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_check_PQresult on line 142: bad response - ERROR: jsonb subscript does not support slices +LINE 1: ..., {"x": [12, {"y":1}]}]}' :: jsonb ) . b [ 1 ] . x [ 0 : ] ) + ^ +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlstate 42804 (sqlcode -400): jsonb subscript does not support slices on line 142 +[NO_PID]: sqlca: code: -400, state: 42804 +SQL error: jsonb subscript does not support slices on line 142 +[NO_PID]: ecpg_execute on line 145: query: select json ( ( '{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}' :: jsonb ) . * ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 145: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_check_PQresult on line 145: bad response - ERROR: row expansion via "*" is not supported here +LINE 1: select json ( ( '{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x... + ^ +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlstate 0A000 (sqlcode -400): row expansion via "*" is not supported here on line 145 +[NO_PID]: sqlca: code: -400, state: 0A000 +SQL error: row expansion via "*" is not supported here on line 145 +[NO_PID]: ecpg_execute on line 148: query: select json ( ( '{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}' :: jsonb ) [ 'b' ] [ 0 : ] ); with 0 parameter(s) on connection ecpg1_regression +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_execute on line 148: using PQexec +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: ecpg_check_PQresult on line 148: bad response - ERROR: jsonb subscript does not support slices +LINE 1: ...x": 1}, {"x": [12, {"y":1}]}]}' :: jsonb ) [ 'b' ] [ 0 : ] ) + ^ +[NO_PID]: sqlca: code: 0, state: 00000 +[NO_PID]: raising sqlstate 42804 (sqlcode -400): jsonb subscript does not support slices on line 148 +[NO_PID]: sqlca: code: -400, state: 42804 +SQL error: jsonb subscript does not support slices on line 148 [NO_PID]: ecpg_finish: connection ecpg1_regression closed [NO_PID]: sqlca: code: 0, state: 00000 diff --git a/src/interfaces/ecpg/test/expected/sql-sqljson.stdout b/src/interfaces/ecpg/test/expected/sql-sqljson.stdout index 83f8df13e5a..442d36931f1 100644 --- a/src/interfaces/ecpg/test/expected/sql-sqljson.stdout +++ b/src/interfaces/ecpg/test/expected/sql-sqljson.stdout @@ -28,3 +28,10 @@ Found is_json[4]: false Found is_json[5]: false Found is_json[6]: true Found is_json[7]: false +Found json={"b": 1, "c": 2} +Found json={"b": 1, "c": 2} +Found json=1 +Found json=null +Found json={"x": 1} +Found json=[1, [12, {"y": 1}]] +Found json={"x": 1} diff --git a/src/interfaces/ecpg/test/sql/sqljson.pgc b/src/interfaces/ecpg/test/sql/sqljson.pgc index ddcbcc3b3cb..57a9bff424d 100644 --- a/src/interfaces/ecpg/test/sql/sqljson.pgc +++ b/src/interfaces/ecpg/test/sql/sqljson.pgc @@ -115,6 +115,39 @@ EXEC SQL END DECLARE SECTION; for (int i = 0; i < sizeof(is_json); i++) printf("Found is_json[%d]: %s\n", i, is_json[i] ? "true" : "false"); + EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}}'::jsonb)."a") INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON('{"a": {"b": 1, "c": 2}}'::jsonb."a") INTO :json; + // error + + EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}}'::jsonb).a) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}}'::jsonb).a.b) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON(COALESCE(JSON(('{"a": {"b": 1, "c": 2}}'::jsonb).c), 'null')) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}'::jsonb).b[0]) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}'::jsonb).b.x) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}'::jsonb)['b'][0]) INTO :json; + printf("Found json=%s\n", json); + + EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}'::jsonb).b[1].x[0:]) INTO :json; + // error + + EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}'::jsonb).*) INTO :json; + // error + + EXEC SQL SELECT JSON(('{"a": {"b": 1, "c": 2}, "b": [{"x": 1}, {"x": [12, {"y":1}]}]}'::jsonb)['b'][0:]) INTO :json; + // error + EXEC SQL DISCONNECT; return 0; diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index fb47c8a893a..0947f836de8 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -4989,6 +4989,12 @@ select ('123'::jsonb)['a']; (1 row) +select ('123'::jsonb).a; + a +--- + +(1 row) + select ('123'::jsonb)[0]; jsonb ------- @@ -5001,12 +5007,24 @@ select ('123'::jsonb)[NULL]; (1 row) +select ('123'::jsonb).NULL; + null +------ + +(1 row) + select ('{"a": 1}'::jsonb)['a']; jsonb ------- 1 (1 row) +select ('{"a": 1}'::jsonb).a; + a +--- + 1 +(1 row) + select ('{"a": 1}'::jsonb)[0]; jsonb ------- @@ -5019,6 +5037,12 @@ select ('{"a": 1}'::jsonb)['not_exist']; (1 row) +select ('{"a": 1}'::jsonb)."not_exist"; + not_exist +----------- + +(1 row) + select ('{"a": 1}'::jsonb)[NULL]; jsonb ------- @@ -5031,6 +5055,12 @@ select ('[1, "2", null]'::jsonb)['a']; (1 row) +select ('[1, "2", null]'::jsonb).a; + a +--- + +(1 row) + select ('[1, "2", null]'::jsonb)[0]; jsonb ------- @@ -5043,6 +5073,12 @@ select ('[1, "2", null]'::jsonb)['1']; "2" (1 row) +select ('[1, "2", null]'::jsonb)."1"; + 1 +--- + +(1 row) + select ('[1, "2", null]'::jsonb)[1.0]; ERROR: subscript type numeric is not supported LINE 1: select ('[1, "2", null]'::jsonb)[1.0]; @@ -5072,6 +5108,12 @@ select ('[1, "2", null]'::jsonb)[1]['a']; (1 row) +select ('[1, "2", null]'::jsonb)[1].a; + a +--- + +(1 row) + select ('[1, "2", null]'::jsonb)[1][0]; jsonb ------- @@ -5084,54 +5126,126 @@ select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb)['b']; "c" (1 row) +select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb).b; + b +----- + "c" +(1 row) + select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb)['d']; jsonb ----------- [1, 2, 3] (1 row) +select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb).d; + d +----------- + [1, 2, 3] +(1 row) + select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb)['d'][1]; jsonb ------- 2 (1 row) +select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb).d[1]; + d +--- + 2 +(1 row) + select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb)['d']['a']; jsonb ------- (1 row) +select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb).d['a']; + d +--- + +(1 row) + +select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb).d.a; + a +--- + +(1 row) + +select ('{"a": 1, "b": "c", "d": {"a": [1, 2, 3]}}'::jsonb).d['a'][0]; + d +--- + 1 +(1 row) + +select ('{"a": 1, "b": "c", "d": {"a": [1, 2, 3]}}'::jsonb).d.a[0]; + a +--- + 1 +(1 row) + select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb)['a']['a1']; jsonb --------------- {"a2": "aaa"} (1 row) +select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb).a.a1; + a1 +--------------- + {"a2": "aaa"} +(1 row) + select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb)['a']['a1']['a2']; jsonb ------- "aaa" (1 row) +select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb).a.a1.a2; + a2 +------- + "aaa" +(1 row) + select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb)['a']['a1']['a2']['a3']; jsonb ------- (1 row) +select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb).a.a1.a2.a3; + a3 +---- + +(1 row) + select ('{"a": ["a1", {"b1": ["aaa", "bbb", "ccc"]}], "b": "bb"}'::jsonb)['a'][1]['b1']; jsonb ----------------------- ["aaa", "bbb", "ccc"] (1 row) +select ('{"a": ["a1", {"b1": ["aaa", "bbb", "ccc"]}], "b": "bb"}'::jsonb).a[1].b1; + b1 +----------------------- + ["aaa", "bbb", "ccc"] +(1 row) + select ('{"a": ["a1", {"b1": ["aaa", "bbb", "ccc"]}], "b": "bb"}'::jsonb)['a'][1]['b1'][2]; jsonb ------- "ccc" (1 row) +select ('{"a": ["a1", {"b1": ["aaa", "bbb", "ccc"]}], "b": "bb"}'::jsonb).a[1].b1[2]; + b1 +------- + "ccc" +(1 row) + -- slices are not supported select ('{"a": 1}'::jsonb)['a':'b']; ERROR: jsonb subscript does not support slices @@ -5841,7 +5955,7 @@ select jsonb_typeof('{"a":1}'::jsonb); select ('{"a":1}'::jsonb).jsonb_typeof; jsonb_typeof -------------- - object + (1 row) select jsonb_array_length('["a", "b", "c"]'::jsonb); @@ -5853,7 +5967,7 @@ select jsonb_array_length('["a", "b", "c"]'::jsonb); select ('["a", "b", "c"]'::jsonb).jsonb_array_length; jsonb_array_length -------------------- - 3 + (1 row) select jsonb_object_keys('{"a":1, "b":2}'::jsonb); @@ -5866,9 +5980,8 @@ select jsonb_object_keys('{"a":1, "b":2}'::jsonb); select ('{"a":1, "b":2}'::jsonb).jsonb_object_keys; jsonb_object_keys ------------------- - a - b -(2 rows) + +(1 row) -- cast jsonb to other types as (jsonb)::type and (jsonb).type select ('123.45'::jsonb)::numeric; @@ -5880,7 +5993,7 @@ select ('123.45'::jsonb)::numeric; select ('123.45'::jsonb).numeric; numeric --------- - 123.45 + (1 row) select ('[{"name": "alice"}, {"name": "bob"}]'::jsonb)::name; @@ -5890,9 +6003,9 @@ select ('[{"name": "alice"}, {"name": "bob"}]'::jsonb)::name; (1 row) select ('[{"name": "alice"}, {"name": "bob"}]'::jsonb).name; - name --------------------------------------- - [{"name": "alice"}, {"name": "bob"}] + name +------------------ + ["alice", "bob"] (1 row) select ('true'::jsonb)::bool; @@ -5904,7 +6017,7 @@ select ('true'::jsonb)::bool; select ('true'::jsonb).bool; bool ------ - t + (1 row) select ('{"text": "hello"}'::jsonb)::text; @@ -5914,8 +6027,319 @@ select ('{"text": "hello"}'::jsonb)::text; (1 row) select ('{"text": "hello"}'::jsonb).text; - text -------------------- - {"text": "hello"} + text +--------- + "hello" +(1 row) + +-- dot notation +CREATE TABLE test_jsonb_dot_notation AS +SELECT '{"a": [1, 2, {"b": "c"}, {"b": "d", "e": "f", "x": {"y": "yyy", "z": "zzz"}}], "b": [3, 4, {"b": "g", "x": {"y": "YYY", "z": "ZZZ"}}]}'::jsonb jb; +SELECT (jb).a FROM test_jsonb_dot_notation; + a +------------------------------------------------------------------------- + [1, 2, {"b": "c"}, {"b": "d", "e": "f", "x": {"y": "yyy", "z": "zzz"}}] +(1 row) + +SELECT (jb)."a" FROM test_jsonb_dot_notation; -- double quote should work + a +------------------------------------------------------------------------- + [1, 2, {"b": "c"}, {"b": "d", "e": "f", "x": {"y": "yyy", "z": "zzz"}}] +(1 row) + +SELECT (jb).'a' FROM test_jsonb_dot_notation; -- single quote should not work +ERROR: syntax error at or near "'a'" +LINE 1: SELECT (jb).'a' FROM test_jsonb_dot_notation; + ^ +select (jb)[0].a from test_jsonb_dot_notation; -- returns same result as (jb).a + a +------------------------------------------------------------------------- + [1, 2, {"b": "c"}, {"b": "d", "e": "f", "x": {"y": "yyy", "z": "zzz"}}] +(1 row) + +select (jb)[0]['a'] from test_jsonb_dot_notation; -- returns NULL + jb +---- + +(1 row) + +select (jb)[1].a from test_jsonb_dot_notation; -- returns NULL + a +--- + +(1 row) + +SELECT (jb).b FROM test_jsonb_dot_notation; + b +--------------------------------------------------- + [3, 4, {"b": "g", "x": {"y": "YYY", "z": "ZZZ"}}] +(1 row) + +SELECT (jb).c FROM test_jsonb_dot_notation; + c +--- + +(1 row) + +SELECT (jb).a.b FROM test_jsonb_dot_notation; + b +------------ + ["c", "d"] +(1 row) + +SELECT (jb).a[2].b FROM test_jsonb_dot_notation; + b +----- + "c" +(1 row) + +SELECT (jb).a.x.y FROM test_jsonb_dot_notation; + y +------- + "yyy" +(1 row) + +SELECT (jb).b.x.z FROM test_jsonb_dot_notation; + z +------- + "ZZZ" +(1 row) + +SELECT (jb).a.b.c FROM test_jsonb_dot_notation; + c +--- + +(1 row) + +SELECT ((jb).b)[:].x FROM test_jsonb_dot_notation t; -- fails +ERROR: jsonb subscript does not support slices +SELECT (jb).a.* FROM test_jsonb_dot_notation; -- fails +ERROR: type jsonb is not composite +-- assignment is not supported +UPDATE test_jsonb_dot_notation SET jb.a = '1'; +ERROR: cannot assign to field "a" of column "jb" because its type jsonb is not a composite type +LINE 1: UPDATE test_jsonb_dot_notation SET jb.a = '1'; + ^ +UPDATE test_jsonb_dot_notation SET (jb).a = '1'; +ERROR: syntax error at or near "." +LINE 1: UPDATE test_jsonb_dot_notation SET (jb).a = '1'; + ^ +-- explains should work +EXPLAIN (VERBOSE, COSTS OFF) SELECT (t.jb).a FROM test_jsonb_dot_notation t; + QUERY PLAN +---------------------------------------------- + Seq Scan on public.test_jsonb_dot_notation t + Output: (jb).a +(2 rows) + +SELECT (t.jb).a FROM test_jsonb_dot_notation t; + a +------------------------------------------------------------------------- + [1, 2, {"b": "c"}, {"b": "d", "e": "f", "x": {"y": "yyy", "z": "zzz"}}] +(1 row) + +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb).a[1] FROM test_jsonb_dot_notation; + QUERY PLAN +-------------------------------------------- + Seq Scan on public.test_jsonb_dot_notation + Output: (jb).a[1] +(2 rows) + +SELECT (jb).a[1] FROM test_jsonb_dot_notation; + a +--- + 2 +(1 row) + +-- views should work +CREATE VIEW test_jsonb_dot_notation_v1 AS SELECT (jb).a[3].x.y FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +CREATE OR REPLACE VIEW public.test_jsonb_dot_notation_v1 AS + SELECT (jb).a[3].x.y AS y + FROM test_jsonb_dot_notation +-- mixed syntax +DROP VIEW test_jsonb_dot_notation_v1; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb)['a'].b FROM test_jsonb_dot_notation; + QUERY PLAN +-------------------------------------------- + Seq Scan on public.test_jsonb_dot_notation + Output: (jb['a'::text]).b +(2 rows) + +SELECT (jb)['a'].b FROM test_jsonb_dot_notation; -- returns an array due to lax mode + b +------------ + ["c", "d"] +(1 row) + +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb)['a'].b FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +CREATE OR REPLACE VIEW public.test_jsonb_dot_notation_v1 AS + SELECT (jb['a'::text]).b AS b + FROM test_jsonb_dot_notation +SELECT * from test_jsonb_dot_notation_v1; + b +------------ + ["c", "d"] +(1 row) + +DROP VIEW public.test_jsonb_dot_notation_v1; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb).a['b'] FROM test_jsonb_dot_notation; + QUERY PLAN +-------------------------------------------- + Seq Scan on public.test_jsonb_dot_notation + Output: ((jb).a)['b'::text] +(2 rows) + +SELECT (jb).a['b'] FROM test_jsonb_dot_notation; -- returns NULL because ['b'] looks for strict match in an object + a +--- + +(1 row) + +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb).a['b'] FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +CREATE OR REPLACE VIEW public.test_jsonb_dot_notation_v1 AS + SELECT ((jb).a)['b'::text] AS a + FROM test_jsonb_dot_notation +SELECT * from test_jsonb_dot_notation_v1; + a +--- + +(1 row) + +DROP VIEW public.test_jsonb_dot_notation_v1; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb).b.x['z'] FROM test_jsonb_dot_notation; + QUERY PLAN +-------------------------------------------- + Seq Scan on public.test_jsonb_dot_notation + Output: ((jb).b.x)['z'::text] +(2 rows) + +SELECT (jb).b.x['z'] FROM test_jsonb_dot_notation; -- warnings + x +------- + "ZZZ" +(1 row) + +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb).b.x['z'] FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +CREATE OR REPLACE VIEW public.test_jsonb_dot_notation_v1 AS + SELECT ((jb).b.x)['z'::text] AS x + FROM test_jsonb_dot_notation +SELECT * from test_jsonb_dot_notation_v1; + x +------- + "ZZZ" +(1 row) + +DROP VIEW public.test_jsonb_dot_notation_v1; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb)['b'].x['z'] FROM test_jsonb_dot_notation; + QUERY PLAN +-------------------------------------------- + Seq Scan on public.test_jsonb_dot_notation + Output: ((jb['b'::text]).x)['z'::text] +(2 rows) + +SELECT (jb)['b'].x['z'] FROM test_jsonb_dot_notation; -- warnings + x +------- + "ZZZ" +(1 row) + +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb)['b'].x['z'] FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +CREATE OR REPLACE VIEW public.test_jsonb_dot_notation_v1 AS + SELECT ((jb['b'::text]).x)['z'::text] AS x + FROM test_jsonb_dot_notation +SELECT * from test_jsonb_dot_notation_v1; + x +------- + "ZZZ" +(1 row) + +DROP VIEW public.test_jsonb_dot_notation_v1; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb).b['x'].z FROM test_jsonb_dot_notation; + QUERY PLAN +-------------------------------------------- + Seq Scan on public.test_jsonb_dot_notation + Output: (((jb).b)['x'::text]).z +(2 rows) + +SELECT (jb).b['x'].z FROM test_jsonb_dot_notation; -- returns NULL + z +--- + +(1 row) + +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb).b['x'].z FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +CREATE OR REPLACE VIEW public.test_jsonb_dot_notation_v1 AS + SELECT (((jb).b)['x'::text]).z AS z + FROM test_jsonb_dot_notation +SELECT * from test_jsonb_dot_notation_v1; + z +--- + +(1 row) + +DROP VIEW public.test_jsonb_dot_notation_v1; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb).b['x']['z'] FROM test_jsonb_dot_notation; + QUERY PLAN +-------------------------------------------- + Seq Scan on public.test_jsonb_dot_notation + Output: ((jb).b)['x'::text]['z'::text] +(2 rows) + +SELECT (jb).b['x']['z'] FROM test_jsonb_dot_notation; -- returns NULL + b +--- + +(1 row) + +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb).b['x']['z'] FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +CREATE OR REPLACE VIEW public.test_jsonb_dot_notation_v1 AS + SELECT ((jb).b)['x'::text]['z'::text] AS b + FROM test_jsonb_dot_notation +SELECT * from test_jsonb_dot_notation_v1; + b +--- + +(1 row) + +DROP VIEW public.test_jsonb_dot_notation_v1; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb)['b']['x'].z FROM test_jsonb_dot_notation; + QUERY PLAN +-------------------------------------------- + Seq Scan on public.test_jsonb_dot_notation + Output: (jb['b'::text]['x'::text]).z +(2 rows) + +SELECT (jb)['b']['x'].z FROM test_jsonb_dot_notation; -- returns NULL + z +--- + +(1 row) + +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb)['b']['x'].z FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +CREATE OR REPLACE VIEW public.test_jsonb_dot_notation_v1 AS + SELECT (jb['b'::text]['x'::text]).z AS z + FROM test_jsonb_dot_notation +SELECT * from test_jsonb_dot_notation_v1; + z +--- + (1 row) +DROP VIEW public.test_jsonb_dot_notation_v1; +-- clean up +DROP TABLE test_jsonb_dot_notation; diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 970ed1cffca..bd89678c718 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -1304,30 +1304,49 @@ select jsonb_insert('{"a": {"b": "value"}}', '{a, b}', '"new_value"', true); -- jsonb subscript select ('123'::jsonb)['a']; +select ('123'::jsonb).a; select ('123'::jsonb)[0]; select ('123'::jsonb)[NULL]; +select ('123'::jsonb).NULL; select ('{"a": 1}'::jsonb)['a']; +select ('{"a": 1}'::jsonb).a; select ('{"a": 1}'::jsonb)[0]; select ('{"a": 1}'::jsonb)['not_exist']; +select ('{"a": 1}'::jsonb)."not_exist"; select ('{"a": 1}'::jsonb)[NULL]; select ('[1, "2", null]'::jsonb)['a']; +select ('[1, "2", null]'::jsonb).a; select ('[1, "2", null]'::jsonb)[0]; select ('[1, "2", null]'::jsonb)['1']; +select ('[1, "2", null]'::jsonb)."1"; select ('[1, "2", null]'::jsonb)[1.0]; select ('[1, "2", null]'::jsonb)[2]; select ('[1, "2", null]'::jsonb)[3]; select ('[1, "2", null]'::jsonb)[-2]; select ('[1, "2", null]'::jsonb)[1]['a']; +select ('[1, "2", null]'::jsonb)[1].a; select ('[1, "2", null]'::jsonb)[1][0]; select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb)['b']; +select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb).b; select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb)['d']; +select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb).d; select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb)['d'][1]; +select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb).d[1]; select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb)['d']['a']; +select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb).d['a']; +select ('{"a": 1, "b": "c", "d": [1, 2, 3]}'::jsonb).d.a; +select ('{"a": 1, "b": "c", "d": {"a": [1, 2, 3]}}'::jsonb).d['a'][0]; +select ('{"a": 1, "b": "c", "d": {"a": [1, 2, 3]}}'::jsonb).d.a[0]; select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb)['a']['a1']; +select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb).a.a1; select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb)['a']['a1']['a2']; +select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb).a.a1.a2; select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb)['a']['a1']['a2']['a3']; +select ('{"a": {"a1": {"a2": "aaa"}}, "b": "bbb", "c": "ccc"}'::jsonb).a.a1.a2.a3; select ('{"a": ["a1", {"b1": ["aaa", "bbb", "ccc"]}], "b": "bb"}'::jsonb)['a'][1]['b1']; +select ('{"a": ["a1", {"b1": ["aaa", "bbb", "ccc"]}], "b": "bb"}'::jsonb).a[1].b1; select ('{"a": ["a1", {"b1": ["aaa", "bbb", "ccc"]}], "b": "bb"}'::jsonb)['a'][1]['b1'][2]; +select ('{"a": ["a1", {"b1": ["aaa", "bbb", "ccc"]}], "b": "bb"}'::jsonb).a[1].b1[2]; -- slices are not supported select ('{"a": 1}'::jsonb)['a':'b']; @@ -1608,3 +1627,99 @@ select ('true'::jsonb)::bool; select ('true'::jsonb).bool; select ('{"text": "hello"}'::jsonb)::text; select ('{"text": "hello"}'::jsonb).text; + +-- dot notation +CREATE TABLE test_jsonb_dot_notation AS +SELECT '{"a": [1, 2, {"b": "c"}, {"b": "d", "e": "f", "x": {"y": "yyy", "z": "zzz"}}], "b": [3, 4, {"b": "g", "x": {"y": "YYY", "z": "ZZZ"}}]}'::jsonb jb; + +SELECT (jb).a FROM test_jsonb_dot_notation; +SELECT (jb)."a" FROM test_jsonb_dot_notation; -- double quote should work +SELECT (jb).'a' FROM test_jsonb_dot_notation; -- single quote should not work +select (jb)[0].a from test_jsonb_dot_notation; -- returns same result as (jb).a +select (jb)[0]['a'] from test_jsonb_dot_notation; -- returns NULL +select (jb)[1].a from test_jsonb_dot_notation; -- returns NULL +SELECT (jb).b FROM test_jsonb_dot_notation; +SELECT (jb).c FROM test_jsonb_dot_notation; +SELECT (jb).a.b FROM test_jsonb_dot_notation; +SELECT (jb).a[2].b FROM test_jsonb_dot_notation; +SELECT (jb).a.x.y FROM test_jsonb_dot_notation; +SELECT (jb).b.x.z FROM test_jsonb_dot_notation; +SELECT (jb).a.b.c FROM test_jsonb_dot_notation; +SELECT ((jb).b)[:].x FROM test_jsonb_dot_notation t; -- fails +SELECT (jb).a.* FROM test_jsonb_dot_notation; -- fails + +-- assignment is not supported +UPDATE test_jsonb_dot_notation SET jb.a = '1'; +UPDATE test_jsonb_dot_notation SET (jb).a = '1'; + +-- explains should work +EXPLAIN (VERBOSE, COSTS OFF) SELECT (t.jb).a FROM test_jsonb_dot_notation t; +SELECT (t.jb).a FROM test_jsonb_dot_notation t; +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb).a[1] FROM test_jsonb_dot_notation; +SELECT (jb).a[1] FROM test_jsonb_dot_notation; + +-- views should work +CREATE VIEW test_jsonb_dot_notation_v1 AS SELECT (jb).a[3].x.y FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 + +-- mixed syntax +DROP VIEW test_jsonb_dot_notation_v1; + +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb)['a'].b FROM test_jsonb_dot_notation; +SELECT (jb)['a'].b FROM test_jsonb_dot_notation; -- returns an array due to lax mode +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb)['a'].b FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +SELECT * from test_jsonb_dot_notation_v1; +DROP VIEW public.test_jsonb_dot_notation_v1; + +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb).a['b'] FROM test_jsonb_dot_notation; +SELECT (jb).a['b'] FROM test_jsonb_dot_notation; -- returns NULL because ['b'] looks for strict match in an object +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb).a['b'] FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +SELECT * from test_jsonb_dot_notation_v1; +DROP VIEW public.test_jsonb_dot_notation_v1; + +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb).b.x['z'] FROM test_jsonb_dot_notation; +SELECT (jb).b.x['z'] FROM test_jsonb_dot_notation; -- warnings +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb).b.x['z'] FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +SELECT * from test_jsonb_dot_notation_v1; +DROP VIEW public.test_jsonb_dot_notation_v1; + +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb)['b'].x['z'] FROM test_jsonb_dot_notation; +SELECT (jb)['b'].x['z'] FROM test_jsonb_dot_notation; -- warnings +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb)['b'].x['z'] FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +SELECT * from test_jsonb_dot_notation_v1; +DROP VIEW public.test_jsonb_dot_notation_v1; + +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb).b['x'].z FROM test_jsonb_dot_notation; +SELECT (jb).b['x'].z FROM test_jsonb_dot_notation; -- returns NULL +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb).b['x'].z FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +SELECT * from test_jsonb_dot_notation_v1; +DROP VIEW public.test_jsonb_dot_notation_v1; + +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb).b['x']['z'] FROM test_jsonb_dot_notation; +SELECT (jb).b['x']['z'] FROM test_jsonb_dot_notation; -- returns NULL +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb).b['x']['z'] FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +SELECT * from test_jsonb_dot_notation_v1; +DROP VIEW public.test_jsonb_dot_notation_v1; + +EXPLAIN (VERBOSE, COSTS OFF) SELECT (jb)['b']['x'].z FROM test_jsonb_dot_notation; +SELECT (jb)['b']['x'].z FROM test_jsonb_dot_notation; -- returns NULL +CREATE VIEW public.test_jsonb_dot_notation_v1 AS +SELECT (jb)['b']['x'].z FROM test_jsonb_dot_notation; +\sv test_jsonb_dot_notation_v1 +SELECT * from test_jsonb_dot_notation_v1; +DROP VIEW public.test_jsonb_dot_notation_v1; + +-- clean up +DROP TABLE test_jsonb_dot_notation; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3c80d49b67e..965cac21d84 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -805,6 +805,7 @@ FdwRoutine FetchDirection FetchDirectionKeywords FetchStmt +FieldAccessorExpr FieldSelect FieldStore File -- 2.39.5 (Apple Git-154)