From 0a02e2ae0236103e641f6570b8135b7ee8a83686 Mon Sep 17 00:00:00 2001 From: Alexander Korotkov Date: Mon, 1 Apr 2019 18:08:15 +0300 Subject: [PATCH] GIN support for @@ and @? jsonpath operators This commit makes existing GIN operator classes jsonb_ops and json_path_ops support "jsonb @@ jsonpath" and "jsonb @? jsonpath" operators. Basic idea is to extract statements of following form out of jsonpath. key1.key2. ... .keyN = const The rest of jsonpath is rechecked from heap. Catversion is bumped. Discussion: https://postgr.es/m/fcc6fc6a-b497-f39a-923d-aa34d0c588e8%402ndQuadrant.com Author: Nikita Glukhov, Alexander Korotkov Reviewed-by: Jonathan Katz, Pavel Stehule --- doc/src/sgml/gin.sgml | 4 + doc/src/sgml/json.sgml | 19 +- src/backend/utils/adt/jsonb_gin.c | 950 +++++++++++++++++++++-- src/include/catalog/catversion.h | 2 +- src/include/catalog/pg_amop.dat | 12 + src/include/utils/jsonb.h | 3 + src/include/utils/jsonpath.h | 2 + src/test/regress/expected/jsonb.out | 453 +++++++++++ src/test/regress/expected/opr_sanity.out | 4 +- src/test/regress/sql/jsonb.sql | 79 ++ src/tools/pgindent/typedefs.list | 8 + 11 files changed, 1457 insertions(+), 79 deletions(-) diff --git a/doc/src/sgml/gin.sgml b/doc/src/sgml/gin.sgml index 488c3d8b45..91197b8383 100644 --- a/doc/src/sgml/gin.sgml +++ b/doc/src/sgml/gin.sgml @@ -102,6 +102,8 @@ ?& ?| @> + @? + @@ @@ -109,6 +111,8 @@ jsonb @> + @? + @@ diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml index 2eccf244cd..3e0e92a785 100644 --- a/doc/src/sgml/json.sgml +++ b/doc/src/sgml/json.sgml @@ -480,6 +480,22 @@ CREATE INDEX idxgintags ON api USING GIN ((jdoc -> 'tags')); (More information on expression indexes can be found in .) + + Also, GIN index supports @@ and @? + operators, which perform jsonpath matching. + +SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] == "qui"'; + + +SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] ? (@ == "qui")'; + + GIN index extracts statements of following form out of + jsonpath: accessors_chain = const. + Accessors chain may consist of .key, + [*] and [index] accessors. + jsonb_ops additionally supports .* + and .** statements. + Another approach to querying is to exploit containment, for example: @@ -498,7 +514,8 @@ SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @> '{"tags": ["qu Although the jsonb_path_ops operator class supports - only queries with the @> operator, it has notable + only queries with the @>, @@ + and @? operators, it has notable performance advantages over the default operator class jsonb_ops. A jsonb_path_ops index is usually much smaller than a jsonb_ops diff --git a/src/backend/utils/adt/jsonb_gin.c b/src/backend/utils/adt/jsonb_gin.c index bae5287f70..a20e44b1fe 100644 --- a/src/backend/utils/adt/jsonb_gin.c +++ b/src/backend/utils/adt/jsonb_gin.c @@ -5,21 +5,69 @@ * * Copyright (c) 2014-2019, PostgreSQL Global Development Group * + * We provide two opclasses for jsonb indexing: jsonb_ops and jsonb_path_ops. + * For their description see json.sgml and comments in jsonb.h. + * + * The operators support, among the others, "jsonb @? jsonpath" and + * "jsonb @@ jsonpath". Expressions containing these operators are easily + * expressed through each other. + * + * jb @? 'path' <=> jb @@ 'EXISTS(path)' + * jb @@ 'expr' <=> jb @? '$ ? (expr)' + * + * Thus, we're going to consider only @@ operator, while regarding @? operator + * the same is true for jb @@ 'EXISTS(path)'. + * + * Result of jsonpath query extraction is a tree, which leaf nodes are index + * entries and non-leaf nodes are AND/OR logical expressions. Basically we + * extract following statements out of jsonpath: + * + * 1) "accessors_chain = const", + * 2) "EXISTS(accessors_chain)". + * + * Accessors chain may consist of .key, [*] and [index] accessors. jsonb_ops + * additionally supports .* and .**. + * + * For now, both jsonb_ops and jsonb_path_ops supports only statements of + * the 1st find. jsonb_ops might also support statements of the 2nd kind, + * but given we have no statistics keys extracted from accessors chain + * are likely non-selective. Therefore, we choose to not confuse optimizer + * and skip statements of the 2nd kind altogether. In future versions that + * might be changed. + * + * In jsonb_ops statement of the 1st kind is split into expression of AND'ed + * keys and const. Sometimes const might be interpreted as both value or key + * in jsonb_ops. Then statement of 1st kind is decomposed into the expression + * below. + * + * key1 AND key2 AND ... AND keyN AND (const_as_value OR const_as_key) + * + * jsonb_path_ops transforms each statement of the 1st kind into single hash + * entry below. + * + * HASH(key1, key2, ... , keyN, const) + * + * Despite statements of the 2nd kind are not supported by both jsonb_ops and + * jsonb_path_ops, EXISTS(path) expressions might be still supported, + * when statements of 1st kind could be extracted out of their filters. * * IDENTIFICATION * src/backend/utils/adt/jsonb_gin.c * *------------------------------------------------------------------------- */ + #include "postgres.h" #include "access/gin.h" #include "access/stratnum.h" #include "catalog/pg_collation.h" #include "catalog/pg_type.h" +#include "miscadmin.h" #include "utils/builtins.h" #include "utils/hashutils.h" #include "utils/jsonb.h" +#include "utils/jsonpath.h" #include "utils/varlena.h" typedef struct PathHashStack @@ -28,9 +76,123 @@ typedef struct PathHashStack struct PathHashStack *parent; } PathHashStack; +/* Buffer for GIN entries */ +typedef struct GinEntries +{ + Datum *buf; + int count; + int allocated; +} GinEntries; + +typedef enum JsonPathGinNodeType +{ + JSP_GIN_OR, + JSP_GIN_AND, + JSP_GIN_ENTRY +} JsonPathGinNodeType; + +typedef struct JsonPathGinNode JsonPathGinNode; + +/* Node in jsonpath expression tree */ +struct JsonPathGinNode +{ + JsonPathGinNodeType type; + union + { + int nargs; /* valid for OR and AND nodes */ + int entryIndex; /* index in GinEntries array, valid for ENTRY + * nodes after entries output */ + Datum entryDatum; /* path hash or key name/scalar, valid for + * ENTRY nodes before entries output */ + } val; + JsonPathGinNode *args[FLEXIBLE_ARRAY_MEMBER]; /* valid for OR and AND + * nodes */ +}; + +/* + * jsonb_ops entry extracted from jsonpath item. Corresponding path item + * may be: '.key', '.*', '.**', '[index]' or '[*]'. + * Entry type is stored in 'type' field. + */ +typedef struct JsonPathGinPathItem +{ + struct JsonPathGinPathItem *parent; + Datum keyName; /* key name (for '.key' path item) or NULL */ + JsonPathItemType type; /* type of jsonpath item */ +} JsonPathGinPathItem; + +/* GIN representation of the extracted json path */ +typedef union JsonPathGinPath +{ + JsonPathGinPathItem *items; /* list of path items (jsonb_ops) */ + uint32 hash; /* hash of the path (jsonb_path_ops) */ +} JsonPathGinPath; + +typedef struct JsonPathGinContext JsonPathGinContext; + +/* Callback, which stores information about path item into JsonPathGinPath */ +typedef bool (*JsonPathGinAddPathItemFunc) (JsonPathGinPath *path, + JsonPathItem *jsp); + +/* + * Callback, which extracts set of nodes from statement of 1st kind + * (scalar != NULL) or statement of 2nd kind (scalar == NULL). + */ +typedef List *(*JsonPathGinExtractNodesFunc) (JsonPathGinContext *cxt, + JsonPathGinPath path, + JsonbValue *scalar, + List *nodes); + +/* Context for jsonpath entries extraction */ +struct JsonPathGinContext +{ + JsonPathGinAddPathItemFunc add_path_item; + JsonPathGinExtractNodesFunc extract_nodes; + bool lax; +}; + static Datum make_text_key(char flag, const char *str, int len); static Datum make_scalar_key(const JsonbValue *scalarVal, bool is_key); +static JsonPathGinNode *extract_jsp_bool_expr(JsonPathGinContext *cxt, + JsonPathGinPath path, JsonPathItem *jsp, bool not); + + +/* Initialize GinEntries struct */ +static void +init_gin_entries(GinEntries *entries, int preallocated) +{ + entries->allocated = preallocated; + entries->buf = preallocated ? palloc(sizeof(Datum) * preallocated) : NULL; + entries->count = 0; +} + +/* Add new entry to GinEntries */ +static int +add_gin_entry(GinEntries *entries, Datum entry) +{ + int id = entries->count; + + if (entries->count >= entries->allocated) + { + if (entries->allocated) + { + entries->allocated *= 2; + entries->buf = repalloc(entries->buf, + sizeof(Datum) * entries->allocated); + } + else + { + entries->allocated = 8; + entries->buf = palloc(sizeof(Datum) * entries->allocated); + } + } + + entries->buf[entries->count++] = entry; + + return id; +} + /* * * jsonb_ops GIN opclass support functions @@ -68,12 +230,11 @@ gin_extract_jsonb(PG_FUNCTION_ARGS) { Jsonb *jb = (Jsonb *) PG_GETARG_JSONB_P(0); int32 *nentries = (int32 *) PG_GETARG_POINTER(1); - int total = 2 * JB_ROOT_COUNT(jb); + int total = JB_ROOT_COUNT(jb); JsonbIterator *it; JsonbValue v; JsonbIteratorToken r; - int i = 0; - Datum *entries; + GinEntries entries; /* If the root level is empty, we certainly have no keys */ if (total == 0) @@ -83,30 +244,23 @@ gin_extract_jsonb(PG_FUNCTION_ARGS) } /* Otherwise, use 2 * root count as initial estimate of result size */ - entries = (Datum *) palloc(sizeof(Datum) * total); + init_gin_entries(&entries, 2 * total); it = JsonbIteratorInit(&jb->root); while ((r = JsonbIteratorNext(&it, &v, false)) != WJB_DONE) { - /* Since we recurse into the object, we might need more space */ - if (i >= total) - { - total *= 2; - entries = (Datum *) repalloc(entries, sizeof(Datum) * total); - } - switch (r) { case WJB_KEY: - entries[i++] = make_scalar_key(&v, true); + add_gin_entry(&entries, make_scalar_key(&v, true)); break; case WJB_ELEM: /* Pretend string array elements are keys, see jsonb.h */ - entries[i++] = make_scalar_key(&v, (v.type == jbvString)); + add_gin_entry(&entries, make_scalar_key(&v, v.type == jbvString)); break; case WJB_VALUE: - entries[i++] = make_scalar_key(&v, false); + add_gin_entry(&entries, make_scalar_key(&v, false)); break; default: /* we can ignore structural items */ @@ -114,9 +268,580 @@ gin_extract_jsonb(PG_FUNCTION_ARGS) } } - *nentries = i; + *nentries = entries.count; - PG_RETURN_POINTER(entries); + PG_RETURN_POINTER(entries.buf); +} + +/* Append JsonPathGinPathItem to JsonPathGinPath (jsonb_ops) */ +static bool +jsonb_ops__add_path_item(JsonPathGinPath *path, JsonPathItem *jsp) +{ + JsonPathGinPathItem *pentry; + Datum keyName; + + switch (jsp->type) + { + case jpiRoot: + path->items = NULL; /* reset path */ + return true; + + case jpiKey: + { + int len; + char *key = jspGetString(jsp, &len); + + keyName = make_text_key(JGINFLAG_KEY, key, len); + break; + } + + case jpiAny: + case jpiAnyKey: + case jpiAnyArray: + case jpiIndexArray: + keyName = PointerGetDatum(NULL); + break; + + default: + /* other path items like item methods are not supported */ + return false; + } + + pentry = palloc(sizeof(*pentry)); + + pentry->type = jsp->type; + pentry->keyName = keyName; + pentry->parent = path->items; + + path->items = pentry; + + return true; +} + +/* Combine existing path hash with next key hash (jsonb_path_ops) */ +static bool +jsonb_path_ops__add_path_item(JsonPathGinPath *path, JsonPathItem *jsp) +{ + switch (jsp->type) + { + case jpiRoot: + path->hash = 0; /* reset path hash */ + return true; + + case jpiKey: + { + JsonbValue jbv; + + jbv.type = jbvString; + jbv.val.string.val = jspGetString(jsp, &jbv.val.string.len); + + JsonbHashScalarValue(&jbv, &path->hash); + return true; + } + + case jpiIndexArray: + case jpiAnyArray: + return true; /* path hash is unchanged */ + + default: + /* other items (wildcard paths, item methods) are not supported */ + return false; + } +} + +static JsonPathGinNode * +make_jsp_entry_node(Datum entry) +{ + JsonPathGinNode *node = palloc(offsetof(JsonPathGinNode, args)); + + node->type = JSP_GIN_ENTRY; + node->val.entryDatum = entry; + + return node; +} + +static JsonPathGinNode * +make_jsp_entry_node_scalar(JsonbValue *scalar, bool iskey) +{ + return make_jsp_entry_node(make_scalar_key(scalar, iskey)); +} + +static JsonPathGinNode * +make_jsp_expr_node(JsonPathGinNodeType type, int nargs) +{ + JsonPathGinNode *node = palloc(offsetof(JsonPathGinNode, args) + + sizeof(node->args[0]) * nargs); + + node->type = type; + node->val.nargs = nargs; + + return node; +} + +static JsonPathGinNode * +make_jsp_expr_node_args(JsonPathGinNodeType type, List *args) +{ + JsonPathGinNode *node = make_jsp_expr_node(type, list_length(args)); + ListCell *lc; + int i = 0; + + foreach(lc, args) + node->args[i++] = lfirst(lc); + + return node; +} + +static JsonPathGinNode * +make_jsp_expr_node_binary(JsonPathGinNodeType type, + JsonPathGinNode *arg1, JsonPathGinNode *arg2) +{ + JsonPathGinNode *node = make_jsp_expr_node(type, 2); + + node->args[0] = arg1; + node->args[1] = arg2; + + return node; +} + +/* Append a list of nodes from the jsonpath (jsonb_ops). */ +static List * +jsonb_ops__extract_nodes(JsonPathGinContext *cxt, JsonPathGinPath path, + JsonbValue *scalar, List *nodes) +{ + JsonPathGinPathItem *pentry; + + if (scalar) + { + JsonPathGinNode *node; + + /* + * Append path entry nodes only if scalar is provided. See header + * comment for details. + */ + for (pentry = path.items; pentry; pentry = pentry->parent) + { + if (pentry->type == jpiKey) /* only keys are indexed */ + nodes = lappend(nodes, make_jsp_entry_node(pentry->keyName)); + } + + /* Append scalar node for equality queries. */ + if (scalar->type == jbvString) + { + JsonPathGinPathItem *last = path.items; + GinTernaryValue key_entry; + + /* + * Assuming that jsonb_ops interprets string array elements as + * keys, we may extract key or non-key entry or even both. In the + * latter case we create OR-node. It is possible in lax mode + * where arrays are automatically unwrapped, or in strict mode for + * jpiAny items. + */ + + if (cxt->lax) + key_entry = GIN_MAYBE; + else if (!last) /* root ($) */ + key_entry = GIN_FALSE; + else if (last->type == jpiAnyArray || last->type == jpiIndexArray) + key_entry = GIN_TRUE; + else if (last->type == jpiAny) + key_entry = GIN_MAYBE; + else + key_entry = GIN_FALSE; + + if (key_entry == GIN_MAYBE) + { + JsonPathGinNode *n1 = make_jsp_entry_node_scalar(scalar, true); + JsonPathGinNode *n2 = make_jsp_entry_node_scalar(scalar, false); + + node = make_jsp_expr_node_binary(JSP_GIN_OR, n1, n2); + } + else + { + node = make_jsp_entry_node_scalar(scalar, + key_entry == GIN_TRUE); + } + } + else + { + node = make_jsp_entry_node_scalar(scalar, false); + } + + nodes = lappend(nodes, node); + } + + return nodes; +} + +/* Append a list of nodes from the jsonpath (jsonb_path_ops). */ +static List * +jsonb_path_ops__extract_nodes(JsonPathGinContext *cxt, JsonPathGinPath path, + JsonbValue *scalar, List *nodes) +{ + if (scalar) + { + /* append path hash node for equality queries */ + uint32 hash = path.hash; + + JsonbHashScalarValue(scalar, &hash); + + return lappend(nodes, + make_jsp_entry_node(UInt32GetDatum(hash))); + } + else + { + /* jsonb_path_ops doesn't support EXISTS queries => nothing to append */ + return nodes; + } +} + +/* + * Extract a list of expression nodes that need to be AND-ed by the caller. + * Extracted expression is 'path == scalar' if 'scalar' is non-NULL, and + * 'EXISTS(path)' otherwise. + */ +static List * +extract_jsp_path_expr_nodes(JsonPathGinContext *cxt, JsonPathGinPath path, + JsonPathItem *jsp, JsonbValue *scalar) +{ + JsonPathItem next; + List *nodes = NIL; + + for (;;) + { + switch (jsp->type) + { + case jpiCurrent: + break; + + case jpiFilter: + { + JsonPathItem arg; + JsonPathGinNode *filter; + + jspGetArg(jsp, &arg); + + filter = extract_jsp_bool_expr(cxt, path, &arg, false); + + if (filter) + nodes = lappend(nodes, filter); + + break; + } + + default: + if (!cxt->add_path_item(&path, jsp)) + + /* + * Path is not supported by the index opclass, return only + * the extracted filter nodes. + */ + return nodes; + break; + } + + if (!jspGetNext(jsp, &next)) + break; + + jsp = &next; + } + + /* + * Append nodes from the path expression itself to the already extracted + * list of filter nodes. + */ + return cxt->extract_nodes(cxt, path, scalar, nodes); +} + +/* + * Extract an expression node from one of following jsonpath path expressions: + * EXISTS(jsp) (when 'scalar' is NULL) + * jsp == scalar (when 'scalar' is not NULL). + * + * The current path (@) is passed in 'path'. + */ +static JsonPathGinNode * +extract_jsp_path_expr(JsonPathGinContext *cxt, JsonPathGinPath path, + JsonPathItem *jsp, JsonbValue *scalar) +{ + /* extract a list of nodes to be AND-ed */ + List *nodes = extract_jsp_path_expr_nodes(cxt, path, jsp, scalar); + + if (list_length(nodes) <= 0) + /* no nodes were extracted => full scan is needed for this path */ + return NULL; + + if (list_length(nodes) == 1) + return linitial(nodes); /* avoid extra AND-node */ + + /* construct AND-node for path with filters */ + return make_jsp_expr_node_args(JSP_GIN_AND, nodes); +} + +/* Recursively extract nodes from the boolean jsonpath expression. */ +static JsonPathGinNode * +extract_jsp_bool_expr(JsonPathGinContext *cxt, JsonPathGinPath path, + JsonPathItem *jsp, bool not) +{ + check_stack_depth(); + + switch (jsp->type) + { + case jpiAnd: /* expr && expr */ + case jpiOr: /* expr || expr */ + { + JsonPathItem arg; + JsonPathGinNode *larg; + JsonPathGinNode *rarg; + JsonPathGinNodeType type; + + jspGetLeftArg(jsp, &arg); + larg = extract_jsp_bool_expr(cxt, path, &arg, not); + + jspGetRightArg(jsp, &arg); + rarg = extract_jsp_bool_expr(cxt, path, &arg, not); + + if (!larg || !rarg) + { + if (jsp->type == jpiOr) + return NULL; + + return larg ? larg : rarg; + } + + type = not ^ (jsp->type == jpiAnd) ? JSP_GIN_AND : JSP_GIN_OR; + + return make_jsp_expr_node_binary(type, larg, rarg); + } + + case jpiNot: /* !expr */ + { + JsonPathItem arg; + + jspGetArg(jsp, &arg); + + /* extract child expression inverting 'not' flag */ + return extract_jsp_bool_expr(cxt, path, &arg, !not); + } + + case jpiExists: /* EXISTS(path) */ + { + JsonPathItem arg; + + if (not) + return NULL; /* NOT EXISTS is not supported */ + + jspGetArg(jsp, &arg); + + return extract_jsp_path_expr(cxt, path, &arg, NULL); + } + + case jpiNotEqual: + + /* + * 'not' == true case is not supported here because '!(path != + * scalar)' is not equivalent to 'path == scalar' in the general + * case because of sequence comparison semantics: 'path == scalar' + * === 'EXISTS (path, @ == scalar)', '!(path != scalar)' === + * 'FOR_ALL(path, @ == scalar)'. So, we should translate '!(path + * != scalar)' into GIN query 'path == scalar || EMPTY(path)', but + * 'EMPTY(path)' queries are not supported by the both jsonb + * opclasses. However in strict mode we could omit 'EMPTY(path)' + * part if the path can return exactly one item (it does not + * contain wildcard accessors or item methods like .keyvalue() + * etc.). + */ + return NULL; + + case jpiEqual: /* path == scalar */ + { + JsonPathItem left_item; + JsonPathItem right_item; + JsonPathItem *path_item; + JsonPathItem *scalar_item; + JsonbValue scalar; + + if (not) + return NULL; + + jspGetLeftArg(jsp, &left_item); + jspGetRightArg(jsp, &right_item); + + if (jspIsScalar(left_item.type)) + { + scalar_item = &left_item; + path_item = &right_item; + } + else if (jspIsScalar(right_item.type)) + { + scalar_item = &right_item; + path_item = &left_item; + } + else + return NULL; /* at least one operand should be a scalar */ + + switch (scalar_item->type) + { + case jpiNull: + scalar.type = jbvNull; + break; + case jpiBool: + scalar.type = jbvBool; + scalar.val.boolean = !!*scalar_item->content.value.data; + break; + case jpiNumeric: + scalar.type = jbvNumeric; + scalar.val.numeric = + (Numeric) scalar_item->content.value.data; + break; + case jpiString: + scalar.type = jbvString; + scalar.val.string.val = scalar_item->content.value.data; + scalar.val.string.len = + scalar_item->content.value.datalen; + break; + default: + elog(ERROR, "invalid scalar jsonpath item type: %d", + scalar_item->type); + return NULL; + } + + return extract_jsp_path_expr(cxt, path, path_item, &scalar); + } + + default: + return NULL; /* not a boolean expression */ + } +} + +/* Recursively emit all GIN entries found in the node tree */ +static void +emit_jsp_gin_entries(JsonPathGinNode *node, GinEntries *entries) +{ + check_stack_depth(); + + switch (node->type) + { + case JSP_GIN_ENTRY: + /* replace datum with its index in the array */ + node->val.entryIndex = add_gin_entry(entries, node->val.entryDatum); + break; + + case JSP_GIN_OR: + case JSP_GIN_AND: + { + int i; + + for (i = 0; i < node->val.nargs; i++) + emit_jsp_gin_entries(node->args[i], entries); + + break; + } + } +} + +/* + * Recursively extract GIN entries from jsonpath query. + * Root expression node is put into (*extra_data)[0]. + */ +static Datum * +extract_jsp_query(JsonPath *jp, StrategyNumber strat, bool pathOps, + int32 *nentries, Pointer **extra_data) +{ + JsonPathGinContext cxt; + JsonPathItem root; + JsonPathGinNode *node; + JsonPathGinPath path = {0}; + GinEntries entries = {0}; + + cxt.lax = (jp->header & JSONPATH_LAX) != 0; + + if (pathOps) + { + cxt.add_path_item = jsonb_path_ops__add_path_item; + cxt.extract_nodes = jsonb_path_ops__extract_nodes; + } + else + { + cxt.add_path_item = jsonb_ops__add_path_item; + cxt.extract_nodes = jsonb_ops__extract_nodes; + } + + jspInit(&root, jp); + + node = strat == JsonbJsonpathExistsStrategyNumber + ? extract_jsp_path_expr(&cxt, path, &root, NULL) + : extract_jsp_bool_expr(&cxt, path, &root, false); + + if (!node) + { + *nentries = 0; + return NULL; + } + + emit_jsp_gin_entries(node, &entries); + + *nentries = entries.count; + if (!*nentries) + return NULL; + + *extra_data = palloc0(sizeof(**extra_data) * entries.count); + **extra_data = (Pointer) node; + + return entries.buf; +} + +/* + * Recursively execute jsonpath expression. + * 'check' is a bool[] or a GinTernaryValue[] depending on 'ternary' flag. + */ +static GinTernaryValue +execute_jsp_gin_node(JsonPathGinNode *node, void *check, bool ternary) +{ + GinTernaryValue res; + GinTernaryValue v; + int i; + + switch (node->type) + { + case JSP_GIN_AND: + res = GIN_TRUE; + for (i = 0; i < node->val.nargs; i++) + { + v = execute_jsp_gin_node(node->args[i], check, ternary); + if (v == GIN_FALSE) + return GIN_FALSE; + else if (v == GIN_MAYBE) + res = GIN_MAYBE; + } + return res; + + case JSP_GIN_OR: + res = GIN_FALSE; + for (i = 0; i < node->val.nargs; i++) + { + v = execute_jsp_gin_node(node->args[i], check, ternary); + if (v == GIN_TRUE) + return GIN_TRUE; + else if (v == GIN_MAYBE) + res = GIN_MAYBE; + } + return res; + + case JSP_GIN_ENTRY: + { + int index = node->val.entryIndex; + + if (ternary) + return ((GinTernaryValue *) check)[index]; + else + return ((bool *) check)[index] ? GIN_TRUE : GIN_FALSE; + } + + default: + elog(ERROR, "invalid jsonpath gin node type: %d", node->type); + return GIN_FALSE; /* keep compiler quiet */ + } } Datum @@ -181,6 +906,17 @@ gin_extract_jsonb_query(PG_FUNCTION_ARGS) if (j == 0 && strategy == JsonbExistsAllStrategyNumber) *searchMode = GIN_SEARCH_MODE_ALL; } + else if (strategy == JsonbJsonpathPredicateStrategyNumber || + strategy == JsonbJsonpathExistsStrategyNumber) + { + JsonPath *jp = PG_GETARG_JSONPATH_P(0); + Pointer **extra_data = (Pointer **) PG_GETARG_POINTER(4); + + entries = extract_jsp_query(jp, strategy, false, nentries, extra_data); + + if (!entries) + *searchMode = GIN_SEARCH_MODE_ALL; + } else { elog(ERROR, "unrecognized strategy number: %d", strategy); @@ -199,7 +935,7 @@ gin_consistent_jsonb(PG_FUNCTION_ARGS) /* Jsonb *query = PG_GETARG_JSONB_P(2); */ int32 nkeys = PG_GETARG_INT32(3); - /* Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); */ + Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); bool *recheck = (bool *) PG_GETARG_POINTER(5); bool res = true; int32 i; @@ -256,6 +992,18 @@ gin_consistent_jsonb(PG_FUNCTION_ARGS) } } } + else if (strategy == JsonbJsonpathPredicateStrategyNumber || + strategy == JsonbJsonpathExistsStrategyNumber) + { + *recheck = true; + + if (nkeys > 0) + { + Assert(extra_data && extra_data[0]); + res = execute_jsp_gin_node((JsonPathGinNode *) extra_data[0], check, + false) != GIN_FALSE; + } + } else elog(ERROR, "unrecognized strategy number: %d", strategy); @@ -270,8 +1018,7 @@ gin_triconsistent_jsonb(PG_FUNCTION_ARGS) /* Jsonb *query = PG_GETARG_JSONB_P(2); */ int32 nkeys = PG_GETARG_INT32(3); - - /* Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); */ + Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); GinTernaryValue res = GIN_MAYBE; int32 i; @@ -308,6 +1055,20 @@ gin_triconsistent_jsonb(PG_FUNCTION_ARGS) } } } + else if (strategy == JsonbJsonpathPredicateStrategyNumber || + strategy == JsonbJsonpathExistsStrategyNumber) + { + if (nkeys > 0) + { + Assert(extra_data && extra_data[0]); + res = execute_jsp_gin_node((JsonPathGinNode *) extra_data[0], check, + true); + + /* Should always recheck the result */ + if (res == GIN_TRUE) + res = GIN_MAYBE; + } + } else elog(ERROR, "unrecognized strategy number: %d", strategy); @@ -331,14 +1092,13 @@ gin_extract_jsonb_path(PG_FUNCTION_ARGS) { Jsonb *jb = PG_GETARG_JSONB_P(0); int32 *nentries = (int32 *) PG_GETARG_POINTER(1); - int total = 2 * JB_ROOT_COUNT(jb); + int total = JB_ROOT_COUNT(jb); JsonbIterator *it; JsonbValue v; JsonbIteratorToken r; PathHashStack tail; PathHashStack *stack; - int i = 0; - Datum *entries; + GinEntries entries; /* If the root level is empty, we certainly have no keys */ if (total == 0) @@ -348,7 +1108,7 @@ gin_extract_jsonb_path(PG_FUNCTION_ARGS) } /* Otherwise, use 2 * root count as initial estimate of result size */ - entries = (Datum *) palloc(sizeof(Datum) * total); + init_gin_entries(&entries, 2 * total); /* We keep a stack of partial hashes corresponding to parent key levels */ tail.parent = NULL; @@ -361,13 +1121,6 @@ gin_extract_jsonb_path(PG_FUNCTION_ARGS) { PathHashStack *parent; - /* Since we recurse into the object, we might need more space */ - if (i >= total) - { - total *= 2; - entries = (Datum *) repalloc(entries, sizeof(Datum) * total); - } - switch (r) { case WJB_BEGIN_ARRAY: @@ -398,7 +1151,7 @@ gin_extract_jsonb_path(PG_FUNCTION_ARGS) /* mix the element or value's hash into the prepared hash */ JsonbHashScalarValue(&v, &stack->hash); /* and emit an index entry */ - entries[i++] = UInt32GetDatum(stack->hash); + add_gin_entry(&entries, UInt32GetDatum(stack->hash)); /* reset hash for next key, value, or sub-object */ stack->hash = stack->parent->hash; break; @@ -419,9 +1172,9 @@ gin_extract_jsonb_path(PG_FUNCTION_ARGS) } } - *nentries = i; + *nentries = entries.count; - PG_RETURN_POINTER(entries); + PG_RETURN_POINTER(entries.buf); } Datum @@ -432,18 +1185,34 @@ gin_extract_jsonb_query_path(PG_FUNCTION_ARGS) int32 *searchMode = (int32 *) PG_GETARG_POINTER(6); Datum *entries; - if (strategy != JsonbContainsStrategyNumber) + if (strategy == JsonbContainsStrategyNumber) + { + /* Query is a jsonb, so just apply gin_extract_jsonb_path ... */ + entries = (Datum *) + DatumGetPointer(DirectFunctionCall2(gin_extract_jsonb_path, + PG_GETARG_DATUM(0), + PointerGetDatum(nentries))); + + /* ... although "contains {}" requires a full index scan */ + if (*nentries == 0) + *searchMode = GIN_SEARCH_MODE_ALL; + } + else if (strategy == JsonbJsonpathPredicateStrategyNumber || + strategy == JsonbJsonpathExistsStrategyNumber) + { + JsonPath *jp = PG_GETARG_JSONPATH_P(0); + Pointer **extra_data = (Pointer **) PG_GETARG_POINTER(4); + + entries = extract_jsp_query(jp, strategy, true, nentries, extra_data); + + if (!entries) + *searchMode = GIN_SEARCH_MODE_ALL; + } + else + { elog(ERROR, "unrecognized strategy number: %d", strategy); - - /* Query is a jsonb, so just apply gin_extract_jsonb_path ... */ - entries = (Datum *) - DatumGetPointer(DirectFunctionCall2(gin_extract_jsonb_path, - PG_GETARG_DATUM(0), - PointerGetDatum(nentries))); - - /* ... although "contains {}" requires a full index scan */ - if (*nentries == 0) - *searchMode = GIN_SEARCH_MODE_ALL; + entries = NULL; + } PG_RETURN_POINTER(entries); } @@ -456,32 +1225,46 @@ gin_consistent_jsonb_path(PG_FUNCTION_ARGS) /* Jsonb *query = PG_GETARG_JSONB_P(2); */ int32 nkeys = PG_GETARG_INT32(3); - - /* Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); */ + Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); bool *recheck = (bool *) PG_GETARG_POINTER(5); bool res = true; int32 i; - if (strategy != JsonbContainsStrategyNumber) - elog(ERROR, "unrecognized strategy number: %d", strategy); - - /* - * jsonb_path_ops is necessarily lossy, not only because of hash - * collisions but also because it doesn't preserve complete information - * about the structure of the JSON object. Besides, there are some - * special rules around the containment of raw scalars in arrays that are - * not handled here. So we must always recheck a match. However, if not - * all of the keys are present, the tuple certainly doesn't match. - */ - *recheck = true; - for (i = 0; i < nkeys; i++) + if (strategy == JsonbContainsStrategyNumber) { - if (!check[i]) + /* + * jsonb_path_ops is necessarily lossy, not only because of hash + * collisions but also because it doesn't preserve complete + * information about the structure of the JSON object. Besides, there + * are some special rules around the containment of raw scalars in + * arrays that are not handled here. So we must always recheck a + * match. However, if not all of the keys are present, the tuple + * certainly doesn't match. + */ + *recheck = true; + for (i = 0; i < nkeys; i++) { - res = false; - break; + if (!check[i]) + { + res = false; + break; + } } } + else if (strategy == JsonbJsonpathPredicateStrategyNumber || + strategy == JsonbJsonpathExistsStrategyNumber) + { + *recheck = true; + + if (nkeys > 0) + { + Assert(extra_data && extra_data[0]); + res = execute_jsp_gin_node((JsonPathGinNode *) extra_data[0], check, + false) != GIN_FALSE; + } + } + else + elog(ERROR, "unrecognized strategy number: %d", strategy); PG_RETURN_BOOL(res); } @@ -494,27 +1277,42 @@ gin_triconsistent_jsonb_path(PG_FUNCTION_ARGS) /* Jsonb *query = PG_GETARG_JSONB_P(2); */ int32 nkeys = PG_GETARG_INT32(3); - - /* Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); */ + Pointer *extra_data = (Pointer *) PG_GETARG_POINTER(4); GinTernaryValue res = GIN_MAYBE; int32 i; - if (strategy != JsonbContainsStrategyNumber) - elog(ERROR, "unrecognized strategy number: %d", strategy); - - /* - * Note that we never return GIN_TRUE, only GIN_MAYBE or GIN_FALSE; this - * corresponds to always forcing recheck in the regular consistent - * function, for the reasons listed there. - */ - for (i = 0; i < nkeys; i++) + if (strategy == JsonbContainsStrategyNumber) { - if (check[i] == GIN_FALSE) + /* + * Note that we never return GIN_TRUE, only GIN_MAYBE or GIN_FALSE; + * this corresponds to always forcing recheck in the regular + * consistent function, for the reasons listed there. + */ + for (i = 0; i < nkeys; i++) { - res = GIN_FALSE; - break; + if (check[i] == GIN_FALSE) + { + res = GIN_FALSE; + break; + } } } + else if (strategy == JsonbJsonpathPredicateStrategyNumber || + strategy == JsonbJsonpathExistsStrategyNumber) + { + if (nkeys > 0) + { + Assert(extra_data && extra_data[0]); + res = execute_jsp_gin_node((JsonPathGinNode *) extra_data[0], check, + true); + + /* Should always recheck the result */ + if (res == GIN_TRUE) + res = GIN_MAYBE; + } + } + else + elog(ERROR, "unrecognized strategy number: %d", strategy); PG_RETURN_GIN_TERNARY_VALUE(res); } diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 6155c3234d..dfb94bfadf 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -53,6 +53,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 201903301 +#define CATALOG_VERSION_NO 201904011 #endif diff --git a/src/include/catalog/pg_amop.dat b/src/include/catalog/pg_amop.dat index 0ab95d8a24..cf63eb7d54 100644 --- a/src/include/catalog/pg_amop.dat +++ b/src/include/catalog/pg_amop.dat @@ -1468,11 +1468,23 @@ { amopfamily => 'gin/jsonb_ops', amoplefttype => 'jsonb', amoprighttype => '_text', amopstrategy => '11', amopopr => '?&(jsonb,_text)', amopmethod => 'gin' }, +{ amopfamily => 'gin/jsonb_ops', amoplefttype => 'jsonb', + amoprighttype => 'jsonpath', amopstrategy => '15', + amopopr => '@?(jsonb,jsonpath)', amopmethod => 'gin' }, +{ amopfamily => 'gin/jsonb_ops', amoplefttype => 'jsonb', + amoprighttype => 'jsonpath', amopstrategy => '16', + amopopr => '@@(jsonb,jsonpath)', amopmethod => 'gin' }, # GIN jsonb_path_ops { amopfamily => 'gin/jsonb_path_ops', amoplefttype => 'jsonb', amoprighttype => 'jsonb', amopstrategy => '7', amopopr => '@>(jsonb,jsonb)', amopmethod => 'gin' }, +{ amopfamily => 'gin/jsonb_path_ops', amoplefttype => 'jsonb', + amoprighttype => 'jsonpath', amopstrategy => '15', + amopopr => '@?(jsonb,jsonpath)', amopmethod => 'gin' }, +{ amopfamily => 'gin/jsonb_path_ops', amoplefttype => 'jsonb', + amoprighttype => 'jsonpath', amopstrategy => '16', + amopopr => '@@(jsonb,jsonpath)', amopmethod => 'gin' }, # SP-GiST range_ops { amopfamily => 'spgist/range_ops', amoplefttype => 'anyrange', diff --git a/src/include/utils/jsonb.h b/src/include/utils/jsonb.h index ec0355f13c..432331b3b9 100644 --- a/src/include/utils/jsonb.h +++ b/src/include/utils/jsonb.h @@ -34,6 +34,9 @@ typedef enum #define JsonbExistsStrategyNumber 9 #define JsonbExistsAnyStrategyNumber 10 #define JsonbExistsAllStrategyNumber 11 +#define JsonbJsonpathExistsStrategyNumber 15 +#define JsonbJsonpathPredicateStrategyNumber 16 + /* * In the standard jsonb_ops GIN opclass for jsonb, we choose to index both diff --git a/src/include/utils/jsonpath.h b/src/include/utils/jsonpath.h index 14f837e00d..ae8a995c7f 100644 --- a/src/include/utils/jsonpath.h +++ b/src/include/utils/jsonpath.h @@ -35,6 +35,8 @@ typedef struct #define PG_GETARG_JSONPATH_P_COPY(x) DatumGetJsonPathPCopy(PG_GETARG_DATUM(x)) #define PG_RETURN_JSONPATH_P(p) PG_RETURN_POINTER(p) +#define jspIsScalar(type) ((type) >= jpiNull && (type) <= jpiBool) + /* * All node's type of jsonpath expression */ diff --git a/src/test/regress/expected/jsonb.out b/src/test/regress/expected/jsonb.out index c251eb70be..1018303006 100644 --- a/src/test/regress/expected/jsonb.out +++ b/src/test/regress/expected/jsonb.out @@ -2731,6 +2731,114 @@ SELECT count(*) FROM testjsonb WHERE j ?& ARRAY['public','disabled']; 42 (1 row) +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == null'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '"CC" == $.wait'; + count +------- + 15 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == "CC" && true == $.public'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25.0'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($)'; + count +------- + 1012 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public)'; + count +------- + 194 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.bar)'; + count +------- + 0 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public) || exists($.disabled)'; + count +------- + 337 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public) && exists($.disabled)'; + count +------- + 42 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? (@ == null)'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? ("CC" == @)'; + count +------- + 15 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.wait == "CC" && true == @.public)'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.age ? (@ == 25)'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.age == 25.0)'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$'; + count +------- + 1012 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.public'; + count +------- + 194 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.bar'; + count +------- + 0 +(1 row) + CREATE INDEX jidx ON testjsonb USING gin (j); SET enable_seqscan = off; SELECT count(*) FROM testjsonb WHERE j @> '{"wait":null}'; @@ -2806,6 +2914,196 @@ SELECT count(*) FROM testjsonb WHERE j ?& ARRAY['public','disabled']; 42 (1 row) +EXPLAIN (COSTS OFF) +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == null'; + QUERY PLAN +----------------------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on testjsonb + Recheck Cond: (j @@ '($."wait" == null)'::jsonpath) + -> Bitmap Index Scan on jidx + Index Cond: (j @@ '($."wait" == null)'::jsonpath) +(5 rows) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == null'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($ ? (@.wait == null))'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.wait ? (@ == null))'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '"CC" == $.wait'; + count +------- + 15 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == "CC" && true == $.public'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25.0'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.array[*] == "foo"'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.array[*] == "bar"'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($ ? (@.array[*] == "bar"))'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.array ? (@[*] == "bar"))'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.array[*] ? (@ == "bar"))'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($)'; + count +------- + 1012 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public)'; + count +------- + 194 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.bar)'; + count +------- + 0 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public) || exists($.disabled)'; + count +------- + 337 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public) && exists($.disabled)'; + count +------- + 42 +(1 row) + +EXPLAIN (COSTS OFF) +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? (@ == null)'; + QUERY PLAN +------------------------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on testjsonb + Recheck Cond: (j @? '$."wait"?(@ == null)'::jsonpath) + -> Bitmap Index Scan on jidx + Index Cond: (j @? '$."wait"?(@ == null)'::jsonpath) +(5 rows) + +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? (@ == null)'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? ("CC" == @)'; + count +------- + 15 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.wait == "CC" && true == @.public)'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.age ? (@ == 25)'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.age == 25.0)'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.array[*] == "bar")'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.array ? (@[*] == "bar")'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.array[*] ? (@ == "bar")'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$'; + count +------- + 1012 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.public'; + count +------- + 194 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.bar'; + count +------- + 0 +(1 row) + -- array exists - array elements should behave as keys (for GIN index scans too) CREATE INDEX jidx_array ON testjsonb USING gin((j->'array')); SELECT count(*) from testjsonb WHERE j->'array' ? 'bar'; @@ -2956,6 +3254,161 @@ SELECT count(*) FROM testjsonb WHERE j @> '{}'; 1012 (1 row) +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == null'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($ ? (@.wait == null))'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.wait ? (@ == null))'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '"CC" == $.wait'; + count +------- + 15 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == "CC" && true == $.public'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25.0'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.array[*] == "foo"'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ '$.array[*] == "bar"'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($ ? (@.array[*] == "bar"))'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.array ? (@[*] == "bar"))'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.array[*] ? (@ == "bar"))'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($)'; + count +------- + 1012 +(1 row) + +EXPLAIN (COSTS OFF) +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? (@ == null)'; + QUERY PLAN +------------------------------------------------------------------- + Aggregate + -> Bitmap Heap Scan on testjsonb + Recheck Cond: (j @? '$."wait"?(@ == null)'::jsonpath) + -> Bitmap Index Scan on jidx + Index Cond: (j @? '$."wait"?(@ == null)'::jsonpath) +(5 rows) + +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? (@ == null)'; + count +------- + 1 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? ("CC" == @)'; + count +------- + 15 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.wait == "CC" && true == @.public)'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.age ? (@ == 25)'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.age == 25.0)'; + count +------- + 2 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.array[*] == "bar")'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.array ? (@[*] == "bar")'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.array[*] ? (@ == "bar")'; + count +------- + 3 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$'; + count +------- + 1012 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.public'; + count +------- + 194 +(1 row) + +SELECT count(*) FROM testjsonb WHERE j @? '$.bar'; + count +------- + 0 +(1 row) + RESET enable_seqscan; DROP INDEX jidx; -- nested tests diff --git a/src/test/regress/expected/opr_sanity.out b/src/test/regress/expected/opr_sanity.out index 49fc313af0..85af36ee5b 100644 --- a/src/test/regress/expected/opr_sanity.out +++ b/src/test/regress/expected/opr_sanity.out @@ -1920,6 +1920,8 @@ ORDER BY 1, 2, 3; 2742 | 9 | ? 2742 | 10 | ?| 2742 | 11 | ?& + 2742 | 15 | @? + 2742 | 16 | @@ 3580 | 1 | < 3580 | 1 | << 3580 | 2 | &< @@ -1985,7 +1987,7 @@ ORDER BY 1, 2, 3; 4000 | 26 | >> 4000 | 27 | >>= 4000 | 28 | ^@ -(123 rows) +(125 rows) -- Check that all opclass search operators have selectivity estimators. -- This is not absolutely required, but it seems a reasonable thing diff --git a/src/test/regress/sql/jsonb.sql b/src/test/regress/sql/jsonb.sql index 1bf32076e3..c1a7880792 100644 --- a/src/test/regress/sql/jsonb.sql +++ b/src/test/regress/sql/jsonb.sql @@ -740,6 +740,24 @@ SELECT count(*) FROM testjsonb WHERE j ? 'public'; SELECT count(*) FROM testjsonb WHERE j ? 'bar'; SELECT count(*) FROM testjsonb WHERE j ?| ARRAY['public','disabled']; SELECT count(*) FROM testjsonb WHERE j ?& ARRAY['public','disabled']; +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == null'; +SELECT count(*) FROM testjsonb WHERE j @@ '"CC" == $.wait'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == "CC" && true == $.public'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25.0'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($)'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public)'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.bar)'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public) || exists($.disabled)'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public) && exists($.disabled)'; +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? (@ == null)'; +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? ("CC" == @)'; +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.wait == "CC" && true == @.public)'; +SELECT count(*) FROM testjsonb WHERE j @? '$.age ? (@ == 25)'; +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.age == 25.0)'; +SELECT count(*) FROM testjsonb WHERE j @? '$'; +SELECT count(*) FROM testjsonb WHERE j @? '$.public'; +SELECT count(*) FROM testjsonb WHERE j @? '$.bar'; CREATE INDEX jidx ON testjsonb USING gin (j); SET enable_seqscan = off; @@ -758,6 +776,39 @@ SELECT count(*) FROM testjsonb WHERE j ? 'bar'; SELECT count(*) FROM testjsonb WHERE j ?| ARRAY['public','disabled']; SELECT count(*) FROM testjsonb WHERE j ?& ARRAY['public','disabled']; +EXPLAIN (COSTS OFF) +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == null'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == null'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($ ? (@.wait == null))'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.wait ? (@ == null))'; +SELECT count(*) FROM testjsonb WHERE j @@ '"CC" == $.wait'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == "CC" && true == $.public'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25.0'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.array[*] == "foo"'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.array[*] == "bar"'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($ ? (@.array[*] == "bar"))'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.array ? (@[*] == "bar"))'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.array[*] ? (@ == "bar"))'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($)'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public)'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.bar)'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public) || exists($.disabled)'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.public) && exists($.disabled)'; +EXPLAIN (COSTS OFF) +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? (@ == null)'; +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? (@ == null)'; +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? ("CC" == @)'; +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.wait == "CC" && true == @.public)'; +SELECT count(*) FROM testjsonb WHERE j @? '$.age ? (@ == 25)'; +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.age == 25.0)'; +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.array[*] == "bar")'; +SELECT count(*) FROM testjsonb WHERE j @? '$.array ? (@[*] == "bar")'; +SELECT count(*) FROM testjsonb WHERE j @? '$.array[*] ? (@ == "bar")'; +SELECT count(*) FROM testjsonb WHERE j @? '$'; +SELECT count(*) FROM testjsonb WHERE j @? '$.public'; +SELECT count(*) FROM testjsonb WHERE j @? '$.bar'; + -- array exists - array elements should behave as keys (for GIN index scans too) CREATE INDEX jidx_array ON testjsonb USING gin((j->'array')); SELECT count(*) from testjsonb WHERE j->'array' ? 'bar'; @@ -807,6 +858,34 @@ SELECT count(*) FROM testjsonb WHERE j @> '{"age":25.0}'; -- exercise GIN_SEARCH_MODE_ALL SELECT count(*) FROM testjsonb WHERE j @> '{}'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == null'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($ ? (@.wait == null))'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.wait ? (@ == null))'; +SELECT count(*) FROM testjsonb WHERE j @@ '"CC" == $.wait'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.wait == "CC" && true == $.public'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.age == 25.0'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.array[*] == "foo"'; +SELECT count(*) FROM testjsonb WHERE j @@ '$.array[*] == "bar"'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($ ? (@.array[*] == "bar"))'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.array ? (@[*] == "bar"))'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($.array[*] ? (@ == "bar"))'; +SELECT count(*) FROM testjsonb WHERE j @@ 'exists($)'; + +EXPLAIN (COSTS OFF) +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? (@ == null)'; +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? (@ == null)'; +SELECT count(*) FROM testjsonb WHERE j @? '$.wait ? ("CC" == @)'; +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.wait == "CC" && true == @.public)'; +SELECT count(*) FROM testjsonb WHERE j @? '$.age ? (@ == 25)'; +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.age == 25.0)'; +SELECT count(*) FROM testjsonb WHERE j @? '$ ? (@.array[*] == "bar")'; +SELECT count(*) FROM testjsonb WHERE j @? '$.array ? (@[*] == "bar")'; +SELECT count(*) FROM testjsonb WHERE j @? '$.array[*] ? (@ == "bar")'; +SELECT count(*) FROM testjsonb WHERE j @? '$'; +SELECT count(*) FROM testjsonb WHERE j @? '$.public'; +SELECT count(*) FROM testjsonb WHERE j @? '$.bar'; + RESET enable_seqscan; DROP INDEX jidx; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 4816b5b271..f31929664a 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -867,6 +867,7 @@ GinBtreeEntryInsertData GinBtreeStack GinBuildState GinChkVal +GinEntries GinEntryAccumulator GinIndexStat GinMetaPageData @@ -1106,6 +1107,13 @@ JsonPath JsonPathBool JsonPathExecContext JsonPathExecResult +JsonPathGinAddPathItemFunc +JsonPathGinContext +JsonPathGinExtractNodesFunc +JsonPathGinNode +JsonPathGinNodeType +JsonPathGinPath +JsonPathGinPathItem JsonPathItem JsonPathItemType JsonPathParseItem