commit 0f84ee89fd9bfd0a6d208e62e7c719ce97cd8651
Author: Alexander Korotkov <akorotkov@postgresql.org>
Date:   Fri Mar 8 23:47:07 2019 +0300

    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.
    
    Discussion: https://postgr.es/m/fcc6fc6a-b497-f39a-923d-aa34d0c588e8%402ndQuadrant.com
    Author: Nikita Glukhov, Alexander Korotkov
    Reviewed-by: Jonathan Katz, Pavel Stehule

diff --git a/doc/src/sgml/gin.sgml b/doc/src/sgml/gin.sgml
index 488c3d8b45d..91197b83835 100644
--- a/doc/src/sgml/gin.sgml
+++ b/doc/src/sgml/gin.sgml
@@ -102,6 +102,8 @@
        <literal>?&amp;</literal>
        <literal>?|</literal>
        <literal>@&gt;</literal>
+       <literal>@?</literal>
+       <literal>@@</literal>
       </entry>
      </row>
      <row>
@@ -109,6 +111,8 @@
       <entry><type>jsonb</type></entry>
       <entry>
        <literal>@&gt;</literal>
+       <literal>@?</literal>
+       <literal>@@</literal>
       </entry>
      </row>
      <row>
diff --git a/doc/src/sgml/json.sgml b/doc/src/sgml/json.sgml
index 2eccf244cd3..3e0e92a7850 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 -&gt; 'tags'));
     (More information on expression indexes can be found in <xref
     linkend="indexes-expressional"/>.)
   </para>
+  <para>
+    Also, GIN index supports <literal>@@</literal> and <literal>@?</literal>
+    operators, which perform <literal>jsonpath</literal> matching.
+<programlisting>
+SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] == "qui"';
+</programlisting>
+<programlisting>
+SELECT jdoc->'guid', jdoc->'name' FROM api WHERE jdoc @@ '$.tags[*] ? (@ == "qui")';
+</programlisting>
+    GIN index extracts statements of following form out of
+    <literal>jsonpath</literal>: <literal>accessors_chain = const</literal>.
+    Accessors chain may consist of <literal>.key</literal>,
+    <literal>[*]</literal> and <literal>[index]</literal> accessors.
+    <literal>jsonb_ops</literal> additionally supports <literal>.*</literal>
+    and <literal>.**</literal> statements.
+  </para>
   <para>
     Another approach to querying is to exploit containment, for example:
 <programlisting>
@@ -498,7 +514,8 @@ SELECT jdoc-&gt;'guid', jdoc-&gt;'name' FROM api WHERE jdoc @&gt; '{"tags": ["qu
 
   <para>
     Although the <literal>jsonb_path_ops</literal> operator class supports
-    only queries with the <literal>@&gt;</literal> operator, it has notable
+    only queries with the <literal>@&gt;</literal>, <literal>@@</literal>
+    and <literal>@?</literal> operators, it has notable
     performance advantages over the default operator
     class <literal>jsonb_ops</literal>.  A <literal>jsonb_path_ops</literal>
     index is usually much smaller than a <literal>jsonb_ops</literal>
diff --git a/src/backend/utils/adt/jsonb_gin.c b/src/backend/utils/adt/jsonb_gin.c
index bae5287f705..a20e44b1fea 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)
-		elog(ERROR, "unrecognized strategy number: %d", strategy);
+	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)));
 
-	/* 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);
 
-	/* ... although "contains {}" requires a full index scan */
-	if (*nentries == 0)
-		*searchMode = GIN_SEARCH_MODE_ALL;
+		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);
+		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/pg_amop.dat b/src/include/catalog/pg_amop.dat
index 0ab95d8a24d..cf63eb7d546 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 ec0355f13c2..432331b3b9e 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 14f837e00d5..ae8a995c7f8 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 c251eb70be9..10183030068 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 49fc313af06..85af36ee5bc 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 1bf32076e30..c1a7880792d 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 4816b5b271d..f31929664ac 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
