/*-------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation.  All rights reserved.
 *
 * src/query/query_operator.c
 *
 * Implementation of BSON query to operator conversion.
 *
 *-------------------------------------------------------------------------
 */
#include <postgres.h>
#include <fmgr.h>
#include <miscadmin.h>
#include <catalog/namespace.h>
#include <catalog/pg_collation.h>
#include <catalog/pg_operator.h>
#include <executor/executor.h>
#include <optimizer/optimizer.h>
#include <nodes/makefuncs.h>
#include <nodes/nodes.h>
#include <nodes/nodeFuncs.h>
#include <utils/builtins.h>
#include <utils/typcache.h>
#include <utils/lsyscache.h>
#include <utils/syscache.h>
#include <utils/timestamp.h>
#include <utils/array.h>
#include <parser/parse_coerce.h>
#include <parser/parsetree.h>
#include <parser/parse_clause.h>
#include <catalog/pg_type.h>
#include <funcapi.h>
#include <lib/stringinfo.h>
#include <metadata/metadata_cache.h>
#include <math.h>
#include <nodes/supportnodes.h>

#include "io/bson_core.h"
#include "query/bson_compare.h"
#include "aggregation/bson_query.h"
#include "types/decimal128.h"
#include "utils/documentdb_errors.h"
#include "commands/defrem.h"
#include "opclass/bson_gin_index_types_core.h"
#include "geospatial/bson_geospatial_common.h"
#include "geospatial/bson_geospatial_geonear.h"
#include "geospatial/bson_geospatial_shape_operators.h"
#include "metadata/collection.h"
#include "planner/documentdb_planner.h"
#include "query/query_operator.h"
#include "sharding/sharding.h"
#include "utils/rel.h"
#include "opclass/bson_text_gin.h"
#include "utils/feature_counter.h"
#include "vector/vector_common.h"
#include "vector/vector_utilities.h"
#include "types/pcre_regex.h"
#include "query/bson_dollar_operators.h"
#include "commands/commands_common.h"
#include "utils/version_utils.h"
#include "utils/hashset_utils.h"
#include "utils/list_utils.h"
#include "collation/collation.h"
#include "jsonschema/bson_json_schema_tree.h"


/*
 * ReplaceBsonQueryOperatorsContext is passed down while looking for
 * <document> @@ <query> expressions in ReplaceBsonQueryOperatorsMutator.
 *
 * We include the query to interpret Vars using currentQuery->rtable,
 * and the parameter list in case the query is defined as a parameter.
 */
typedef struct ReplaceBsonQueryOperatorsContext
{
	/* current query in which we are looking for an OpExpr */
	Query *currentQuery;

	/* parameter values of the current execution */
	ParamListInfo boundParams;

	/* List of sort clauses, if any query operator adds them
	 * e.g. $near, $nearSphere etc, will be NULL for most of
	 * the query operators.
	 *
	 * Please note that the `ressortgroupref` is needed to be updated
	 * based on the overall query structure later
	 */
	List *sortClauses;

	/* List of Target entries for these sort clauses
	 *
	 * Please note that the `resno` is needed to be updated
	 * based on the overall query structure later
	 */
	List *targetEntries;
} ReplaceBsonQueryOperatorsContext;

/* Context passed as an argument to CreateIdFilterForQuery */
typedef struct IdFilterWalkerContext
{
	/* The id filter qualifiers extracted from the query operator quals */
	List *idQuals;

	/* The index into the RTE where the collection is. */
	Index collectionVarno;

	/* Whether the _id filter is of a type where collation rules apply, e.g., UTF8 */
	bool isCollationAware;

	/* Whether or not the _id filter is an equality (point read) */
	bool isPointReadQuery;
} IdFilterWalkerContext;


typedef struct MatchNamespaceFiltersContext
{
	/* The list of database name filters found in plain path "ns.db"
	 * e.g. { "ns.db": "testDatabase" } / { "ns.db": { $in: [ "testDatabase1", "testDatabase2" ] } }
	 * { "ns.db": { $eq: "testDatabase3" } }
	 */

	List *nsDbPathFilters;

	/* The list of database name filters found in plain path "ns.coll"
	 * e.g. { "ns.coll": "testCollection" } / { "ns.coll": { $in: [ "testCollection1", "testCollection2" ] } }
	 * { "ns.coll": { $eq: "testCollection3" } }
	 */
	List *nsCollPathFilters;

	/*
	 * The list of pair of <database> <collection> name filters found in ns field
	 * e.g. { "ns" : { $in: [
	 *  { "db" : "db1", "coll": "collection1" },
	 *  { "db": "db2", "coll": "collection2" }]}}
	 */
	List *nsValueFilters;

	bool multipleNSFilters;

	bool isInvalidNSFilters;
} MatchNamespaceFiltersContext;

extern bool EnableObjectIdFuncExprConversion;
extern bool EnablePartialFilterEvalOnPlanner;

/* --------------------------------------------------------- */
/* Forward declaration */
/* --------------------------------------------------------- */
static Const * MakeBsonConst(pgbson *pgbson);
static Node * ReplaceBsonQueryOperatorsMutator(Node *node,
											   ReplaceBsonQueryOperatorsContext *context);
static Expr * ExpandBsonQueryOperator(OpExpr *queryOpExpr, Node *queryNode,
									  Query *currentQuery, ParamListInfo boundParams,
									  List **targetEntries, List **sortClauses,
									  const char *collationString, Const *variableSpec);
static Expr * CreateBoolExprFromLogicalExpression(bson_iter_t *queryDocIterator,
												  BsonQueryOperatorContext *c,
												  const char *traversedPath);
static List * CreateQualsFromLogicalExpressionArrayIterator(bson_iter_t *arrayIterator,
															BsonQueryOperatorContext *c,
															const char *traversedPath);
static Expr * CreateOpExprFromComparisonExpression(bson_iter_t *queryDocIterator,
												   BsonQueryOperatorContext *context,
												   const char *traversedPath,
												   const char *currentKey);
static Expr * CreateOpExprFromOperatorDocIterator(const char *path,
												  bson_iter_t *operatorDocIterator,
												  BsonQueryOperatorContext *context);
static Expr * CreateOpExprFromOperatorDocIteratorCore(bson_iter_t *operatorDocIterator,
													  BsonQueryOperatorContext *context,
													  const char *path,
													  bool *regexFound,
													  bson_value_t **options);
static Expr * CreateFuncExprForQueryOperator(BsonQueryOperatorContext *context, const
											 char *path,
											 const MongoQueryOperator *operator,
											 const bson_value_t *value);
static Const * CreateConstFromBsonValue(const char *path, const bson_value_t *value, const
										char *collationString);
static Expr * CreateExprForDollarAll(const char *path,
									 bson_iter_t *operatorDocIterator,
									 BsonQueryOperatorContext *context,
									 const MongoQueryOperator *operator);
static Expr * ExpandExprForDollarAll(const char *path,
									 bson_iter_t *operatorDocIterator,
									 BsonQueryOperatorContext *context,
									 const MongoQueryOperator *operator);
static MongoCollection * GetCollectionByDocumentVar(Expr *documentExpr,
													Query *currentQuery,
													Index *collectionVarno,
													ParamListInfo boundParams,
													bool *isCollectionBasedRTE);
static MongoCollection * GetCollectionForRTE(RangeTblEntry *rte, ParamListInfo
											 boundParams);
static Expr * CreateExprForDollarRegex(bson_iter_t *currIter, bson_value_t **options,
									   BsonQueryOperatorContext *context,
									   const MongoQueryOperator *operator,
									   const char *path);
static Expr * CreateFuncExprForRegexOperator(const bson_value_t *options,
											 const bson_value_t *regexBsonValue,
											 BsonQueryOperatorContext *context,
											 const MongoQueryOperator *operator,
											 const char *path);
static Expr * CreateExprForDollarMod(bson_iter_t *currIter,
									 BsonQueryOperatorContext *context,
									 const MongoQueryOperator *operator,
									 const char *path);
static Expr * CreateExprForBitwiseQueryOperators(bson_iter_t *operatorDocIterator,
												 BsonQueryOperatorContext *context,
												 const MongoQueryOperator *operator,
												 const char *path);
static bool SortAndWriteInt32BsonTypeArray(const bson_value_t *array,
										   pgbson_writer *writer,
										   const char *opName);
static List * CreateQualsFromQueryDocIteratorInternal(bson_iter_t *queryDocIterator,
													  BsonQueryOperatorContext *context,
													  const char *traversedPath);
static Expr * CreateQualForBsonValueExpressionCore(const bson_value_t *expression,
												   BsonQueryOperatorContext *context,
												   const char *traversedPath,
												   const char *basePath);
static Expr * TryProcessOrIntoDollarIn(BsonQueryOperatorContext *context,
									   List *orQuals);
static Expr * TryOptimizeDollarOrExpr(BsonQueryOperatorContext *context,
									  List *orQuals);
static Expr * ParseBsonValueForNearAndCreateOpExpr(bson_iter_t *operatorDocIterator,
												   BsonQueryOperatorContext *context,
												   const char *path, const
												   char *mongoOperatorName);
static void ValidateOptionsArgument(const bson_value_t *argBsonValue);
static void ValidateRegexArgument(const bson_value_t *argBsonValue);
static void EnsureValidTypeNameForDollarType(const char *typeName);
static void EnsureValidTypeCodeForDollarType(int64_t typeCode);

static Expr * WithIndexSupportExpression(Expr *docExpr, Expr *geoOperatorExpr,
										 const char *path, bool isSpherical);
static Expr * TryOptimizeNotInnerExpr(Expr *innerExpr, BsonQueryOperatorContext *context);
static pgbson * FindNamespaceFiltersInMatchSpecCore(bson_iter_t *specIter,
													MatchNamespaceFiltersContext *context);
static bool AddNamespaceFilterToList(const bson_value_t *value, List **list);
static pgbson * AddNamespaceValueFiltersToList(bson_iter_t *valueItr,
											   List **nsValueFilter,
											   bool *isDbFound, bool *isCollFound);
static pgbson * HandleEqualityNamespaceFilter(bson_iter_t *iter, bool *nsInlined,
											  MatchNamespaceFiltersContext *context);

/* Return true if double value can be represented as fixed integer
 * e.g., 10.023 -> this number can not be represented as fixed integer so return false
 *       10.000 -> this number can be represented as fixed integer (10) so return true
 */
static inline bool
IsDoubleAFixedInteger(double value)
{
	return floor(value) == value;
}


static inline void
pg_attribute_noreturn()
ThrowInvalidRegexOptions(char c)
{
	ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION51108), errmsg(
						"Invalid flag detected within the specified regex options %c", c),
					errdetail_log(
						"Invalid flag detected within the specified regex options %c",
						c)));
}

/* --------------------------------------------------------- */
/* Top level exports */
/* --------------------------------------------------------- */
PG_FUNCTION_INFO_V1(bson_query_match);
PG_FUNCTION_INFO_V1(bson_true_match);
PG_FUNCTION_INFO_V1(query_match_support);


/*
 * bson_query_match is a lazy, inefficient implementation of the @@
 * operator and bson_query_match, used only when we cannot replace it in the planner hook.
 */
Datum
bson_query_match(PG_FUNCTION_ARGS)
{
	pgbson *document = PG_GETARG_PGBSON(0);
	pgbson *query = PG_GETARG_PGBSON(1);

	Const *documentConst = MakeBsonConst(document);
	Const *queryConst = MakeBsonConst(query);

	ereport(NOTICE, (errmsg("using bson_query_match implementation")));

	ReplaceBsonQueryOperatorsContext context;
	memset(&context, 0, sizeof(context));

	Node *quals = NULL;
	if (PG_NARGS() == 2)
	{
		/* Expand the @@ operator into regular BSON operators */
		OpExpr *queryExpr = makeNode(OpExpr);
		queryExpr->opno = BsonQueryOperatorId();
		queryExpr->opfuncid = BsonQueryMatchFunctionId();
		queryExpr->inputcollid = InvalidOid;
		queryExpr->opresulttype = BsonTypeId();
		queryExpr->args = list_make2(documentConst, queryConst);
		queryExpr->location = -1;

		quals = ReplaceBsonQueryOperatorsMutator((Node *) queryExpr, &context);
	}
	else if (PG_NARGS() == 4)
	{
		/* TODO: Remove after v0.110 when function has only STRICT forms */
		if (PG_ARGISNULL(0) && PG_ARGISNULL(1))
		{
			PG_RETURN_NULL();
		}

		Const *variableSpecConst = NULL;
		if (PG_ARGISNULL(2))
		{
			variableSpecConst = MakeBsonConst(PgbsonInitEmpty());
		}
		else
		{
			pgbson *variableSpecBson = PG_GETARG_PGBSON(2);
			variableSpecConst = MakeBsonConst(variableSpecBson);
		}

		Const *collationConst = NULL;
		if (PG_ARGISNULL(3))
		{
			collationConst = makeConst(TEXTOID, -1, InvalidOid, -1,
									   CStringGetDatum(""), false, false);
		}
		else
		{
			Datum collationDatum = CStringGetDatum(PG_GETARG_CSTRING(3));
			collationConst = makeConst(TEXTOID, -1, InvalidOid, -1, collationDatum,
									   false, false);
		}

		List *args = list_make4(documentConst, queryConst, variableSpecConst,
								collationConst);
		FuncExpr *funcExpr = makeFuncExpr(BsonQueryMatchWithLetAndCollationFunctionId(),
										  BsonTypeId(), args,
										  InvalidOid, InvalidOid, InvalidOid);

		quals = ReplaceBsonQueryOperatorsMutator((Node *) funcExpr, &context);
	}

	/* evaluate the constant expressions */
	Node *evaluatedExpr = eval_const_expressions(NULL, (Node *) quals);
	if (!IsA(evaluatedExpr, Const))
	{
		ereport(ERROR, (errmsg("failed to evaluated expression to constant")));
	}

	/* obtain the boolean result */
	Const *evaluatedConst = (Const *) evaluatedExpr;

	PG_RETURN_DATUM(evaluatedConst->constvalue);
}


/*
 * bson_true_match is a dummy placeholder function used to hold
 * a pointer to the parameterized value in the planner hook for the @@
 * operator. This is needed for citus distribution to have a pointer from
 * the query to the parameterized value to ensure it's distributed to the workers
 * properly. We make this a single function that takes 1 param and returns true
 * so that it's evaluated once per query context.
 */
Datum
bson_true_match(PG_FUNCTION_ARGS)
{
	PG_RETURN_BOOL(true);
}


/*
 * Is a support function for the query match function to expand it into the individual clauses.
 */
Datum
query_match_support(PG_FUNCTION_ARGS)
{
	Node *supportRequest = (Node *) PG_GETARG_POINTER(0);
	if (!IsA(supportRequest, SupportRequestSimplify))
	{
		PG_RETURN_POINTER(NULL);
	}

	/* Try to convert operator/function call to index conditions */
	SupportRequestSimplify *req = (SupportRequestSimplify *) supportRequest;
	FuncExpr *funcExpr = req->fcall;

	bool hasLetAndCollation =
		funcExpr->funcid == BsonQueryMatchWithLetAndCollationFunctionId();
	if (!hasLetAndCollation && funcExpr->funcid != BsonQueryMatchFunctionId())
	{
		PG_RETURN_POINTER(NULL);
	}

	Expr *queryArg = lsecond(funcExpr->args);
	if (!IsA(queryArg, Const))
	{
		PG_RETURN_POINTER(NULL);
	}

	Const *queryConst = (Const *) queryArg;
	if (queryConst->constisnull)
	{
		PG_RETURN_POINTER(NULL);
	}

	BsonQueryOperatorContext context = { 0 };
	context.documentExpr = linitial(funcExpr->args);
	context.inputType = MongoQueryOperatorInputType_Bson;
	context.simplifyOperators = true;
	context.coerceOperatorExprIfApplicable = true;
	context.requiredFilterPathNameHashSet = NULL;
	context.variableContext = NULL;

	if (hasLetAndCollation)
	{
		Expr *variableSpecArg = lthird(funcExpr->args);
		Expr *collationArg = lfourth(funcExpr->args);

		if (!IsA(variableSpecArg, Const) || !IsA(collationArg, Const))
		{
			PG_RETURN_POINTER(NULL);
		}

		Const *variableSpecConst = (Const *) variableSpecArg;
		if (!variableSpecConst->constisnull)
		{
			context.variableContext = (Expr *) variableSpecConst;
		}

		Const *collationConst = (Const *) collationArg;
		if (!collationConst->constisnull)
		{
			char *collationString =
				TextDatumGetCString(collationConst->constvalue);
			if (IsCollationApplicable(collationString))
			{
				context.collationString = collationString;
			}
		}
	}

	pgbson *queryDocument = DatumGetPgBson(queryConst->constvalue);

	/* open the Mongo query document */
	bson_iter_t queryDocIter;
	PgbsonInitIterator(queryDocument, &queryDocIter);

	/* convert the Mongo query to a list of Postgres quals */
	List *quals = CreateQualsFromQueryDocIterator(&queryDocIter, &context);
	UpdateQueryOperatorContextSortList(req->root->parse,
									   context.targetEntries,
									   context.sortClauses);

	if (quals != NIL)
	{
		PG_RETURN_POINTER(make_ands_explicit(quals));
	}

	PG_RETURN_POINTER(NULL);
}


/*
 * MakeBsonConst creates a Const expression for a given bson.
 */
static Const *
MakeBsonConst(pgbson *pgbson)
{
	Const *bsonConst = makeNode(Const);
	bsonConst->consttype = BsonTypeId();
	bsonConst->consttypmod = -1;
	bsonConst->constlen = -1;
	bsonConst->constvalue = PointerGetDatum(pgbson);
	bsonConst->constbyval = false;
	bsonConst->constisnull = false;
	bsonConst->location = -1;

	return bsonConst;
}


/*
 * MakeSimpleDocumentVar returns a Var node for the document column of a
 * Mongo data table assuming that returned node will be the only variable of
 * the query in which caller will use the Var, and that variable is not a
 * subquery variable.
 */
Var *
MakeSimpleDocumentVar(void)
{
	/* the only Var in the Query context (if any) */
	Index varno = 1;

	/* not lives in a subquery */
	Index varlevelsup = 0;

	return makeVar(varno, DOCUMENT_DATA_TABLE_DOCUMENT_VAR_ATTR_NUMBER,
				   BsonTypeId(), DOCUMENT_DATA_TABLE_DOCUMENT_VAR_TYPMOD,
				   DOCUMENT_DATA_TABLE_DOCUMENT_VAR_COLLATION, varlevelsup);
}


/*
 * ReplaceBsonQueryOperators replaces all occurrences of <bson> @@ <bson>
 * with an expanded expression that uses low-level BSON operators.
 */
Node *
ReplaceBsonQueryOperators(Query *node, ParamListInfo boundParams)
{
	ReplaceBsonQueryOperatorsContext context;
	memset(&context, 0, sizeof(context));
	context.currentQuery = node;
	context.boundParams = boundParams;

	return ReplaceBsonQueryOperatorsMutator((Node *) node, &context);
}


/*
 * Creates a parsed query AST for a given document containing
 * a query expression. The input VAR is placed as an internal typed
 * variable.
 * e.g. { "$or" [ { "$gte": 3 }, { "$lte": 6}]}
 *   -> VAR @>= '{ "" : 3 }' OR VAR @<= '{ "": 6 }'
 *
 * The collation is passed in the context and then is embedded in the
 * operator for final comparisons.
 */
Expr *
CreateQualForBsonValueExpression(const bson_value_t *expression, const
								 char *collationString,
								 bool skipObjectArrayFilter)
{
	if (expression->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"expression should be a document")));
	}

	Var *var = makeVar(1, 1, INTERNALOID, -1, DEFAULT_COLLATION_OID, 0);
	BsonQueryOperatorContext context = { 0 };
	context.documentExpr = (Expr *) var;
	context.inputType = MongoQueryOperatorInputType_BsonValue;
	context.simplifyOperators = true;
	context.coerceOperatorExprIfApplicable = false;
	context.requiredFilterPathNameHashSet = NULL;
	context.variableContext = NULL;
	context.collationString = collationString;
	context.skipObjectArrayFilter = skipObjectArrayFilter;

	const char *traversedPath = NULL;
	const char *basePath = "";
	return CreateQualForBsonValueExpressionCore(expression, &context,
												traversedPath, basePath);
}


/*
 * Creates a parsed query AST for a given array containing
 * a list of query expressions. The input VAR is placed as an internal typed
 * variable.
 * e.g. [ 1, 2]
 *   -> VAR @= '{ "" : 1 }' AND VAR @= '{ "": 2 }'
 */
Expr *
CreateQualForBsonValueArrayExpression(const bson_value_t *expression)
{
	if (expression->value_type != BSON_TYPE_ARRAY)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Expression must be an array type")));
	}

	Var *var = makeVar(1, 1, INTERNALOID, -1, DEFAULT_COLLATION_OID, 0);
	BsonQueryOperatorContext context = { 0 };
	context.documentExpr = (Expr *) var;
	context.inputType = MongoQueryOperatorInputType_BsonValue;
	context.simplifyOperators = true;
	context.coerceOperatorExprIfApplicable = false;
	context.requiredFilterPathNameHashSet = NULL;
	context.variableContext = NULL;

	const char *traversedPath = NULL;
	const char *basePath = "";

	bson_iter_t queryDocIterator;
	bson_iter_init_from_data(&queryDocIterator,
							 expression->value.v_doc.data,
							 expression->value.v_doc.data_len);
	List *quals = NIL;
	while (bson_iter_next(&queryDocIterator))
	{
		const bson_value_t *value = bson_iter_value(&queryDocIterator);
		if (BSON_ITER_HOLDS_DOCUMENT(&queryDocIterator))
		{
			quals = lappend(quals,
							CreateQualForBsonValueExpressionCore(value,
																 &context,
																 traversedPath,
																 basePath));
		}
		else
		{
			const MongoQueryOperator *eqOperator =
				GetMongoQueryOperatorByQueryOperatorType(
					QUERY_OPERATOR_EQ, context.inputType);

			/* <path> : <value>, convert to = expression  */
			quals = lappend(quals,
							CreateFuncExprForQueryOperator(&context, "",
														   eqOperator, value));
		}
	}

	return make_ands_explicit(quals);
}


/*
 * Creates a BsonValue based Expression for a top level query (i.e. one
 * typically supplied by an @@ operator).
 */
List *
CreateQualsForBsonValueTopLevelQueryIter(bson_iter_t *queryIter,
										 const char *collationString)
{
	Var *var = makeVar(1, 1, INTERNALOID, -1, DEFAULT_COLLATION_OID, 0);
	BsonQueryOperatorContext context = { 0 };
	context.documentExpr = (Expr *) var;
	context.inputType = MongoQueryOperatorInputType_BsonValue;
	context.simplifyOperators = true;
	context.coerceOperatorExprIfApplicable = false;
	context.requiredFilterPathNameHashSet = NULL;
	context.variableContext = NULL;

	if (IsCollationApplicable(collationString))
	{
		context.collationString = collationString;
	}

	return CreateQualsFromQueryDocIterator(queryIter, &context);
}


/*
 * BsonQueryOperatorContextCommonBuilder is a helper function to create a BsonQueryOperatorContext with common values.
 * This is used to avoid code duplication in the query operator implementations and to ensure that the context is initialized properly.
 * You can modify the context as needed after calling this function.
 */
void
BsonQueryOperatorContextCommonBuilder(BsonQueryOperatorContext *context)
{
	Var *var = makeVar(1, 1, BsonTypeId(), -1, DEFAULT_COLLATION_OID, 0);
	context->documentExpr = (Expr *) var;
	context->inputType = MongoQueryOperatorInputType_Bson;
	context->simplifyOperators = true;
	context->coerceOperatorExprIfApplicable = false;
	context->requiredFilterPathNameHashSet = NULL;
	context->variableContext = NULL;
}


/*
 * Creates a parsed query AST for a given document containing
 * a query expression. The input is a documentExpr provided
 * as an input as a bson type.
 * variable.
 * e.g. { "$or" [ { "$gte": 3 }, { "$lte": 6}]}
 * Given a base queryPath of "a.b"
 *   -> documentExpr @>= '{ "a.b" : 3 }' OR documentExpr @<= '{ "a.b": 6 }'
 */
Expr *
CreateQualForBsonExpression(const bson_value_t *expression, const
							char *queryPath, BsonQueryOperatorContext *context)
{
	if (expression->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"expression should be a document")));
	}

	const char *traversedPath = queryPath;
	const char *basePath = queryPath;
	return CreateQualForBsonValueExpressionCore(expression, context, traversedPath,
												basePath);
}


/*
 * Creates a parsed query AST for a given document containing
 * a query expression off the specified base path.
 * variable.
 * e.g. { "$or" [ { "$gte": 3 }, { "$lte": 6}]}
 *   -> documentExpr @>= '{ "" : 3 }' OR documentExpr @<= '{ "": 6 }'
 */
static Expr *
CreateQualForBsonValueExpressionCore(const bson_value_t *expression,
									 BsonQueryOperatorContext *context,
									 const char *traversedPath,
									 const char *basePath)
{
	bson_iter_t queryDocIterator;
	bson_iter_init_from_data(&queryDocIterator,
							 expression->value.v_doc.data,
							 expression->value.v_doc.data_len);

	/* This is a variant of CreateQualsFromQueryDocIterator that
	 * allows for array based qualifiers (e.g. $gte: 2) without a path
	 * associated with it. We track top level operators and create a dummy
	 * value filter
	 * Additionally, it adds a final qual for array/object type filters if the input
	 * filter has a path or top level operators ($and/$or) since array filters
	 * require this.
	 */
	List *quals = NIL;
	bool addObjectArrayFilter = false;
	bool regexFound = false;
	bson_value_t *options = NULL;
	while (bson_iter_next(&queryDocIterator))
	{
		/* field or logical operator */
		const char *path = bson_iter_key(&queryDocIterator);
		Expr *qual;

		if (path[0] == '$')
		{
			/* in this fork there's 2 possibilities:
			 * There's a logical operator at the root
			 * e.g. { "$eq": 5 } or { "$and": [ ... ]}
			 * This is different from regular queries where the top level operator
			 * has to be a path.
			 * Consequently, we do a first pass check if it matches any operator,
			 * and pass that through.
			 * $and/$or/$nor is handled explicitly.
			 * $eq etc are handled by the default case.
			 */
			const MongoQueryOperator *operator;
			operator = GetMongoQueryOperatorByMongoOpName(path, context->inputType);

			switch (operator->operatorType)
			{
				/* Handle logical operators that are found the same as query expressions */
				case QUERY_OPERATOR_AND:
				case QUERY_OPERATOR_OR:
				case QUERY_OPERATOR_NOR:
				case QUERY_OPERATOR_EXPR:
				case QUERY_OPERATOR_TEXT:
				case QUERY_OPERATOR_ALWAYS_FALSE:
				case QUERY_OPERATOR_ALWAYS_TRUE:
				case QUERY_OPERATOR_JSONSCHEMA:
				{
					regexFound = false;
					addObjectArrayFilter = true;
					qual = CreateBoolExprFromLogicalExpression(&queryDocIterator,
															   context,
															   traversedPath);
					break;
				}

				case QUERY_OPERATOR_COMMENT:
				{
					/* Ignore comment */
					qual = NULL;
					break;
				}

				default:
				{
					/* Otherwise, it's a valid operator, create an OpExpr for the expression */
					qual = CreateOpExprFromOperatorDocIteratorCore(&queryDocIterator,
																   context, basePath,
																   &regexFound, &options);
					break;
				}
			}
		}
		else
		{
			/* all other paths are comparisons */
			addObjectArrayFilter = true;
			regexFound = false;
			qual = CreateOpExprFromComparisonExpression(&queryDocIterator, context,
														traversedPath,
														path);
		}

		if (qual != NULL)
		{
			quals = lappend(quals, qual);
		}
	}

	if (addObjectArrayFilter && !context->skipObjectArrayFilter &&
		context->inputType == MongoQueryOperatorInputType_BsonValue)
	{
		/*
		 * For some expressions like { "b": 2 } or $and/$or in an expression context, we need to only consider
		 * values that are objects/arrays. We add that as an explicit filter here
		 */
		const MongoQueryOperator *typeOperator =
			GetMongoQueryOperatorByQueryOperatorType(QUERY_OPERATOR_TYPE,
													 context->inputType);
		bson_value_t dollarTypeValue;
		dollarTypeValue.value_type = BSON_TYPE_INT32;
		dollarTypeValue.value.v_int32 = BSON_TYPE_DOCUMENT;
		Expr *isObjectQual = CreateFuncExprForQueryOperator(
			context,
			basePath, typeOperator,
			&dollarTypeValue);

		dollarTypeValue.value.v_int32 = BSON_TYPE_ARRAY;
		Expr *isArrayQual = CreateFuncExprForQueryOperator(context,
														   basePath, typeOperator,
														   &dollarTypeValue);

		BoolExpr *logicalExpr = makeNode(BoolExpr);
		logicalExpr->boolop = OR_EXPR;
		logicalExpr->args = list_make2(isObjectQual, isArrayQual);
		logicalExpr->location = -1;

		quals = lappend(quals, logicalExpr);
	}

	return make_ands_explicit(quals);
}


/*
 * Recursively find namespace filters in a $match stage.
 * Extracts ns.db and ns.coll filters into dbValue and collValue respectively.
 * Other filters are written back to a new pgbson document which is returned.
 * e.g. if the match spec is { ns.db: "testdb", ns : { coll: "testcoll" }, age: { $gt: 30 } }
 * dbValue="testdb", collValue="testcoll" and returned match document = { age: { $gt: 30 } }
 *
 * multipleNSFilters flag is used to signal that the parsing found multiple `and ns` filters
 *
 * TODO: Handle $or single elemenent ns filters in future if needed.
 *
 * Returns NULL for new filter document in case `no valid/ conflicting` ns filters are found
 */
pgbson *
FindNamespaceFiltersInMatchSpec(bson_iter_t *specIter, List **nsFilters,
								bool *multipleNSFilters,
								bool *isInvalidNSFilters)
{
	if (nsFilters == NULL || specIter == NULL)
	{
		return NULL;
	}

	MatchNamespaceFiltersContext matchNSContext;
	memset(&matchNSContext, 0, sizeof(MatchNamespaceFiltersContext));
	pgbson *postInlineFilters = FindNamespaceFiltersInMatchSpecCore(specIter,
																	&matchNSContext);
	*multipleNSFilters = matchNSContext.multipleNSFilters;
	*isInvalidNSFilters = matchNSContext.isInvalidNSFilters;

	if (!matchNSContext.multipleNSFilters && !matchNSContext.isInvalidNSFilters)
	{
		int dbPathFiltersCount = list_length(matchNSContext.nsDbPathFilters);
		int collPathFiltersCount = list_length(matchNSContext.nsCollPathFilters);
		int nsValueFiltersCount = list_length(matchNSContext.nsValueFilters);

		if (dbPathFiltersCount > 0 && collPathFiltersCount > 0 &&
			nsValueFiltersCount == 0)
		{
			/* Only path filters available, cross join */
			ListCell *dbCell;
			foreach(dbCell, matchNSContext.nsDbPathFilters)
			{
				ListCell *collCell;
				foreach(collCell, matchNSContext.nsCollPathFilters)
				{
					*nsFilters = lappend(*nsFilters,
										 list_make2(lfirst(dbCell), lfirst(collCell)));
				}
			}
		}
		else if (dbPathFiltersCount == 0 && collPathFiltersCount == 0 &&
				 nsValueFiltersCount > 0)
		{
			/* Only value filters available */
			*nsFilters = matchNSContext.nsValueFilters;
		}
		else if ((dbPathFiltersCount > 0 ||
				  collPathFiltersCount > 0) &&
				 nsValueFiltersCount > 0)
		{
			/* Both path and value filters available, these are AND conditions on namespace,
			 * so all the filters at the intersection can be applied and inlined.
			 * If there is no intersection that means this is just an empty result set.
			 * We don't throw an error for empty set.
			 *
			 * Consider a case like this:
			 * Document : { db: "d1", coll: "c1" }
			 * {
			 *      ns.coll: { $in: ["c1"] },
			 *      ns.db: { $in: ["d1"] },
			 *      ns: { $in: [ { db: "d2", coll: "c1" } ] }
			 * }
			 *
			 * Since ns is provided as value filter as well with $in we need to make sure we do the intersections
			 * correctly with indivudual filters in $in, the ns filter actually doesn't match the above doc resulting in empty
			 * set
			 */


			/* Loop through all the ns value filter pairs and see if they intersect with any path filters
			 */
			ListCell *nsValueFilterCell = NULL;

			SortStringList(matchNSContext.nsDbPathFilters);
			SortStringList(matchNSContext.nsCollPathFilters);

			HTAB *addedNsFilters = CreateStringViewHashSet();

			foreach(nsValueFilterCell, matchNSContext.nsValueFilters)
			{
				CHECK_FOR_INTERRUPTS();

				List *nsFilter = (List *) lfirst(nsValueFilterCell);
				char *dbFilter = list_nth(nsFilter, 0);
				char *collFilter = list_nth(nsFilter, 1);
				StringView dbFilterView = CreateStringViewFromString(dbFilter);
				StringView collFilterView = CreateStringViewFromString(collFilter);

				bool exists = false;
				exists = StringListBinarySearch(matchNSContext.nsDbPathFilters,
												dbFilterView.string);
				if (!exists && dbPathFiltersCount > 0)
				{
					continue;
				}

				exists = StringListBinarySearch(matchNSContext.nsCollPathFilters,
												collFilterView.string);
				if (!exists && collPathFiltersCount > 0)
				{
					continue;
				}

				/* Found intersection, only add if there are unique db and coll filters which are not added already */
				StringView namespaceFilterView = CreateStringViewFromString(
					psprintf("%s.%s", dbFilterView.string,
							 collFilterView.string));
				hash_search(addedNsFilters, &namespaceFilterView, HASH_ENTER, &exists);
				if (!exists)
				{
					*nsFilters = lappend(*nsFilters, nsFilter);
				}
			}

			hash_destroy(addedNsFilters);

			if (*nsFilters == NIL)
			{
				/* No intersection found, this is empty set because of multiple filter */
				*multipleNSFilters = true;
				return NULL;
			}
		}
		else
		{
			/* Unknown combination, consider invalid */
			*isInvalidNSFilters = true;
			return NULL;
		}
	}

	return postInlineFilters;
}


static pgbson *
HandleEqualityNamespaceFilter(bson_iter_t *iter, bool *nsInlined,
							  MatchNamespaceFiltersContext *context)
{
	bool isDbFound = false;
	bool isCollFound = false;
	bool isNsValueFilterEmpty = list_length(context->nsValueFilters) == 0;
	pgbson *newFilteredDoc = AddNamespaceValueFiltersToList(iter,
															&context->nsValueFilters,
															&isDbFound, &isCollFound);
	if (!isDbFound || !isCollFound)
	{
		/*
		 * Both the filters need to be present in single $eq operator
		 * to consider it as namespace filter
		 */
		context->isInvalidNSFilters = true;
		return NULL;
	}
	else if (!isNsValueFilterEmpty)
	{
		/*
		 * We found a valid filter again when we already had some filters in ns nested value path,
		 * this is empty set AND
		 */
		context->isInvalidNSFilters = true;
		return NULL;
	}

	*nsInlined = true;
	if (!IsPgbsonEmptyDocument(newFilteredDoc))
	{
		*nsInlined = false;
	}
	return newFilteredDoc;
}


static pgbson *
FindNamespaceFiltersInMatchSpecCore(bson_iter_t *specIter,
									MatchNamespaceFiltersContext *matchCtxt)
{
	check_stack_depth();
	CHECK_FOR_INTERRUPTS();

	pgbson_writer writer;
	PgbsonWriterInit(&writer);

	bool isDbFound;
	bool isCollFound;

	while (bson_iter_next(specIter))
	{
		const char *key = bson_iter_key(specIter);
		const bson_value_t *value = bson_iter_value(specIter);
		if (strcmp(key, "$and") == 0 && value->value_type == BSON_TYPE_ARRAY)
		{
			bson_iter_t andArrayIter;
			if (bson_iter_recurse(specIter, &andArrayIter))
			{
				/*
				 * We start a new $and document to see if any of the internal filters of $and
				 * contains ns filters, if yes then these are removed. consider a case like this
				 * {$and: [{ns.db: "db"}, {ns.coll: "coll"}]}
				 *
				 * Here both the individual filters are for ns and are not involved in final filters
				 * so we need to add $and filters at the end once we are sure it is not inlined completely.
				 */
				pgbson_writer andWriter;
				PgbsonWriterInit(&andWriter);
				pgbson_array_writer andArrayWriter;
				PgbsonWriterStartArray(&andWriter, "$and", 4, &andArrayWriter);

				bool andElementInlined = true;
				while (bson_iter_next(&andArrayIter))
				{
					bson_iter_t andElementIter;

					if (BSON_ITER_HOLDS_DOCUMENT(&andArrayIter) &&
						bson_iter_recurse(&andArrayIter, &andElementIter))
					{
						pgbson *andElementDoc = FindNamespaceFiltersInMatchSpecCore(
							&andElementIter, matchCtxt);

						if (matchCtxt->isInvalidNSFilters || matchCtxt->multipleNSFilters)
						{
							/* Early return in case of invalid or multiple ns filters */
							return NULL;
						}

						if (!IsPgbsonEmptyDocument(andElementDoc))
						{
							PgbsonArrayWriterWriteDocument(&andArrayWriter,
														   andElementDoc);
							andElementInlined = false;
						}
					}
					else
					{
						/* Other filters present */
						andElementInlined = false;
						PgbsonArrayWriterWriteValue(&andArrayWriter,
													bson_iter_value(&andArrayIter));
					}
				}
				PgbsonWriterEndArray(&andWriter, &andArrayWriter);
				if (!andElementInlined)
				{
					bson_value_t andDocValue = PgbsonWriterGetValue(&andWriter);

					/* Only concat the remaining portion of $and filters */
					PgbsonWriterConcatBytes(&writer, andDocValue.value.v_doc.data,
											andDocValue.value.v_doc.data_len);
				}
			}
		}
		else if (strcmp(key, "ns.db") == 0)
		{
			if (!AddNamespaceFilterToList(bson_iter_value(specIter),
										  &matchCtxt->nsDbPathFilters))
			{
				/* Failed to add db filter because there is invalid match value */
				matchCtxt->isInvalidNSFilters = true;
				return NULL;
			}
		}
		else if (strcmp(key, "ns.coll") == 0)
		{
			if (!AddNamespaceFilterToList(bson_iter_value(specIter),
										  &matchCtxt->nsCollPathFilters))
			{
				/* Failed to add coll filter because there is invalid match value */
				matchCtxt->isInvalidNSFilters = true;
				return NULL;
			}
		}
		else if (strcmp(key, "ns") == 0)
		{
			const bson_value_t *nsValue = bson_iter_value(specIter);
			bson_iter_t nsIter;
			if (nsValue->value_type == BSON_TYPE_DOCUMENT &&
				bson_iter_recurse(specIter, &nsIter))
			{
				/*
				 * We do the similar thing here as with $and above. We parse the ns document
				 * to extract db and coll filters. Other filters are written back to the matchWriter.
				 */
				pgbson_writer nsWriter, nsElemWriter;
				PgbsonWriterInit(&nsWriter);
				PgbsonWriterStartDocument(&nsWriter, "ns", 2, &nsElemWriter);
				bool nsInlined = true;

				pgbsonelement nsElement;
				bool isSingleFilter = TryGetSinglePgbsonElementFromBsonIterator(&nsIter,
																				&nsElement);
				bool isOperator = nsElement.pathLength > 0 && nsElement.path[0] == '$';
				bool isEqOperator = strncmp(nsElement.path, "$eq",
											nsElement.pathLength) == 0;
				if (isOperator)
				{
					/*
					 * Don't inline, if its is not a single operator or not $eq/$in
					 */
					if (!(isSingleFilter &&
						  (strcmp(nsElement.path, "$eq") == 0 || strcmp(nsElement.path,
																		"$in") == 0)))
					{
						matchCtxt->isInvalidNSFilters = true;
						return NULL;
					}

					if (isEqOperator)
					{
						/* $eq operator */
						if (nsElement.bsonValue.value_type == BSON_TYPE_DOCUMENT)
						{
							bson_iter_t eqValueItr;
							BsonValueInitIterator(&nsElement.bsonValue, &eqValueItr);
							pgbson *filteredDoc = HandleEqualityNamespaceFilter(
								&eqValueItr, &nsInlined,
								matchCtxt);
							if (!nsInlined && filteredDoc != NULL)
							{
								PgbsonWriterConcat(&nsElemWriter, filteredDoc);
							}
						}
						else
						{
							/* Invalid $eq value type */
							matchCtxt->isInvalidNSFilters = true;
							return NULL;
						}
					}
					else
					{
						/* $in operator */
						if (nsElement.bsonValue.value_type == BSON_TYPE_ARRAY)
						{
							bson_iter_t inArrayIter;
							BsonValueInitIterator(&nsElement.bsonValue, &inArrayIter);
							pgbson_writer nsInArrayWriter;
							PgbsonWriterInit(&nsInArrayWriter);

							pgbson_array_writer nsInArrayElemWriter;
							PgbsonWriterStartArray(&nsInArrayWriter, "$in", 3,
												   &nsInArrayElemWriter);

							while (bson_iter_next(&inArrayIter))
							{
								const bson_value_t *arrayValue = bson_iter_value(
									&inArrayIter);
								if (arrayValue->value_type == BSON_TYPE_DOCUMENT)
								{
									isDbFound = false;
									isCollFound = false;
									bson_iter_t arrayDocItr;
									BsonValueInitIterator(arrayValue, &arrayDocItr);
									pgbson *newFilteredDoc =
										AddNamespaceValueFiltersToList(&arrayDocItr,
																	   &matchCtxt->
																	   nsValueFilters,
																	   &isDbFound,
																	   &isCollFound);

									/*
									 * This is $in so invalid filters are not made part of the namespace filters list and are written
									 * back to the remaining portion of the match document, there can be potential valid filter along
									 * the list.
									 */
									if (!IsPgbsonEmptyDocument(newFilteredDoc))
									{
										PgbsonArrayWriterWriteDocument(
											&nsInArrayElemWriter,
											newFilteredDoc);
										nsInlined = false;
									}
								}
							}
							PgbsonWriterEndArray(&nsInArrayWriter,
												 &nsInArrayElemWriter);
							if (!nsInlined)
							{
								/* Not inlined completely so write the remaining portion */
								PgbsonWriterConcat(&nsElemWriter,
												   PgbsonWriterGetPgbson(
													   &nsInArrayWriter));
							}
						}
						else
						{
							matchCtxt->isInvalidNSFilters = true;
							return NULL;
						}
					}
				}
				else
				{
					/* regular document filter, it is similar to $eq condition above */
					bson_iter_t nsValueItr;
					BsonValueInitIterator(nsValue, &nsValueItr);
					pgbson *filteredDoc = HandleEqualityNamespaceFilter(&nsValueItr,
																		&nsInlined,
																		matchCtxt);
					if (!nsInlined && filteredDoc != NULL)
					{
						PgbsonWriterConcat(&nsElemWriter, filteredDoc);
					}
				}
				PgbsonWriterEndDocument(&nsWriter, &nsElemWriter);
				if (!nsInlined)
				{
					PgbsonWriterConcat(&writer, PgbsonWriterGetPgbson(&nsWriter));
				}
			}
			else
			{
				matchCtxt->isInvalidNSFilters = true;
				return NULL;
			}
		}
		else
		{
			/* Other filters present */
			PgbsonWriterAppendValue(&writer, key, strlen(key),
									bson_iter_value(specIter));
		}
	}

	return PgbsonWriterGetPgbson(&writer);
}


/*
 * Iterates through the iterator and add `db` or `coll` UTF8 values to list,
 * other values are ignored.
 */
static pgbson *
AddNamespaceValueFiltersToList(bson_iter_t *valueItr, List **nsValueList,
							   bool *isDbFound, bool *isCollFound)
{
	pgbson_writer newFilters;
	PgbsonWriterInit(&newFilters);

	*isDbFound = false;
	*isCollFound = false;
	bson_value_t dbFilter = { 0 };
	bson_value_t collFilter = { 0 };
	while (bson_iter_next(valueItr))
	{
		const char *key = bson_iter_key(valueItr);
		const bson_value_t *value = bson_iter_value(valueItr);
		if (strcmp(key, "db") == 0)
		{
			if (value->value_type == BSON_TYPE_UTF8)
			{
				dbFilter = *value;
			}
		}
		else if (strcmp(key, "coll") == 0)
		{
			if (value->value_type == BSON_TYPE_UTF8)
			{
				collFilter = *value;
			}
		}
		else
		{
			PgbsonWriterAppendValue(&newFilters, key, strlen(key), value);
		}
	}

	*isDbFound = (dbFilter.value_type != BSON_TYPE_EOD);
	*isCollFound = (collFilter.value_type != BSON_TYPE_EOD);

	if (!*isDbFound || !*isCollFound)
	{
		/* If any of these filters are not found, write it back to the writer */
		if (*isDbFound)
		{
			PgbsonWriterAppendValue(&newFilters, "db", 2, &dbFilter);
		}
		else if (*isCollFound)
		{
			PgbsonWriterAppendValue(&newFilters, "coll", 4, &collFilter);
		}
	}
	else
	{
		/* Both filters present */
		char *db = pnstrdup(dbFilter.value.v_utf8.str, dbFilter.value.v_utf8.len);
		char *coll = pnstrdup(collFilter.value.v_utf8.str, collFilter.value.v_utf8.len);
		*nsValueList = lappend(*nsValueList, list_make2(db, coll));
	}

	return PgbsonWriterGetPgbson(&newFilters);
}


/*
 * Adds the match filters on namespace to an array writer, it only allows certain equality operators today
 * such as $eq and $in to be valid.
 */
static bool
AddNamespaceFilterToList(const bson_value_t *value, List **list)
{
	bool isValid = false;
	if (value->value_type == BSON_TYPE_UTF8)
	{
		char *copiedStr = pnstrdup(value->value.v_utf8.str,
								   value->value.v_utf8.len);
		*list = lappend(*list, copiedStr);
		isValid = true;
	}
	else if (value->value_type == BSON_TYPE_DOCUMENT)
	{
		bson_iter_t iter;
		BsonValueInitIterator(value, &iter);
		pgbsonelement element;
		if (TryGetSinglePgbsonElementFromBsonIterator(&iter, &element))
		{
			if (strcmp(element.path, "$in") == 0 &&
				element.bsonValue.value_type == BSON_TYPE_ARRAY)
			{
				bson_iter_t arrayIter;
				BsonValueInitIterator(&element.bsonValue, &arrayIter);
				while (bson_iter_next(&arrayIter))
				{
					const bson_value_t *arrayValue = bson_iter_value(&arrayIter);

					/*
					 * $in is an OR clause even if some entries here are invalid or not strings, we
					 * skip those entries.
					 */
					if (arrayValue->value_type == BSON_TYPE_UTF8)
					{
						char *copiedStr = pnstrdup(arrayValue->value.v_utf8.str,
												   arrayValue->value.v_utf8.len);
						*list = lappend(*list, copiedStr);
						isValid = true;
					}
				}
			}
			else if (strcmp(element.path, "$eq") == 0)
			{
				if (element.bsonValue.value_type == BSON_TYPE_UTF8)
				{
					char *copiedStr = pnstrdup(element.bsonValue.value.v_utf8.str,
											   element.bsonValue.value.v_utf8.len);
					*list = lappend(*list, copiedStr);
					isValid = true;
				}
			}
		}
	}
	return isValid;
}


/*
 * ReplaceBsonQueryOperatorsMutator is a mutator that replaces all occurrences
 * of <bson> @@ <bson> with an expanded expression that uses low-level BSON
 * operators.
 */
static Node *
ReplaceBsonQueryOperatorsMutator(Node *node, ReplaceBsonQueryOperatorsContext *context)
{
	if (node == NULL)
	{
		return NULL;
	}

	if (IsA(node, OpExpr))
	{
		OpExpr *opExpr = (OpExpr *) node;

		if (opExpr->opno == BsonQueryOperatorId())
		{
			/* operator always has 2 arguments */
			Assert(list_length(opExpr->args) == 2);

			Node *queryNode = lsecond(opExpr->args);
			queryNode = EvaluateBoundParameters(queryNode, context->boundParams);

			/*
			 * ExpandBsonQueryOperator adds shard_key_value filters based on the query
			 * for sharded collections. Below, we add shard_key_value = 0 filters for
			 * non-sharded collections in FROM.
			 */
			if (IsA(queryNode, Const))
			{
				const char *collationString = NULL;
				Const *variableSpecConst = NULL;

				Node *expandedExpr =
					(Node *) ExpandBsonQueryOperator(opExpr,
													 queryNode,
													 context->currentQuery,
													 context->boundParams,
													 &(context->targetEntries),
													 &(context->sortClauses),
													 collationString, variableSpecConst);

				return expandedExpr;
			}
		}

		return node;
	}
	else if (IsA(node, FuncExpr))
	{
		FuncExpr *funcExpr = (FuncExpr *) node;

		if (funcExpr->funcid == BsonQueryMatchWithLetAndCollationFunctionId())
		{
			Node *queryNode = lsecond(funcExpr->args);
			if (IsA(queryNode, Param))
			{
				queryNode = EvaluateBoundParameters(queryNode,
													context->boundParams);
			}
			if (IsA(queryNode, RelabelType))
			{
				RelabelType *relabeled = (RelabelType *) queryNode;
				if (relabeled->relabelformat == COERCE_IMPLICIT_CAST)
				{
					queryNode = (Node *) relabeled->arg;
				}
			}

			Node *variableSpecNode = lthird(funcExpr->args);
			if (IsA(variableSpecNode, Param))
			{
				variableSpecNode = EvaluateBoundParameters(variableSpecNode,
														   context->boundParams);
			}
			if (IsA(variableSpecNode, RelabelType))
			{
				RelabelType *relabeled = (RelabelType *) variableSpecNode;
				if (relabeled->relabelformat == COERCE_IMPLICIT_CAST)
				{
					variableSpecNode = (Node *) relabeled->arg;
				}
			}

			Node *collationStringNode = lfourth(funcExpr->args);
			if (IsA(collationStringNode, Param))
			{
				collationStringNode = EvaluateBoundParameters(collationStringNode,
															  context->boundParams);
			}

			if (IsA(queryNode, Const) && IsA(variableSpecNode, Const) &&
				IsA(collationStringNode, Const))
			{
				Node *documentNode = linitial(funcExpr->args);
				if (IsA(documentNode, RelabelType))
				{
					RelabelType *relabeled = (RelabelType *) documentNode;
					if (relabeled->relabelformat == COERCE_IMPLICIT_CAST)
					{
						documentNode = (Node *) relabeled->arg;
					}
				}

				Const *variableSpecConst = (Const *) variableSpecNode;

				char *collationString = NULL;
				Const *collationConst = (Const *) collationStringNode;
				if (!collationConst->constisnull)
				{
					collationString = TextDatumGetCString(collationConst->constvalue);
				}

				/* Create a dummy opExr with the document and query as operands. */
				/* We only need the operands in ExpandBsonQueryOperator so we ignore setting the other fields.  */
				OpExpr *opExpr = makeNode(OpExpr);
				opExpr->args = list_make2(documentNode, queryNode);

				Node *expandedExpr =
					(Node *) ExpandBsonQueryOperator(opExpr,
													 queryNode,
													 context->currentQuery,
													 context->boundParams,
													 &(context->targetEntries),
													 &(context->sortClauses),
													 collationString,
													 variableSpecConst);

				return expandedExpr;
			}
		}

		return node;
	}
	else if (IsA(node, Query))
	{
		Query *prevQuery = context->currentQuery;
		List *prevSortClauses = context->sortClauses;
		List *prevTargetEntries = context->targetEntries;
		Query *currentQuery = (Query *) node;
		context->sortClauses = NIL;
		context->targetEntries = NIL;

		/* descending into (sub)query */
		context->currentQuery = currentQuery;

		/* also descend into subqueries */
		Query *result = query_tree_mutator(currentQuery, ReplaceBsonQueryOperatorsMutator,
										   context, 0);

		UpdateQueryOperatorContextSortList(result, context->sortClauses,
										   context->targetEntries);

		ListCell *rteCell = NULL;
		int varno = 0;

		List *quals = make_ands_implicit((Expr *) result->jointree->quals);
		foreach(rteCell, result->rtable)
		{
			varno++;

			RangeTblEntry *rte = (RangeTblEntry *) lfirst(rteCell);
			if (!IsResolvableDocumentDbCollectionBasedRTE(rte, context->boundParams))
			{
				continue;
			}

			MongoCollection *collection = GetCollectionForRTE(rte, context->boundParams);
			if (collection != NULL && collection->shardKey == NULL)
			{
				/* construct a shard_key_value = <collection_id> filter */
				Expr *zeroShardKeyFilter = CreateNonShardedShardKeyValueFilter(varno,
																			   collection);

				/* add the filter to WHERE */
				quals = lappend(quals, zeroShardKeyFilter);
			}
		}

		result->jointree->quals = (Node *) make_ands_explicit(quals);

		/* back to parent query */
		context->currentQuery = prevQuery;
		context->sortClauses = prevSortClauses;
		context->targetEntries = prevTargetEntries;

		return (Node *) result;
	}

	return expression_tree_mutator(node, ReplaceBsonQueryOperatorsMutator, context);
}


/*
 * ExpandBsonQueryOperator returns an expanded expression that is logically
 * equivalent to the Mongo query BSON on the right side of the expression.
 * hasSublink flag is set when query tree has SUBLINK. This is used later in ReplaceBsonQueryOperatorsMutator to set Query->hasSubLinks flag.
 * Example: There is $elemMatch operator in expression which results in creation of SUBLINK in query tree.
 */
static Expr *
ExpandBsonQueryOperator(OpExpr *queryOpExpr, Node *queryNode,
						Query *currentQuery, ParamListInfo boundParams,
						List **targetEntries, List **sortClauses,
						const char *collationString, Const *variableSpec)
{
	BsonQueryOperatorContext context = { 0 };
	context.documentExpr = linitial(queryOpExpr->args);
	context.inputType = MongoQueryOperatorInputType_Bson;
	context.simplifyOperators = true;
	context.coerceOperatorExprIfApplicable = true;
	context.requiredFilterPathNameHashSet = NULL;

	context.variableContext = NULL;
	if (variableSpec != NULL && !variableSpec->constisnull)
	{
		context.variableContext = (Expr *) variableSpec;
	}

	if (IsCollationApplicable(collationString))
	{
		context.collationString = collationString;
	}

	Node *queryExpr = queryNode;

	/* we can only expand Const queries */
	Assert(IsA(queryExpr, Const));

	/* extract the Mongo query document from the query Const */
	Const *queryConst = (Const *) queryExpr;

	/* Early bailout from planning if the query is null */
	if (queryConst->constisnull)
	{
		/* Return the original query expression for @@ which is marked strict and will return
		 * NULL when one of the operand is NULL
		 */
		return (Expr *) queryOpExpr;
	}

	pgbson *queryDocument = DatumGetPgBson(queryConst->constvalue);

	/* open the Mongo query document */
	bson_iter_t queryDocIter;
	PgbsonInitIterator(queryDocument, &queryDocIter);

	/* convert the Mongo query to a list of Postgres quals */
	List *quals = CreateQualsFromQueryDocIterator(&queryDocIter, &context);

	if (context.targetEntries != NULL)
	{
		*targetEntries = context.targetEntries;
	}
	if (context.sortClauses != NULL)
	{
		*sortClauses = context.sortClauses;
	}

	/* extract the collection via the document Var */
	if (quals != NIL)
	{
		Index collectionVarno;
		MongoCollection *collection = NULL;
		bool isCollectionBasedRTE = false;

		/* if query is on a collection, get the collection metadata */
		collection = GetCollectionByDocumentVar(context.documentExpr,
												currentQuery,
												&collectionVarno, boundParams,
												&isCollectionBasedRTE);

		if (collection != NULL)
		{
			bool hasShardKeyFilters = false;

			/* For collection-based RTE on unsharded collections, shard_key_value is already appended, so skip here */
			if (!isCollectionBasedRTE ||
				collection->shardKey != NULL)
			{
				/* Retrieve the shard_key_value filter applicable to the specified collection */
				bson_value_t queryDocValue = ConvertPgbsonToBsonValue(queryDocument);
				bool areShardKeysCollationAware = false;
				Expr *shardKeyFilters =
					CreateShardKeyFiltersForQuery(&queryDocValue, collection->shardKey,
												  collection->collectionId,
												  collectionVarno,
												  &areShardKeysCollationAware);

				/* add the shard key filter if the query's shard key value is */
				/* not collation-sensitive. */
				/* If it is, we ignore the filter and distribute the execution. */
				if (shardKeyFilters != NULL &&
					!(IsCollationApplicable(collationString) &&
					  areShardKeysCollationAware))
				{
					hasShardKeyFilters = true;
					quals = lappend(quals, shardKeyFilters);
				}
			}
			else
			{
				hasShardKeyFilters = true;
			}

			if (hasShardKeyFilters)
			{
				/* Mongo allows collation on _id field. We need to make sure we do that as well. We can't
				 * push the Id filter to primary key index if the type needs to be collation aware (e.g., _id contains UTF8 )*/
				bool isCollationAware;
				bool isPointRead;
				Expr *idFilter = CreateIdFilterForQuery(quals,
														collectionVarno,
														&isCollationAware,
														&isPointRead);

				/* include _id filter in quals */
				if (idFilter != NULL &&
					!(isCollationAware &&
					  IsCollationApplicable(context.collationString)))
				{
					quals = lappend(quals, idFilter);
				}
			}
		}
	}

	/* create a function and hide it. Make the function point to the parameterized value
	 * So that the distributed planner works against this in Citus
	 * Since we replace the parameterized @@ operator with actual operators, the parameterized value
	 * is left behind with nothing in the query AST pointing to it. When Citus does a distributed
	 * query and pushes it to the remote workers via LibPQ, it sets the Oid of the parameterized
	 * types to 0 on the Coordinator; The worker then re-resolves the Oid based on the type Oid
	 * found in the worker. However, this doesn't work if the parameter is not referenced by the query
	 * (if there's unused parameters). This is a bug in Citus, and while this is unresolved, we can't
	 * leave the parameter lying around. Consequently, we use a dummy operator to point to the parameter
	 * to make distributed queries work.
	 * TODO: fix this in Citus and remove this block.
	 * Tracking bug: https://github.com/citusdata/citus/issues/5787
	 */
	Node *secondArg = lsecond(queryOpExpr->args);
	if (IsA(secondArg, Param))
	{
		FuncExpr *trueFunction = makeFuncExpr(
			BsonTrueFunctionId(), BOOLOID, list_make1(secondArg), InvalidOid,
			InvalidOid, COERCE_EXPLICIT_CALL);
		quals = lappend(quals, trueFunction);
	}

	/* multiple quals are implicit ANDs */
	return make_ands_explicit(quals);
}


/* For a given value, returns true if it is a valid element/document for $in/$nin ops.
 * Valid document : It can have $regex in it or {path:value} pattern but cannot have any other operator.
 */
bool
IsValidBsonDocumentForDollarInOrNinOp(const bson_value_t *value)
{
	if (value->value_type == BSON_TYPE_DOCUMENT)
	{
		bson_iter_t iterator;
		if (bson_iter_init_from_data(
				&iterator,
				value->value.v_doc.data,
				value->value.v_doc.data_len) &&
			bson_iter_next(&iterator))
		{
			const char *key = bson_iter_key(&iterator);

			/* $ref/$id are valid for DBRef */
			if (key[0] == '$')
			{
				return strcmp(key, "$regex") == 0 || strcmp(key, "$ref") == 0 ||
					   strcmp(key, "$id") == 0;
			}
		}
	}

	return true;
}


/*
 * ValidateQueryDocument is a wrapper around CreateQualsFromQueryDocIterator
 * that can be used to validate given query document.
 */
void
ValidateQueryDocument(pgbson *queryDocument)
{
	bson_value_t queryValue = ConvertPgbsonToBsonValue(queryDocument);
	return ValidateQueryDocumentValue(&queryValue);
}


void
ValidateQueryDocumentValue(const bson_value_t *value)
{
	if (value->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INTERNALERROR),
						errmsg("Validate query must be given a document - not: %s",
							   BsonTypeName(value->value_type))));
	}

	bson_iter_t queryDocIter;
	BsonValueInitIterator(value, &queryDocIter);

	BsonQueryOperatorContext context = {
		.documentExpr = (Expr *) MakeSimpleDocumentVar(),
		.inputType = MongoQueryOperatorInputType_Bson,
		.simplifyOperators = false,
		.coerceOperatorExprIfApplicable = false,
		.convertSupportedInToScalarArrayOp = false,
		.requiredFilterPathNameHashSet = NULL,
		.variableContext = NULL,
	};

	CreateQualsFromQueryDocIterator(&queryDocIter, &context);
}


/*
 * QueryDocumentsAreEquivalent returns true if given query documents are
 * equivalent.
 */
bool
QueryDocumentsAreEquivalent(const pgbson *leftQueryDocument,
							const pgbson *rightQueryDocument,
							bool convertSupportedInToScalarArrayOp)
{
	bson_iter_t leftDocumentIter;
	PgbsonInitIterator(leftQueryDocument, &leftDocumentIter);

	BsonQueryOperatorContext leftDocOpContext = {
		.documentExpr = (Expr *) MakeSimpleDocumentVar(),
		.inputType = MongoQueryOperatorInputType_Bson,
		.simplifyOperators = false,
		.coerceOperatorExprIfApplicable = true,
		.convertSupportedInToScalarArrayOp = convertSupportedInToScalarArrayOp,
		.requiredFilterPathNameHashSet = NULL,
		.variableContext = NULL,
	};

	List *leftDocumentQuals = CreateQualsFromQueryDocIterator(&leftDocumentIter,
															  &leftDocOpContext);

	bson_iter_t rightDocumentIter;
	PgbsonInitIterator(rightQueryDocument, &rightDocumentIter);

	BsonQueryOperatorContext rightDocOpContext = {
		.documentExpr = (Expr *) MakeSimpleDocumentVar(),
		.inputType = MongoQueryOperatorInputType_Bson,
		.simplifyOperators = false,
		.coerceOperatorExprIfApplicable = true,
		.convertSupportedInToScalarArrayOp = convertSupportedInToScalarArrayOp,
		.requiredFilterPathNameHashSet = NULL,
		.variableContext = NULL,
	};

	List *rightDocumentQuals = CreateQualsFromQueryDocIterator(&rightDocumentIter,
															   &rightDocOpContext);

	bool weak = false;
	return predicate_implied_by(leftDocumentQuals, rightDocumentQuals, weak) &&
		   predicate_implied_by(rightDocumentQuals, leftDocumentQuals, weak);
}


/*
 * CreateQualsFromQueryDocIterator constructs a list of quals from a
 * query document iterator, which can be recursively called for logical
 * expressions.
 */
List *
CreateQualsFromQueryDocIterator(bson_iter_t *queryDocIterator,
								BsonQueryOperatorContext *context)
{
	const char *traversedPath = NULL;
	return CreateQualsFromQueryDocIteratorInternal(queryDocIterator, context,
												   traversedPath);
}


/*
 * if $ref and $id exist in the queryDocIterator, it needs to be treated as childPath,
 * and the quals need to be created for the childPath.
 * else should be treated as a regular queryDocIterator.
 */
static Expr *
CreateQualsForDBRef(const char *operatorName, bson_iter_t *refIterator,
					BsonQueryOperatorContext *context,
					const char *path)
{
	bson_value_t dbrefValue;
	pgbson_writer writer;
	PgbsonWriterInit(&writer);
	PgbsonWriterAppendValue(&writer, operatorName, strlen(operatorName), bson_iter_value(
								refIterator));

	while (bson_iter_next(refIterator))
	{
		PgbsonWriterAppendIter(&writer, refIterator);
	}
	PgbsonWriterCopyDocumentDataToBsonValue(&writer, &dbrefValue);
	const MongoQueryOperator *eqOperator = GetMongoQueryOperatorByQueryOperatorType(
		QUERY_OPERATOR_EQ, context->inputType);

	return CreateFuncExprForQueryOperator(context, path,
										  eqOperator, &dbrefValue);
}


/*
 * Core implementation of CreateQualsFromQueryDocIterator.
 */
static List *
CreateQualsFromQueryDocIteratorInternal(bson_iter_t *queryDocIterator,
										BsonQueryOperatorContext *context,
										const char *traversedPath)
{
	List *quals = NIL;
	check_stack_depth();
	while (bson_iter_next(queryDocIterator))
	{
		/* field or logical operator */
		const char *path = bson_iter_key(queryDocIterator);
		Expr *qual;

		if (path[0] == '$')
		{
			/* we expect all top-level operators to be logical expressions */
			qual = CreateBoolExprFromLogicalExpression(queryDocIterator, context,
													   traversedPath);
		}
		else
		{
			/* all other paths are comparisons */
			qual = CreateOpExprFromComparisonExpression(queryDocIterator, context,
														traversedPath,
														path);
		}

		if (qual != NULL)
		{
			quals = lappend(quals, qual);
		}
	}

	return quals;
}


/*
 * CreateBoolExprFromLogicalExpression converts $and, $or, $nor and $not
 * expressions to Postgres expressions.
 */
static Expr *
CreateBoolExprFromLogicalExpression(bson_iter_t *queryDocIterator,
									BsonQueryOperatorContext *context,
									const char *traversedPath)
{
	const char *mongoOperatorName = bson_iter_key(queryDocIterator);
	const MongoQueryOperator *operator = GetMongoQueryOperatorByMongoOpName(
		mongoOperatorName, context->inputType);

	/*
	 * Increment the feature counter for the operator,
	 * make sure we don't attempt to read out of range feature as this
	 * is in shared memory space
	 */
	if (operator->featureType >= 0 && operator->featureType < MAX_FEATURE_INDEX)
	{
		ReportFeatureUsage(operator->featureType);
	}

	MongoQueryOperatorType operatorType = operator->operatorType;

	if (operatorType == QUERY_OPERATOR_COMMENT)
	{
		/* Skip $comment */
		return NULL;
	}

	if (operatorType != QUERY_OPERATOR_AND &&
		operatorType != QUERY_OPERATOR_OR &&
		operatorType != QUERY_OPERATOR_NOR &&
		operatorType != QUERY_OPERATOR_EXPR &&
		operatorType != QUERY_OPERATOR_TEXT &&
		operatorType != QUERY_OPERATOR_ALWAYS_TRUE &&
		operatorType != QUERY_OPERATOR_ALWAYS_FALSE &&
		operatorType != QUERY_OPERATOR_SAMPLERATE &&
		operatorType != QUERY_OPERATOR_JSONSCHEMA)
	{
		/* invalid query operator such as $eq at top level of query document */
		/* We throw feature not supported since $where and such might be specified here */
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
						errmsg(
							"unknown top level operator: %s. If you have a field "
							"name that starts with a '$' symbol, consider using "
							"$getField or $setField.",
							mongoOperatorName),
						errdetail_log(
							"unknown top level operator: %s. If you have a field "
							"name that starts with a '$' symbol, consider using "
							"$getField or $setField.",
							mongoOperatorName)));
	}

	if (operatorType == QUERY_OPERATOR_ALWAYS_TRUE ||
		operatorType == QUERY_OPERATOR_ALWAYS_FALSE)
	{
		const bson_value_t *value = bson_iter_value(queryDocIterator);
		if (!BsonValueIsNumberOrBool(value) || BsonValueAsInt32(value) != 1)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
							errmsg("%s must always be an integer value of exactly 1",
								   operatorType == QUERY_OPERATOR_ALWAYS_TRUE ?
								   "$alwaysTrue" : "$alwaysFalse")));
		}

		bool isNull = false;
		return (Expr *) makeBoolConst(operatorType == QUERY_OPERATOR_ALWAYS_TRUE, isNull);
	}

	if (operatorType == QUERY_OPERATOR_SAMPLERATE)
	{
		/* This expression { $sampleRate: 0.33 } is equivalent to
		 *  using the $rand: { $lt: [ { $rand: {} }, 0.33 ] } */
		const bson_value_t *sampleRate = bson_iter_value(queryDocIterator);
		if (!BsonValueIsNumber(sampleRate))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg(
								"The operator $sampleRate requires an argument of numeric data type."),
							errdetail_log("argument to $sampleRate is: %s",
										  BsonValueToJsonForLogging(sampleRate))));
		}
		double sampleRateValue = BsonValueAsDouble(sampleRate);

		/* sampleRate value outside of [0, 1] is invalid.
		 * use de-morgan's law to handle nan, because the comparisons involving NaN are always false.
		 * ref: https://standards.ieee.org/ieee/754/6210, boolean sameQuantum(source, source) */
		if (!(sampleRateValue >= 0 && sampleRateValue <= 1))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg(
								"numeric argument to $sampleRate must be in [0, 1]"),
							errdetail_log(
								"numeric argument to $sampleRate is: %f",
								sampleRateValue)));
		}

		/* PostgresDrandomFunctionId() returns a double precision float
		 * in a continuous uniform distribution between [0.0, 1.0).
		 * if sample_rate=0.0, zero documents will be selected
		 * if sample_rate=1.0, all documents will be selected */
		Expr *randomValueExpr = (Expr *) makeFuncExpr(PostgresDrandomFunctionId(),
													  FLOAT8OID,
													  NIL,
													  InvalidOid, InvalidOid,
													  COERCE_EXPLICIT_CALL);

		Expr *constExpr = (Expr *) makeConst(FLOAT8OID, -1, InvalidOid, sizeof(float8),
											 Float8GetDatum(sampleRateValue), false,
											 true);
		Expr *opExpr = make_opclause(Float8LessOperator, BOOLOID, false,
									 randomValueExpr, constExpr, InvalidOid, InvalidOid);
		return opExpr;
	}

	if (operatorType == QUERY_OPERATOR_EXPR)
	{
		if (context->inputType != MongoQueryOperatorInputType_Bson)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg(
								"$expr can only be applied to the top-level document")));
		}

		if (context->variableContext != NULL)
		{
			/* $expr with let */
			Const *constValue = CreateConstFromBsonValue("", bson_iter_value(
															 queryDocIterator),
														 context->collationString);
			return (Expr *) makeFuncExpr(BsonExprWithLetFunctionId(), BOOLOID,
										 list_make3(context->documentExpr, constValue,
													context->variableContext),
										 InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
		}

		/* Special case for $expr */
		const char *path = "";
		return CreateFuncExprForQueryOperator(context,
											  path,
											  operator,
											  bson_iter_value(queryDocIterator));
	}

	if (operatorType == QUERY_OPERATOR_TEXT)
	{
		if (context->hasOperatorRestrictions)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg(
								"$text cannot be used in this specific context")));
		}

		if (context->inputType != MongoQueryOperatorInputType_Bson)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg(
								"$text can only be applied to the top-level document")));
		}

		/* Special handling required for operator text */
		const char *path = "";
		BsonValidateTextQuery(bson_iter_value(queryDocIterator));
		return CreateFuncExprForQueryOperator(context,
											  path,
											  operator,
											  bson_iter_value(queryDocIterator));
	}

	if (operatorType == QUERY_OPERATOR_JSONSCHEMA)
	{
		if (context->inputType != MongoQueryOperatorInputType_Bson)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg(
								"$jsonSchema can only be applied to the top-level document")));
		}

		/* Special case for $jsonSchema */
		/* validate $jsonSchema syntax*/
		SchemaTreeState localTreeStateIgnore = { };
		bson_iter_t schemaIter;
		const bson_value_t *queryDocValue = bson_iter_value(queryDocIterator);
		BsonValueInitIterator(queryDocValue, &schemaIter);
		BuildSchemaTree(&localTreeStateIgnore, &schemaIter);

		const char *path = "";
		return CreateFuncExprForQueryOperator(context,
											  path,
											  operator,
											  bson_iter_value(queryDocIterator));
	}

	bson_iter_t logicalExpressionsIterator;

	/* type safety checks */
	if (bson_iter_type(queryDocIterator) != BSON_TYPE_ARRAY)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Expected 'array' type for %s",
							mongoOperatorName)));
	}

	/* open array of expressions (or document in case of $not) */
	bson_iter_recurse(queryDocIterator, &logicalExpressionsIterator);

	/* convert logical query operator to BoolExpr */
	BoolExpr *logicalExpr = makeNode(BoolExpr);

	switch (operatorType)
	{
		case QUERY_OPERATOR_AND:
		{
			List *innerQuals =
				CreateQualsFromLogicalExpressionArrayIterator(&logicalExpressionsIterator,
															  context, traversedPath);

			if (context->simplifyOperators &&
				list_length(innerQuals) == 1)
			{
				/* And of a single entry is just the entry */
				return linitial(innerQuals);
			}

			logicalExpr->boolop = AND_EXPR;
			logicalExpr->args = innerQuals;
			logicalExpr->location = -1;
			break;
		}

		case QUERY_OPERATOR_OR:
		{
			List *innerQuals =
				CreateQualsFromLogicalExpressionArrayIterator(&logicalExpressionsIterator,
															  context, traversedPath);

			/* Special case, if the $or is a simple array of equality on the same path
			 * then it's converted to a $in
			 */
			if (context->simplifyOperators)
			{
				Expr *processedQual = TryProcessOrIntoDollarIn(context, innerQuals);
				if (processedQual != NULL)
				{
					return processedQual;
				}

				processedQual = TryOptimizeDollarOrExpr(context, innerQuals);
				if (processedQual != NULL)
				{
					return processedQual;
				}
			}

			logicalExpr->boolop = OR_EXPR;
			logicalExpr->args = innerQuals;
			logicalExpr->location = -1;
			break;
		}

		case QUERY_OPERATOR_NOR:
		{
			List *innerQuals =
				CreateQualsFromLogicalExpressionArrayIterator(&logicalExpressionsIterator,
															  context, traversedPath);

			BoolExpr *orExpr = makeNode(BoolExpr);
			orExpr->boolop = OR_EXPR;
			orExpr->args = innerQuals;
			orExpr->location = -1;

			Const *falseConst = makeConst(BOOLOID, -1, InvalidOid, 1,
										  BoolGetDatum(false), false, true);

			/* convert NULL to false */
			CoalesceExpr *coalesceExpr = makeNode(CoalesceExpr);
			coalesceExpr->coalescetype = BOOLOID;
			coalesceExpr->coalescecollid = InvalidOid;
			coalesceExpr->args = list_make2(orExpr, falseConst);
			coalesceExpr->location = -1;

			/* negate (NULL and false become true) */
			logicalExpr->boolop = NOT_EXPR;
			logicalExpr->args = list_make1(coalesceExpr);
			logicalExpr->location = -1;
			break;
		}

		default:
		{
			ereport(ERROR, (errmsg("unrecognized logical operator: %d", operatorType)));
		}
	}

	/* Fail if geoNear op was found under $or or $nor along with other filters. */
	if ((operatorType == QUERY_OPERATOR_OR || operatorType == QUERY_OPERATOR_NOR) &&
		context->targetEntries != NULL &&
		TargetListContainsGeonearOp(context->targetEntries))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"$geoNear must appear at the top level of the expression")));
	}

	return (Expr *) logicalExpr;
}


/*
 * CreateQualsFromLogicalExpressionArrayIterator converts all elements of the given array
 * to quals
 */
static List *
CreateQualsFromLogicalExpressionArrayIterator(bson_iter_t *expressionsArrayIterator,
											  BsonQueryOperatorContext *context,
											  const char *traversedPath)
{
	List *quals = NIL;

	while (bson_iter_next(expressionsArrayIterator))
	{
		if (bson_iter_type(expressionsArrayIterator) != BSON_TYPE_DOCUMENT)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
								"$or/$and/$nor entries need to be full objects")));
		}

		/* open expression document */
		bson_iter_t expressionDocIterator;
		bson_iter_recurse(expressionsArrayIterator, &expressionDocIterator);

		/* convert to list of quals */
		List *expressionQuals = CreateQualsFromQueryDocIteratorInternal(
			&expressionDocIterator,
			context,
			traversedPath);

		/* make ANDs of individual expressions explicit */
		Expr *andedExpressionQual = make_ands_explicit(expressionQuals);

		quals = lappend(quals, andedExpressionQual);
	}

	if (quals == NIL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"$or/$and/$nor arrays must have at least one entry")));
	}

	return quals;
}


/* Check if the bson iterator's value type is UNDEFINED
 * iter: iterator which holds the value to be validated
 * isInMatchExpression: true, when the operator for which the
 *          value to be validated is either $in/$nin
 */
static inline void
ValidateIfIteratorValueUndefined(bson_iter_t *iter, bool isInMatchExpression)
{
	if (BSON_ITER_HOLDS_UNDEFINED(iter))
	{
		if (isInMatchExpression)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
								"InMatchExpression equality cannot be undefined")));
		}
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Comparison with undefined value is not allowed")));
	}
}


/*
 * CreateOpExprFromComparisonExpression creates an operator expression of the form
 * (<bson> <operator> <bson> [AND ...]) for a single path from queryDocIterator.
 */
static Expr *
CreateOpExprFromComparisonExpression(bson_iter_t *queryDocIterator,
									 BsonQueryOperatorContext *context,
									 const char *traversedPath,
									 const char *currentBsonKey)
{
	/* Check if the operator value to be validated is for $in/$nin */
	bool isInMatchExpression = false;

	/* Check if the value without operator is 'undefined', for e.g. {x: undefined} */
	ValidateIfIteratorValueUndefined(queryDocIterator, isInMatchExpression);

	const char *key = currentBsonKey;

	StringInfo pathBuffer = makeStringInfo();
	if (traversedPath != NULL)
	{
		appendStringInfo(pathBuffer, "%s.", traversedPath);
	}

	appendStringInfoString(pathBuffer, key);
	char *path = pathBuffer->data;
	const bson_value_t *value = bson_iter_value(queryDocIterator);

	if (bson_iter_type(queryDocIterator) == BSON_TYPE_REGEX)
	{
		/* <path> : {"$regex": "/.../"}, convert to @~ expression  */
		const MongoQueryOperator *regexOperator =
			GetMongoQueryOperatorByQueryOperatorType(
				QUERY_OPERATOR_REGEX, context->inputType);

		return CreateFuncExprForQueryOperator(context, path, regexOperator,
											  value);
	}
	else if (bson_iter_type(queryDocIterator) != BSON_TYPE_DOCUMENT)
	{
		const MongoQueryOperator *eqOperator = GetMongoQueryOperatorByQueryOperatorType(
			QUERY_OPERATOR_EQ, context->inputType);

		/* <path> : <value>, convert to = expression  */
		return CreateFuncExprForQueryOperator(context, path, eqOperator, value);
	}

	/*
	 * We have a <field> : <query operator> pair, where query operator is
	 * of the form { "$op" : <value> }, with the possibility of multiple
	 * operators.
	 */

	/* open query operator BSON document */
	bson_iter_t operatorDocIterator;
	bson_iter_recurse(queryDocIterator, &operatorDocIterator);

	/*
	 * If the document under path is empty or starts with a non-operator,
	 * treat it as an equality comparison.
	 */
	bson_iter_t checkIterator = operatorDocIterator;
	if (!bson_iter_next(&checkIterator) || bson_iter_key(&checkIterator)[0] != '$')
	{
		const MongoQueryOperator *eqOperator = GetMongoQueryOperatorByQueryOperatorType(
			QUERY_OPERATOR_EQ, context->inputType);
		return CreateFuncExprForQueryOperator(context, path, eqOperator, value);
	}

	/*
	 * Operator document of the form { "$op" : <value>, ... }.
	 * Convert operators into corresponding Postgres expressions.
	 */
	return CreateOpExprFromOperatorDocIterator(path, &operatorDocIterator, context);
}


/*
 * Simple wrapper inline function to minimize the switch statement duplication.
 */
inline static Expr *
CreateFuncExprForSimpleQueryOperator(bson_iter_t *operatorDocIterator,
									 BsonQueryOperatorContext *context,
									 const MongoQueryOperator *operator,
									 const char *path)
{
	/* get value we are comparing against */
	const bson_value_t *value = bson_iter_value(operatorDocIterator);

	/* construct the <document> <operator> { <path> : <value> } expression */
	return CreateFuncExprForQueryOperator(
		context,
		path, operator,
		value);
}


/*
 * CreateOpExprFromOperatorDocIterator creates an operator expression of the form
 * (<bson> <operator> <bson> [AND ...]) for a given path and expression of the
 * form { "$op" : <value>, ... }.
 */
static Expr *
CreateOpExprFromOperatorDocIterator(const char *path,
									bson_iter_t *operatorDocIterator,
									BsonQueryOperatorContext *context)
{
	bool regexFound = false;
	bson_value_t *options = NULL;
	List *quals = NIL;

	while (bson_iter_next(operatorDocIterator))
	{
		Expr *qual = CreateOpExprFromOperatorDocIteratorCore(operatorDocIterator, context,
															 path, &regexFound, &options);
		if (qual != NULL)
		{
			quals = lappend(quals, qual);
		}
	}

	/* "options" is initialized only when $options is seen before $regex.
	 * Later when $regex is found, options will be consumed and an expression
	 * is created. It will be then set to NULL. If this
	 * (setting of options to NULL) did not happen, that means there is
	 * $options present as an orphan without a $regex. Hence throw error */
	if (options != NULL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"$options needs a $regex")));
	}

	return make_ands_explicit(quals);
}


Expr *
CreateScalarArrayOpExprForInWithBsonIndexBounds(Expr *documentExpr, const char *path,
												const char *collationString,
												bson_iter_t *arrayIter)
{
	List *inArgsList = NIL;
	while (bson_iter_next(arrayIter))
	{
		const bson_value_t *currentValue = bson_iter_value(arrayIter);
		Const *bsonConst = CreateConstFromBsonValue(
			path, currentValue, collationString);
		bsonConst->consttype = BsonIndexBoundsTypeId();
		inArgsList = lappend(inArgsList, bsonConst);
	}

	/*
	 * In the case where we're operating with a $in and indexes with
	 * PFE we need to coerce the expression to the type of the index
	 */
	if (inArgsList != NIL)
	{
		ScalarArrayOpExpr *inOperator = makeNode(ScalarArrayOpExpr);
		inOperator->useOr = true;
		inOperator->opno = BsonIndexBoundsEqualOperatorId();
		inOperator->opfuncid = BsonIndexBoundsEqualOperatorFuncId();

		/* Second arg is an ArrayExpr containing the documents */
		ArrayExpr *arrayExpr = makeNode(ArrayExpr);
		arrayExpr->array_typeid = GetBsonIndexBoundsArrayTypeOid();
		arrayExpr->element_typeid = BsonIndexBoundsTypeId();
		arrayExpr->multidims = false;
		arrayExpr->elements = inArgsList;
		inOperator->args = list_make2(documentExpr, arrayExpr);

		return (Expr *) inOperator;
	}

	return NULL;
}


static Expr *
CreateOpExprFromOperatorDocIteratorCore(bson_iter_t *operatorDocIterator,
										BsonQueryOperatorContext *context,
										const char *path,
										bool *regexFound,
										bson_value_t **options)
{
	/* Retrieve query operator category */
	const char *mongoOperatorName = bson_iter_key(operatorDocIterator);
	const MongoQueryOperator *operator =
		GetMongoQueryOperatorByMongoOpName(mongoOperatorName, context->inputType);

	/*
	 * Increment the feature counter for the operator,
	 * make sure we don't attempt to read out of range feature as this
	 * is in shared memory space
	 */
	if (operator->featureType >= 0 && operator->featureType < MAX_FEATURE_INDEX)
	{
		ReportFeatureUsage(operator->featureType);
	}

	switch (operator->operatorType)
	{
		case QUERY_OPERATOR_IN:
		case QUERY_OPERATOR_NIN:
		{
			if (!BSON_ITER_HOLDS_ARRAY(operatorDocIterator))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg("Expected 'array' type for %s",
									   mongoOperatorName)));
			}

			bson_iter_t arrayIterator;
			int32_t numValues = 0;
			bson_value_t currentValue = { 0 };
			bool hasRegex = false;
			if (bson_iter_recurse(operatorDocIterator, &arrayIterator))
			{
				while (bson_iter_next(&arrayIterator))
				{
					numValues++;
					currentValue = *bson_iter_value(&arrayIterator);
					hasRegex = hasRegex || BSON_ITER_HOLDS_REGEX(&arrayIterator);
					if (!IsValidBsonDocumentForDollarInOrNinOp(
							&currentValue))
					{
						ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
											"cannot nest $ under %s",
											operator->mongoOperatorName)));
					}
					else
					{
						/* Check if the operator value to be validated is for $in/$nin */
						bool isInMatchExpression = true;
						ValidateIfIteratorValueUndefined(&arrayIterator,
														 isInMatchExpression);
					}
				}
			}

			if (numValues == 0 && context->simplifyOperators)
			{
				/* $in: [] is alwaysFalse; $nin: [] is alwaysTrue */
				bool isNull = false;
				return (Expr *) makeBoolConst(
					operator->operatorType == QUERY_OPERATOR_IN ? false : true, isNull);
			}

			if (numValues == 1 && context->simplifyOperators &&
				!hasRegex)
			{
				/* Special case, $in with a single value is converted to $eq except in the case of Regexes */
				MongoQueryOperatorType operatorType = operator->operatorType ==
													  QUERY_OPERATOR_IN ?
													  QUERY_OPERATOR_EQ :
													  QUERY_OPERATOR_NE;
				const MongoQueryOperator *actualOperator =
					GetMongoQueryOperatorByQueryOperatorType(
						operatorType, context->inputType);
				return CreateFuncExprForQueryOperator(context, path, actualOperator,
													  &currentValue);
			}
			else if (context->convertSupportedInToScalarArrayOp &&
					 context->coerceOperatorExprIfApplicable &&
					 context->inputType == MongoQueryOperatorInputType_Bson &&
					 numValues >= 1 &&
					 operator->operatorType == QUERY_OPERATOR_IN && !hasRegex)
			{
				List *inArgsList = NIL;
				if (bson_iter_recurse(operatorDocIterator, &arrayIterator))
				{
					while (bson_iter_next(&arrayIterator))
					{
						currentValue = *bson_iter_value(&arrayIterator);
						Const *bsonConst = CreateConstFromBsonValue(
							path, &currentValue,
							context->collationString);
						inArgsList = lappend(inArgsList, bsonConst);
					}
				}

				/*
				 * In the case where we're operating with a $in and indexes with
				 * PFE we need to coerce the expression to the type of the index
				 */
				ScalarArrayOpExpr *inOperator = makeNode(ScalarArrayOpExpr);
				inOperator->useOr = true;
				inOperator->opno = BsonEqualMatchRuntimeOperatorId();
				inOperator->opfuncid = BsonEqualMatchRuntimeFunctionId();

				/* Second arg is an ArrayExpr containing the documents */
				ArrayExpr *arrayExpr = makeNode(ArrayExpr);
				arrayExpr->array_typeid = GetClusterBsonQueryArrayTypeId();
				arrayExpr->element_typeid = GetClusterBsonQueryTypeId();
				arrayExpr->multidims = false;
				arrayExpr->elements = inArgsList;
				inOperator->args = list_make2(context->documentExpr, arrayExpr);

				return (Expr *) inOperator;
			}
			else
			{
				Expr *inExpr = CreateFuncExprForSimpleQueryOperator(operatorDocIterator,
																	context, operator,
																	path);

				/*
				 * For $in queries to match indexes that have partial filter expressions
				 * on them, we need a ScalarArrayOpExpr - do this with a custom bson type
				 * that we can then remove in the relpathlist to avoid runtime evaluation
				 */
				if (context->simplifyOperators &&
					context->coerceOperatorExprIfApplicable &&
					context->inputType == MongoQueryOperatorInputType_Bson &&
					operator->operatorType == QUERY_OPERATOR_IN &&
					!hasRegex && !EnablePartialFilterEvalOnPlanner)
				{
					if (bson_iter_recurse(operatorDocIterator, &arrayIterator))
					{
						Expr *inOperator =
							CreateScalarArrayOpExprForInWithBsonIndexBounds(
								context->documentExpr, path,
								context->
								collationString, &arrayIterator);

						if (inOperator != NULL)
						{
							inExpr = (Expr *) make_and_qual((Node *) inExpr,
															(Node *) inOperator);
						}
					}
				}

				return inExpr;
			}
		}

		case QUERY_OPERATOR_ALL:
		{
			if (!BSON_ITER_HOLDS_ARRAY(operatorDocIterator))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg("Expected 'array' type for %s",
									   mongoOperatorName)));
			}

			bson_iter_t arrayIterator;
			if (bson_iter_recurse(operatorDocIterator, &arrayIterator))
			{
				while (bson_iter_next(&arrayIterator))
				{
					/* Check if the operator value to be validated is for $in/$nin */
					bool isInMatchExpression = false;
					ValidateIfIteratorValueUndefined(&arrayIterator,
													 isInMatchExpression);
				}
			}

			if (context->simplifyOperators &&
				context->inputType == MongoQueryOperatorInputType_Bson)
			{
				return ExpandExprForDollarAll(path, operatorDocIterator,
											  context, operator);
			}

			return CreateExprForDollarAll(path, operatorDocIterator,
										  context, operator);
		}

		case QUERY_OPERATOR_SIZE:
		{
			const bson_value_t *value = bson_iter_value(operatorDocIterator);

			/**
			 * TODO: FIXME - Verify whether strict number check is required here
			 * */
			if (!BsonValueIsNumberOrBool(value))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(
									"Failed to parse $size. Expected a number in: $size: %s",
									BsonValueToJsonForLogging(value)),
								errdetail_log(
									"Failed to parse $size. Expected a number in: $size, found %s",
									BsonTypeName(value->value_type))));
			}
			else
			{
				double doubleValue = BsonValueAsDouble(value);
				if (!IsDoubleAFixedInteger(doubleValue))
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
									errmsg(
										"Failed to parse $size. Expected an integer in: $size: %s",
										BsonValueToJsonForLogging(value)),
									errdetail_log(
										"Failed to parse $size. Expected an integer in: $size, found %s",
										BsonTypeName(value->value_type))));
				}
				else if (doubleValue < 0)
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
										"Parsing of operator $size failed. A non-negative value was expected in operator $size: %s",
										BsonValueToJsonForLogging(value))));
				}
			}

			return CreateFuncExprForSimpleQueryOperator(operatorDocIterator,
														context, operator,
														path);
		}

		case QUERY_OPERATOR_ELEMMATCH:
		{
			if (bson_iter_type(operatorDocIterator) != BSON_TYPE_DOCUMENT)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$elemMatch needs an Object")));
			}

			return CreateFuncExprForSimpleQueryOperator(operatorDocIterator,
														context, operator,
														path);
		}

		case QUERY_OPERATOR_TYPE:
		{
			const bson_value_t *typeIdValue = bson_iter_value(operatorDocIterator);
			if (BSON_ITER_HOLDS_UTF8(operatorDocIterator))
			{
				/* try to resolve the type */
				EnsureValidTypeNameForDollarType(typeIdValue->value.v_utf8.str);
			}
			else if (BSON_ITER_HOLDS_NUMBER(operatorDocIterator))
			{
				double doubleValue = BsonValueAsDouble(typeIdValue);
				if (!IsDoubleAFixedInteger(doubleValue))
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
									errmsg(
										"Unsupported or invalid numerical type code %s",
										BsonValueToJsonForLogging(typeIdValue))));
				}

				/* try to resolve the type */
				int64_t typeNumber = BsonValueAsInt64(typeIdValue);
				EnsureValidTypeCodeForDollarType(typeNumber);
			}
			else if (BSON_ITER_HOLDS_ARRAY(operatorDocIterator))
			{
				bson_iter_t arrayIterator;
				bson_iter_recurse(operatorDocIterator, &arrayIterator);
				bool typeArrayHasElements = false;
				while (bson_iter_next(&arrayIterator))
				{
					typeArrayHasElements = true;
					const bson_value_t *typeIdArrayValue = bson_iter_value(
						&arrayIterator);
					if (BSON_ITER_HOLDS_UTF8(&arrayIterator))
					{
						/* try to resolve the type */
						EnsureValidTypeNameForDollarType(
							typeIdArrayValue->value.v_utf8.str);
					}
					else if (BSON_ITER_HOLDS_NUMBER(&arrayIterator))
					{
						double doubleValue = BsonValueAsDouble(typeIdArrayValue);
						if (!IsDoubleAFixedInteger(doubleValue))
						{
							ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
											errmsg(
												"Unsupported or invalid numerical type code %s",
												BsonValueToJsonForLogging(
													typeIdArrayValue))));
						}

						/* try to resolve the type */
						int64_t typeNumber = BsonValueAsInt64(typeIdArrayValue);
						EnsureValidTypeCodeForDollarType(typeNumber);
					}
					else
					{
						ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
										errmsg(
											"The specified type must be expressed either in numerical form or as a textual string.")));
					}
				}

				if (!typeArrayHasElements)
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),

					                /* Compatibility Notice: The text in this error string is copied verbatim
					                 * from MongoDB output to maintain compatibility with
					                 * existing tools and scripts that rely on specific error message formats.
					                 * Modifying this text may cause unexpected behavior in dependent systems. */
									errmsg("%s must match at least one type", path)));
				}
			}
			else
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(
									"The specified type must be expressed either in numerical form or as a textual string.")));
			}

			return CreateFuncExprForSimpleQueryOperator(operatorDocIterator,
														context, operator,
														path);
		}

		case QUERY_OPERATOR_EXISTS:
		{
			bool existsArg = BsonValueAsBool(bson_iter_value(operatorDocIterator));

			if (existsArg)
			{
				/* In order to support partial filter expressions with $exist: true we need to transform it to a >= MinKey as Postgres partial indexes only support
				 * GT, LT, EQ, NE operator types.
				 */
				const MongoQueryOperator *actualOperator =
					GetMongoQueryOperatorByQueryOperatorType(
						QUERY_OPERATOR_GTE, context->inputType);

				bson_value_t minKeyValue = {
					.value_type = BSON_TYPE_MINKEY
				};

				return CreateFuncExprForQueryOperator(context, path, actualOperator,
													  &minKeyValue);
			}
			else
			{
				/* $exists: false is not supported on partial filter expressions */
				bool isInMatchExpression = false;
				ValidateIfIteratorValueUndefined(operatorDocIterator,
												 isInMatchExpression);
				return CreateFuncExprForSimpleQueryOperator(operatorDocIterator,
															context, operator,
															path);
			}
		}

		case QUERY_OPERATOR_EQ:
		{
			/* Check if the operator value to be validated is for $in/$nin */
			bool isInMatchExpression = false;
			ValidateIfIteratorValueUndefined(operatorDocIterator, isInMatchExpression);
			return CreateFuncExprForSimpleQueryOperator(operatorDocIterator,
														context, operator,
														path);
		}

		case QUERY_OPERATOR_GT:
		case QUERY_OPERATOR_GTE:
		case QUERY_OPERATOR_LT:
		case QUERY_OPERATOR_LTE:
		case QUERY_OPERATOR_NE:
		{
			/* Regex arguments not allowed with these operators */
			if (BSON_ITER_HOLDS_REGEX(operatorDocIterator))
			{
				if (operator->operatorType == QUERY_OPERATOR_NE)
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
									errmsg("Can't have regex as arg to $ne.")));
				}

				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(
									"Can't have RegEx as arg to predicate over field '%s'.",
									path)));
			}

			/* Check if the operator value to be validated is for $in/$nin */
			bool isInMatchExpression = false;
			ValidateIfIteratorValueUndefined(operatorDocIterator, isInMatchExpression);
			return CreateFuncExprForSimpleQueryOperator(operatorDocIterator,
														context, operator,
														path);
		}

		case QUERY_OPERATOR_MOD:
		{
			return CreateExprForDollarMod(operatorDocIterator,
										  context, operator, path);
		}

		case QUERY_OPERATOR_REGEX:
		{
			*regexFound = true;

			return CreateExprForDollarRegex(operatorDocIterator,
											options, context, operator, path);
		}

		case QUERY_OPERATOR_NOT:
		{
			Expr *innerExpr;

			if (bson_iter_type(operatorDocIterator) == BSON_TYPE_DOCUMENT)
			{
				/* open inner document */
				bson_iter_t notIterator;
				bson_iter_recurse(operatorDocIterator, &notIterator);

				/* Throw error if $not spec is empty */
				bson_iter_t checkIterator = notIterator;
				if (!bson_iter_next(&checkIterator))
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
										"Operator $not must not be left empty")));
				}

				/*
				 * Convert query document within { <path> : {"$not" : { ... }}}
				 * to an expression on the original path.
				 */
				innerExpr =
					CreateOpExprFromOperatorDocIterator(path, &notIterator, context);
			}
			else if (bson_iter_type(operatorDocIterator) == BSON_TYPE_REGEX)
			{
				const bson_value_t *regexValue = bson_iter_value(operatorDocIterator);

				const MongoQueryOperator *regexOperator =
					GetMongoQueryOperatorByQueryOperatorType(QUERY_OPERATOR_REGEX,
															 context->inputType);

				/* convert <path> : {"$regex": "/.../"} to @~ expression  */
				innerExpr =
					CreateFuncExprForQueryOperator(context, path,
												   regexOperator, regexValue);
			}
			else
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$not needs a regex or a document")));
			}

			Expr *optimizedExpr = TryOptimizeNotInnerExpr(innerExpr, context);
			if (optimizedExpr != NULL)
			{
				return optimizedExpr;
			}

			Const *falseConst = makeConst(BOOLOID, -1, InvalidOid, 1,
										  BoolGetDatum(false), false, true);

			/* convert NULL to false */
			CoalesceExpr *coalesceExpr = makeNode(CoalesceExpr);
			coalesceExpr->coalescetype = BOOLOID;
			coalesceExpr->coalescecollid = InvalidOid;
			coalesceExpr->args = list_make2(innerExpr, falseConst);
			coalesceExpr->location = -1;

			/* negate (NULL and false become true) */
			BoolExpr *notExpr = makeNode(BoolExpr);
			notExpr->boolop = NOT_EXPR;
			notExpr->args = list_make1(coalesceExpr);
			notExpr->location = -1;
			return (Expr *) notExpr;
		}

		case QUERY_OPERATOR_BITS_ANY_CLEAR:
		case QUERY_OPERATOR_BITS_ALL_CLEAR:
		case QUERY_OPERATOR_BITS_ALL_SET:
		case QUERY_OPERATOR_BITS_ANY_SET:
		{
			return CreateExprForBitwiseQueryOperators(operatorDocIterator,
													  context,
													  operator,
													  path);
		}

		case QUERY_OPERATOR_TEXT:
		{
			ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg(
								"$text operator functionality not implemented yet")));
		}

		case QUERY_OPERATOR_WITHIN:
		case QUERY_OPERATOR_GEOWITHIN:
		{
			if (!BSON_ITER_HOLDS_DOCUMENT(operatorDocIterator))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(
									"Expected 'document' type for Geometry but found '%s' instead",
									BsonTypeName(
										bson_iter_type(operatorDocIterator)))));
			}

			const bson_value_t *value = bson_iter_value(operatorDocIterator);
			bson_value_t shapesValue;
			const ShapeOperator *shapeOperator = GetShapeOperatorByValue(value,
																		 &shapesValue);

			ShapeOperatorInfo *opInfo = palloc0(sizeof(ShapeOperatorInfo));
			opInfo->queryStage = QueryStage_RUNTIME;
			opInfo->queryOperatorType = operator->operatorType;

			/* Only Validate the shapeOperator */
			shapeOperator->getShapeDatum(&shapesValue, opInfo);

			Expr *geoWithinFuncExpr = CreateFuncExprForSimpleQueryOperator(
				operatorDocIterator, context, operator, path);
			return WithIndexSupportExpression(context->documentExpr, geoWithinFuncExpr,
											  path, shapeOperator->isSpherical);
		}

		case QUERY_OPERATOR_GEOINTERSECTS:
		{
			if (!BSON_ITER_HOLDS_DOCUMENT(operatorDocIterator))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(
									"Expected 'document' type for Geometry but found '%s' instead",
									BsonTypeName(
										bson_iter_type(operatorDocIterator)))));
			}

			const bson_value_t *value = bson_iter_value(operatorDocIterator);
			bson_value_t shapesValue;
			const ShapeOperator *shapeOperator = GetShapeOperatorByValue(value,
																		 &shapesValue);

			if (shapeOperator->op != GeospatialShapeOperator_GEOMETRY)
			{
				/* In mongo $centerSphere with $geoIntersects does not throw error but it should
				 * https://jira.mongodb.org/browse/SERVER-30390
				 */

				ereport(ERROR, (
							errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg(
								"$geoIntersect is not supported with the given geometry input: %s",
								BsonValueToJsonForLogging(value)),
							errdetail_log(
								"$geoIntersect is not supported with the given geometry input.")));
			}

			ShapeOperatorInfo *opInfo = palloc0(sizeof(ShapeOperatorInfo));
			opInfo->queryOperatorType = operator->operatorType;

			/* Validate the query at planning */
			shapeOperator->getShapeDatum(&shapesValue, opInfo);

			Expr *geoIntersectsFuncExpr = CreateFuncExprForSimpleQueryOperator(
				operatorDocIterator, context, operator, path);
			return WithIndexSupportExpression(context->documentExpr,
											  geoIntersectsFuncExpr,
											  path, shapeOperator->isSpherical);
		}

		case QUERY_OPERATOR_NEAR:
		case QUERY_OPERATOR_NEARSPHERE:
		case QUERY_OPERATOR_GEONEAR:
		{
			if (context->hasOperatorRestrictions)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(
									"%s cannot be used in this particular context",
									mongoOperatorName)));
			}

			if (!BSON_ITER_HOLDS_DOCUMENT(operatorDocIterator) &&
				!BSON_ITER_HOLDS_ARRAY(operatorDocIterator))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg("near must be first in: { $near: %s }",
									   BsonValueToJsonForLogging(
										   bson_iter_value(operatorDocIterator)))));
			}

			return ParseBsonValueForNearAndCreateOpExpr(operatorDocIterator, context,
														path, mongoOperatorName);
		}

		/* logical operators are not supposed to be recognized at this level */
		case QUERY_OPERATOR_AND:
		case QUERY_OPERATOR_NOR:
		case QUERY_OPERATOR_OR:

		default:
		{
			if (strcmp(mongoOperatorName, "$options") == 0)
			{
				if (*regexFound)
				{
					/* Just ignore this $options as this is already dealt when processing $regex */
					*regexFound = false;
					return NULL;
				}

				/* This happens when options for $regex is provided before
				 * $regex. Then store the Options string in "options" and
				 * use it when $regex is hit next */
				*options = palloc(sizeof(bson_value_t));
				bson_value_copy(bson_iter_value(operatorDocIterator), *options);

				return NULL;
			}

			if (strcmp(mongoOperatorName, "$ref") == 0 ||
				strcmp(mongoOperatorName, "$id") == 0)
			{
				bool isRef = strcmp(mongoOperatorName, "$ref") == 0;
				bson_iter_t refIterator = *operatorDocIterator;

				if (!bson_iter_next(&refIterator))
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
										"Unrecognized operator specified: %s",
										mongoOperatorName),
									errdetail_log("Unrecognized operator specified: %s",
												  mongoOperatorName)));
				}

				/*If queryDoc is a string like {'$id': '', '$ref': ''}, treat it as a DBRef as well */
				if ((isRef && strcmp(bson_iter_key(&refIterator), "$id") == 0) ||
					(!isRef && strcmp(bson_iter_key(&refIterator), "$ref") == 0))
				{
					return CreateQualsForDBRef(mongoOperatorName, operatorDocIterator,
											   context, path);
				}
				else
				{
					if (isRef && strlen(bson_iter_key(&refIterator)) > 0 && bson_iter_key(
							&refIterator)[0] != '$')
					{
						ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION55), errmsg(
											"The $ref field should always be directly followed by $id field.")));
					}
					else
					{
						ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
											"Unrecognized operator specified: %s",
											mongoOperatorName),
										errdetail_log(
											"Unrecognized operator specified: %s",
											mongoOperatorName)));
					}
				}
			}

			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
								"Unrecognized operator specified: %s",
								mongoOperatorName),
							errdetail_log("Unrecognized operator specified: %s",
										  mongoOperatorName)));
		}
	}
}


/*
 * Validates that a given string type name is correct as
 * an input for $type.
 */
static void
EnsureValidTypeNameForDollarType(const char *typeName)
{
	/* Special case for number */
	if (strcmp(typeName, "number") == 0)
	{
		return;
	}

	BsonTypeFromName(typeName);
}


/*
 * Validates that a given type code is valid as an input for
 * $type and throws if it is not.
 */
static void
EnsureValidTypeCodeForDollarType(int64_t typeCode)
{
	bson_type_t ignoreType;
	if (!TryGetTypeFromInt64(typeCode, &ignoreType))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Invalid numerical type code: %lld",
							(long long int) typeCode)));
	}
}


/*
 * Creates Expression for
 * {"path" : { "$bitsOp": value} }.
 */
static Expr *
CreateExprForBitwiseQueryOperators(bson_iter_t *operatorDocIterator,
								   BsonQueryOperatorContext *context,
								   const MongoQueryOperator *operator,
								   const char *path)
{
	pgbson_writer writer;
	PgbsonWriterInit(&writer);
	const bson_value_t *operatorDocValue = bson_iter_value(operatorDocIterator);
	bson_type_t operatorDocValueType = bson_iter_type(operatorDocIterator);
	bool isInputArrayAlreadySorted = false;
	Expr *qual;

	switch (operatorDocValueType)
	{
		case BSON_TYPE_INT64:
		{
			/*
			 * NOTE: Mongo does not throw the exception but they claim in their
			 * public doc that numeric bitmask should fit into 32 bit signed int
			 */
			int int32Val = BsonValueAsInt32(operatorDocValue);
			int64 int64Val = operatorDocValue->value.v_int64;

			if (int32Val != int64Val)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
								errmsg(
									"Unable to store as a 32-bit integer: %s: %s.0",
									operator->mongoOperatorName,
									BsonValueToJsonForLogging(
										operatorDocValue)),
								errdetail_log(
									"Cannot represent argument of type %s as a 32-bit integer in operator: %s",
									BsonTypeName(operatorDocValue->value_type),
									operator->mongoOperatorName)));
			}

			int dataLength = sizeof(int32Val);
			WriteSetBitPositionArray((uint8_t *) &int32Val, dataLength, &writer);
			break;
		}

		case BSON_TYPE_DECIMAL128:
		{
			if (!IsDecimal128InInt32Range(operatorDocValue) ||
				!IsDecimal128AFixedInteger(operatorDocValue))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
								errmsg(
									"Unable to store as a 32-bit integer: %s: %s.0",
									operator->mongoOperatorName,
									BsonValueToJsonForLogging(
										operatorDocValue)),
								errdetail_log(
									"Cannot represent argument of type %s as a 32-bit integer in operator: %s",
									BsonTypeName(operatorDocValue->value_type),
									operator->mongoOperatorName)));
			}

			int intVal = BsonValueAsInt32(operatorDocValue);
			if (intVal < 0)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
								errmsg(
									"A positive number was expected in the following context: %s: %d.0",
									operator->mongoOperatorName,
									intVal),
								errdetail_log(
									"A positive number was expected in the following context: %s: %d.0",
									operator->mongoOperatorName,
									intVal)));
			}

			int dataLength = sizeof(intVal);
			WriteSetBitPositionArray((uint8_t *) &intVal, dataLength, &writer);
			break;
		}

		case BSON_TYPE_DOUBLE:
		{
			double doubleVal = BsonValueAsDouble(operatorDocValue);

			if (!IsDoubleAFixedInteger(doubleVal))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
								errmsg(
									"Expected value of integer type: %s: %s",
									operator->mongoOperatorName,
									BsonValueToJsonForLogging(
										operatorDocValue)),
								errdetail_log(
									"Expected value of integer type: %s: %s",
									operator->mongoOperatorName,
									BsonTypeName(operatorDocValue->value_type))));
			}

			int intVal = BsonValueAsInt32(operatorDocValue);

			if (intVal < 0)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE), errmsg(
									"A positive number was expected in the following context: %s: %d.0",
									operator->mongoOperatorName,
									intVal)));
			}

			int dataLength = sizeof(intVal);
			WriteSetBitPositionArray((uint8_t *) &intVal, dataLength, &writer);
			break;
		}

		case BSON_TYPE_INT32:
		{
			int intVal = operatorDocValue->value.v_int32;

			/* Negative integer is an incorrect input */
			if (intVal < 0)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE), errmsg(
									"A positive number was expected in the following context: %s: %d.0",
									operator->mongoOperatorName,
									intVal)));
			}

			/* convert filter to set bit position array */
			int dataLength = sizeof(intVal);
			WriteSetBitPositionArray((uint8_t *) &intVal, dataLength, &writer);
			break;
		}

		case BSON_TYPE_BINARY:
		{
			/* reading base64 decoded string from operatorDocValue*/
			unsigned char *decodedData = operatorDocValue->value.v_binary.data;
			int decodeDataLength = operatorDocValue->value.v_binary.data_len;

			/* convert filter to set bit position array */
			WriteSetBitPositionArray(decodedData, decodeDataLength, &writer);
			break;
		}

		case BSON_TYPE_ARRAY:
		{
			/* Sort Set bit position array */
			isInputArrayAlreadySorted = SortAndWriteInt32BsonTypeArray(operatorDocValue,
																	   &writer,
																	   operator->
																	   mongoOperatorName);
			break;
		}

		default:
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
							errmsg(
								"%s takes an Array, a number, or a BinData but received: %s: \\%s\\",
								path,
								operator->mongoOperatorName,
								BsonValueToJsonForLogging(
									operatorDocValue)),
							errdetail_log(
								"Path takes an Array, a number, or a BinData but received: %s: \\%s\\",
								operator->mongoOperatorName,
								BsonTypeName(
									operatorDocValue->value_type))));
		}
	}


	if (isInputArrayAlreadySorted)
	{
		qual = CreateFuncExprForSimpleQueryOperator(operatorDocIterator,
													context, operator,
													path);
	}
	else
	{
		bson_iter_t arrayIter;
		pgbson *setPositionArrayBson = PgbsonWriterGetPgbson(&writer);
		PgbsonInitIteratorAtPath(setPositionArrayBson, "", &arrayIter);
		qual = CreateFuncExprForSimpleQueryOperator(&arrayIter,
													context, operator,
													path);
	}
	return qual;
}


/*
 * CreateOpExprForQueryOperator returns an FuncExpr to perform comparison
 * defined by the query operator document.
 */
static Expr *
CreateFuncExprForQueryOperator(BsonQueryOperatorContext *context, const char *path,
							   const MongoQueryOperator *operator,
							   const bson_value_t *value)
{
	/* check if the operator requires an index for given path */
	if (context->requiredFilterPathNameHashSet != NULL)
	{
		bool found = false;
		StringView hashEntry = CreateStringViewFromString(path);
		hash_search(context->requiredFilterPathNameHashSet, &hashEntry, HASH_FIND,
					&found);

		if (!found)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg(
								"The index for filter path '%s' was not found, please check whether the index is created.",
								path)));
		}
	}

	Expr *comparison = NULL;

	/* construct left and right side of the comparison */
	Const *constValue = CreateConstFromBsonValue(path, value, context->collationString);

	Oid functionOid = operator->postgresRuntimeFunctionOidLookup();
	if (!OidIsValid(functionOid))
	{
		ereport(ERROR, (errmsg("<bson> %s <bson> operator not defined",
							   operator->mongoOperatorName)));
	}

	constValue->consttype = operator->operandTypeOid();

	if (context->coerceOperatorExprIfApplicable &&
		operator->postgresRuntimeOperatorOidLookup != NULL)
	{
		/* First try to see if the Oid exists */
		Oid operatorOid = operator->postgresRuntimeOperatorOidLookup();
		if (OidIsValid(operatorOid))
		{
			OpExpr *opExpr = (OpExpr *) make_opclause(operatorOid, BOOLOID,
													  false,
													  context->documentExpr,
													  (Expr *) constValue,
													  InvalidOid, InvalidOid);
			opExpr->opfuncid = functionOid;
			return (Expr *) opExpr;
		}
	}

	List *args = list_make2(context->documentExpr, constValue);

	/* construct Func(document, <value>) expression */
	comparison = (Expr *) makeFuncExpr(functionOid, BOOLOID,
									   args, InvalidOid, InvalidOid,
									   COERCE_EXPLICIT_CALL);
	return comparison;
}


/*
 * CreateConstFromBsonValue returns a Const that mimics the output of bson_get_value.
 */
static Const *
CreateConstFromBsonValue(const char *path, const bson_value_t *value,
						 const char *collationString)
{
	/* convert value to BSON Datum */
	pgbson_writer writer;
	PgbsonWriterInit(&writer);
	PgbsonWriterAppendValue(&writer, path, strlen(path), value);

	if (IsCollationApplicable(collationString))
	{
		PgbsonWriterAppendUtf8(&writer, "collation", 9, collationString);
	}

	pgbson *bson = PgbsonWriterGetPgbson(&writer);
	return MakeBsonConst(bson);
}


/*
 * For $all, expand it into an AND of the constituent expressions.
 * This will let the planner do the optimizations for index pushdown and
 * evaluations.
 */
static Expr *
ExpandExprForDollarAll(const char *path,
					   bson_iter_t *operatorDocIterator,
					   BsonQueryOperatorContext *context,
					   const MongoQueryOperator *operator)
{
	bson_iter_t arrayIterator;

	/* open array of elements (or documents in case of $all : [{$elemMatch : {}}...] ) and validate it. */
	bson_iter_recurse(operatorDocIterator, &arrayIterator);
	bool foundObject = false;
	bool foundElement = false;
	bool foundElemMatch = false;

	List *allElements = NIL;
	const MongoQueryOperator *equalOp = GetMongoQueryOperatorByQueryOperatorType(
		QUERY_OPERATOR_EQ,
		context->
		inputType);

	const MongoQueryOperator *regexOp = GetMongoQueryOperatorByQueryOperatorType(
		QUERY_OPERATOR_REGEX,
		context->
		inputType);
	while (bson_iter_next(&arrayIterator))
	{
		const bson_value_t *value = bson_iter_value(&arrayIterator);

		if (value->value_type == BSON_TYPE_REGEX)
		{
			if (foundObject)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$all/$elemMatch has to be consistent")));
			}

			foundElement = true;
			Expr *allExpr = CreateFuncExprForQueryOperator(context, path, regexOp, value);
			allElements = lappend(allElements, allExpr);
			continue;
		}
		else if (value->value_type != BSON_TYPE_DOCUMENT)
		{
			if (foundObject)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$all/$elemMatch has to be consistent")));
			}

			foundElement = true;
			Expr *allExpr = CreateFuncExprForQueryOperator(context, path, equalOp, value);
			allElements = lappend(allElements, allExpr);
			continue;
		}

		/* We know it's a document at this point */
		/* if an empty document. Consider it as foundElement. */
		if (IsBsonValueEmptyDocument(value))
		{
			if (foundObject)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$all/$elemMatch has to be consistent")));
			}

			foundElement = true;
			Expr *allExpr = CreateFuncExprForQueryOperator(context, path, equalOp, value);
			allElements = lappend(allElements, allExpr);
			continue;
		}

		pgbsonelement singleElement;
		if (!TryGetBsonValueToPgbsonElement(value, &singleElement))
		{
			/* It's a document but not an operator - treat as element but track that it's not with
			 * an object
			 */
			if (foundObject)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$all/$elemMatch has to be consistent")));
			}

			foundElement = true;
			Expr *allExpr = CreateFuncExprForQueryOperator(context, path, equalOp, value);
			allElements = lappend(allElements, allExpr);
			continue;
		}

		/* it is expression of form {path : value}, Consider it as foundElement. */
		if (singleElement.pathLength > 0 && singleElement.path[0] != '$')
		{
			if (foundObject)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$all/$elemMatch has to be consistent")));
			}

			/* It's an element, treat it as $eq on the value */
			foundElement = true;
			Expr *allExpr = CreateFuncExprForQueryOperator(context, path, equalOp, value);
			allElements = lappend(allElements, allExpr);
			continue;
		}

		const MongoQueryOperator *keyOp = GetMongoQueryOperatorByMongoOpName(
			singleElement.path,
			context->
			inputType);
		MongoQueryOperatorType operatorType = keyOp->operatorType;

		if (operatorType == QUERY_OPERATOR_AND || operatorType == QUERY_OPERATOR_OR ||
			operatorType == QUERY_OPERATOR_NOR || operatorType == QUERY_OPERATOR_NOT)
		{
			if (foundObject)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$all/$elemMatch has to be consistent")));
			}
			foundElement = true;
			Expr *allExpr = CreateFuncExprForQueryOperator(context, path, equalOp, value);
			allElements = lappend(allElements, allExpr);
			continue;
		}

		if (foundElement)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
								"no $ expressions in $all")));
		}
		else if (foundElemMatch && keyOp->operatorType != QUERY_OPERATOR_ELEMMATCH)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
								"$all/$elemMatch has to be consistent")));
		}
		else if (keyOp->operatorType != QUERY_OPERATOR_ELEMMATCH)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
								"no $ expressions in $all")));
		}
		else
		{
			Expr *allExpr = CreateFuncExprForQueryOperator(context, path, keyOp,
														   &singleElement.bsonValue);
			allElements = lappend(allElements, allExpr);
			foundElemMatch = true;
		}

		foundObject = true;
	}

	if (list_length(allElements) == 0)
	{
		/* $all of empty array is false */
		return (Expr *) makeBoolConst(false, false);
	}

	if (list_length(allElements) == 1)
	{
		/* If there is a single element remaining, return it immediately without further processing */
		return (Expr *) linitial(allElements);
	}

	return make_ands_explicit(allElements);
}


/*
 * Creates Expression quals for $all : [...]. This also validates the array content for various scenarios and reports error accordingly.
 * All of the array elements could contain either simple elements or $elemMatch expressions (not both together).
 */
static Expr *
CreateExprForDollarAll(const char *path,
					   bson_iter_t *operatorDocIterator,
					   BsonQueryOperatorContext *context,
					   const MongoQueryOperator *operator)
{
	bson_iter_t arrayIterator;

	/* open array of elements (or documents in case of $all : [{$elemMatch : {}}...] ) and validate it. */
	bson_iter_recurse(operatorDocIterator, &arrayIterator);
	bool foundObject = false;
	bool foundElement = false;
	bool foundElemMatch = false;

	while (bson_iter_next(&arrayIterator))
	{
		if (bson_iter_type(&arrayIterator) != BSON_TYPE_DOCUMENT)
		{
			if (foundObject)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$all/$elemMatch has to be consistent")));
			}

			foundElement = true;
			continue;
		}

		bson_iter_t docIterator;
		bson_iter_recurse(&arrayIterator, &docIterator);

		/* if an empty document. Consider it as foundElement. */
		if (!bson_iter_next(&docIterator))
		{
			if (foundObject)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$all/$elemMatch has to be consistent")));
			}
			foundElement = true;
			continue;
		}

		const char *bsonKey = bson_iter_key(&docIterator);

		/* it is expression of form {path : value}, Consider it as foundElement. */
		if (bsonKey[0] != '$')
		{
			if (foundObject)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$all/$elemMatch has to be consistent")));
			}
			foundElement = true;
			continue;
		}

		const MongoQueryOperator *keyOp = GetMongoQueryOperatorByMongoOpName(bsonKey,
																			 context->
																			 inputType);
		MongoQueryOperatorType operatorType = keyOp->operatorType;

		if (operatorType == QUERY_OPERATOR_AND || operatorType == QUERY_OPERATOR_OR ||
			operatorType == QUERY_OPERATOR_NOR || operatorType == QUERY_OPERATOR_NOT)
		{
			if (foundObject)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"$all/$elemMatch has to be consistent")));
			}
			foundElement = true;
			continue;
		}

		if (foundElement)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
								"no $ expressions in $all")));
		}
		else if (foundElemMatch && keyOp->operatorType != QUERY_OPERATOR_ELEMMATCH)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
								"$all/$elemMatch has to be consistent")));
		}
		else if (keyOp->operatorType != QUERY_OPERATOR_ELEMMATCH)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
								"no $ expressions in $all")));
		}
		else if (keyOp->operatorType == QUERY_OPERATOR_ELEMMATCH)
		{
			foundElemMatch = true;
		}

		foundObject = true;
	}

	return CreateFuncExprForSimpleQueryOperator(operatorDocIterator,
												context, operator,
												path);
}


/*
 * CreateShardKeyFiltersForQuery returns a filter of the form:
 * shard_key_value = <hash of shard key fields>
 * if the list of quals restricts the shard key to a single value.
 *
 * We currently only extract a single shard key value based on top-level
 * filters and $and.
 */
Expr *
CreateShardKeyFiltersForQuery(const bson_value_t *queryDocument, pgbson *shardKeyBson,
							  uint64_t collectionId, Index collectionVarno,
							  bool *isShardKeyValueCollationAware)
{
	/* compute the hash of the shard key valeus */
	if (shardKeyBson == NULL)
	{
		/* create Const for the hash of shard key field values */
		Datum shardKeyFieldValuesHashDatum = Int64GetDatum(collectionId);
		Const *shardKeyValueConst = makeConst(INT8OID, -1, InvalidOid, 8,
											  shardKeyFieldValuesHashDatum, false, true);

		*isShardKeyValueCollationAware = false;

		/* construct document <operator> <value> expression */
		return CreateShardKeyValueFilter(collectionVarno, shardKeyValueConst);
	}

	/* Now for sharded cases */
	return ComputeShardKeyExprForQueryValue(shardKeyBson, collectionId, queryDocument,
											collectionVarno,
											isShardKeyValueCollationAware);
}


/*
 * GetCollectionByDocumentVar finds the collection referenced by
 * documentExpr if it contains a single Var, or NULL if it not a single Var.
 * First tries to get the collection from a DocumentDb relation (RTE_RELATION with
 * DocumentDb namespace), otherwise falls back to getting it from an ApiSchema.collection call.
 */
static MongoCollection *
GetCollectionByDocumentVar(Expr *documentExpr,
						   Query *currentQuery,
						   Index *collectionVarno,
						   ParamListInfo boundParams,
						   bool *isCollectionBasedRTE)
{
	List *documentVars = pull_var_clause((Node *) documentExpr, 0);
	*isCollectionBasedRTE = false;

	if (list_length(documentVars) != 1)
	{
		return NULL;
	}

	Var *documentVar = linitial(documentVars);
	*collectionVarno = documentVar->varno;

	RangeTblEntry *rte = rt_fetch(documentVar->varno, currentQuery->rtable);

	if (rte->rtekind == RTE_RELATION && rte->relkind == RELKIND_RELATION)
	{
		bool requireShardTable = false;
		return GetMongoCollectionByRelationOid(rte->relid, requireShardTable);
	}
	else if (IsResolvableDocumentDbCollectionBasedRTE(rte, boundParams))
	{
		*isCollectionBasedRTE = true;
		return GetCollectionForRTE(rte, boundParams);
	}
	else
	{
		return NULL;
	}
}


/*
 * GetCollectionForRTE returns the MongoCollection metadata for a given
 * ApiSchema.collection(..) RTE.
 */
static MongoCollection *
GetCollectionForRTE(RangeTblEntry *rte, ParamListInfo boundParams)
{
	Assert(IsResolvableDocumentDbCollectionBasedRTE(rte, boundParams));

	RangeTblFunction *rangeTableFunc = linitial(rte->functions);
	FuncExpr *funcExpr = (FuncExpr *) rangeTableFunc->funcexpr;
	Const *dbConst = GetConstParamValue((Node *) linitial(funcExpr->args),
										boundParams);
	Const *collectionConst = GetConstParamValue((Node *) lsecond(funcExpr->args),
												boundParams);
	Datum databaseNameDatum = dbConst->constvalue;
	Datum collectionNameDatum = collectionConst->constvalue;

	MongoCollection *collection =
		GetMongoCollectionByNameDatum(databaseNameDatum, collectionNameDatum,
									  AccessShareLock);

	return collection;
}


/*
 * CreateZeroShardKeyValueFilter creates a filter of the form shard_key_value = <collection_id>
 * for the given varno (read: rtable index).
 */
Expr *
CreateNonShardedShardKeyValueFilter(int collectionVarno, const
									MongoCollection *collection)
{
	/* reinterpret cast to int64_t. */
	int64_t shardKeyValue = *(int64_t *) &collection->collectionId;
	Const *nonShardedShardKeyConst = makeConst(INT8OID, -1, InvalidOid, 8,
											   Int64GetDatum(shardKeyValue), false, true);

	return CreateShardKeyValueFilter(collectionVarno, nonShardedShardKeyConst);
}


/*
 * Creates a basic object_id <op> <value> op_expr for a given collection var.
 */
Expr *
MakeSimpleIdExpr(const bson_value_t *filterValue, Index collectionVarno, Oid operatorId)
{
	pgbson *qualValue = BsonValueToDocumentPgbson(filterValue);
	Const *documentIdConst = makeConst(BsonTypeId(), -1, InvalidOid, -1,
									   PointerGetDatum(qualValue),
									   false, false);

	/* _id is always the second column in our data tables */
	AttrNumber documentIdAttnum = 2;
	Var *documentIdVar = makeVar(collectionVarno, documentIdAttnum, BsonTypeId(), -1,
								 InvalidOid, 0);

	/* construct object_id <operator> <value> expression */
	Expr *documentIdFilter = make_opclause(operatorId, BOOLOID, false,
										   (Expr *) documentIdVar,
										   (Expr *) documentIdConst, InvalidOid,
										   InvalidOid);
	return documentIdFilter;
}


Expr *
MakeLowerBoundIdExpr(const bson_value_t *filterValue, Index collectionVarno)
{
	bson_value_t minBound = { 0 };

	if (filterValue->value_type == BSON_TYPE_MAXKEY)
	{
		minBound.value_type = BSON_TYPE_MINKEY;
	}
	else
	{
		minBound = GetLowerBound(filterValue->value_type);
	}

	return MakeSimpleIdExpr(&minBound, collectionVarno,
							BsonGreaterThanEqualOperatorId());
}


Expr *
MakeUpperBoundIdExpr(const bson_value_t *filterValue, Index collectionVarno)
{
	/* Since btree doesn't do type bracketing - apply type bracketing here */
	bool isUpperBoundInclusive = false;
	bson_value_t maxBound = { 0 };
	if (filterValue->value_type == BSON_TYPE_MINKEY)
	{
		isUpperBoundInclusive = true;
		maxBound.value_type = BSON_TYPE_MAXKEY;
	}
	else
	{
		maxBound = GetUpperBound(filterValue->value_type,
								 &isUpperBoundInclusive);
	}
	return MakeSimpleIdExpr(&maxBound, collectionVarno,
							isUpperBoundInclusive ?
							BsonLessThanEqualOperatorId() : BsonLessThanOperatorId());
}


static bool
ValidateAndGetObjectIdQual(List *opArgs, IdFilterWalkerContext *context,
						   pgbsonelement *qualElement, const char **collationString)
{
	Expr *firstArg = linitial(opArgs);
	Expr *secondArg = lsecond(opArgs);

	if (!IsA(firstArg, Var))
	{
		return false;
	}

	/* Skip if the qual is not against the document column */
	Var *firstVar = (Var *) firstArg;
	if (firstVar->varattno != DOCUMENT_DATA_TABLE_DOCUMENT_VAR_ATTR_NUMBER ||
		((Index) firstVar->varno) != context->collectionVarno)
	{
		return false;
	}

	if (!IsA(secondArg, Const))
	{
		return false;
	}

	/* We know it's a filter on the document column */
	Const *secondConst = (Const *) secondArg;
	Assert(secondConst->consttype == BsonTypeId() ||
		   secondConst->consttype == BsonQueryTypeId());

	pgbson *qual = DatumGetPgBson(secondConst->constvalue);

	*collationString = PgbsonToSinglePgbsonElementWithCollation(qual,
																qualElement);

	return qualElement->pathLength == IdFieldStringView.length &&
		   strncmp(qualElement->path, IdFieldStringView.string,
				   IdFieldStringView.length) == 0;
}


static void
ConvertQuerySpecToBsonQuery(Expr *querySpec)
{
	if (IsA(querySpec, Const))
	{
		Const *constExpr = (Const *) querySpec;
		constExpr->consttype = BsonQueryTypeId();
	}
	else if (IsA(querySpec, Var))
	{
		Var *varExpr = (Var *) querySpec;
		varExpr->vartype = BsonQueryTypeId();
	}
	else if (IsA(querySpec, Param))
	{
		Param *paramExpr = (Param *) querySpec;
		paramExpr->paramtype = BsonQueryTypeId();
	}
}


static bool
TryConvertExistingExpression(FuncExpr *expr, IdFilterWalkerContext *context,
							 const MongoIndexOperatorInfo *operator)
{
	switch (operator->indexStrategy)
	{
		case BSON_INDEX_STRATEGY_DOLLAR_REGEX:
		{
			if (!IsClusterVersionAtleast(DocDB_V0, 112, 1))
			{
				return false;
			}

			pgbsonelement qualElement = { 0 };
			const char *collationString;
			if (!ValidateAndGetObjectIdQual(expr->args, context, &qualElement,
											&collationString))
			{
				return false;
			}

			/* Update args to point to the object_id variant and return true*/
			Var *objectIdVar = makeVar(context->collectionVarno,
									   DOCUMENT_DATA_TABLE_OBJECT_ID_VAR_ATTR_NUMBER,
									   BsonTypeId(), -1, InvalidOid, 0);
			expr->funcid = BsonRegexObjectIdMatchFunctionId();

			Expr *querySpec = lsecond(expr->args);
			ConvertQuerySpecToBsonQuery(querySpec);
			expr->args = list_make3(linitial(expr->args), objectIdVar, querySpec);

			return true;
		}

		default:
		{
			return false;
		}
	}
}


/*
 * Given the binary arguments of a FuncExpr or OpExpr,
 * A specified Collection Var index in a RangeTable,
 * A mongo operator ($in, $eq, $gt etc), constructs an appropriate
 * Expr if one can be made for an _id filter. Creates a qual for the
 * object_id column in the table and adds it to the list idFilterQuals.
 * The modified list is returned to the caller.
 * e.g.
 * { "_id": { $in: [ 1, 2, 3 ]}} is converted to
 * object_id IN ( { "": 1}, { "": 2 }) etc.
 * Note that this only works for B-tree supported OpIds since the
 * (shard_key_value, object_id) index is a B-tree index.
 */
static void
CheckAndAddIdFilter(List *opArgs, IdFilterWalkerContext *context,
					const MongoIndexOperatorInfo *operator)
{
	pgbsonelement qualElement = { 0 };
	const char *collationString;
	if (!ValidateAndGetObjectIdQual(opArgs, context, &qualElement,
									&collationString))
	{
		return;
	}

	/*  regex. _id can take Code, but Code is agnostic to collation
	 * _id can't be array accrording to mongo senmantics, but it can
	 * be an array in documentdb since we rewrite $or conditions on _id as $in. */
	if (qualElement.bsonValue.value_type == BSON_TYPE_UTF8 ||
		qualElement.bsonValue.value_type == BSON_TYPE_DOCUMENT ||
		qualElement.bsonValue.value_type == BSON_TYPE_ARRAY)
	{
		context->isCollationAware = IsCollationApplicable(collationString);
	}

	Expr *lowerBound = NULL;
	Expr *upperBound = NULL;
	if (GetBtreeIndexBoundQuals(operator->indexStrategy, &qualElement.bsonValue,
								context->collectionVarno, &lowerBound, &upperBound))
	{
		/* Add the bounds to the list of ID filters */
		if (lowerBound)
		{
			context->idQuals = lappend(context->idQuals, lowerBound);
		}
		if (upperBound)
		{
			context->idQuals = lappend(context->idQuals, upperBound);
		}

		if (operator->indexStrategy == BSON_INDEX_STRATEGY_DOLLAR_EQUAL)
		{
			context->isPointReadQuery = true;
		}
	}
}


/*
 * Visitor used by CreateIdFilterForQuery when traversing a given query's
 * qualifiers to extract object_id filters.
 */
static bool
VisitIdFilterExpression(Node *node, IdFilterWalkerContext *context)
{
	CHECK_FOR_INTERRUPTS();

	if (node == NULL)
	{
		return false;
	}

	if (IsA(node, BoolExpr))
	{
		BoolExpr *boolExpr = (BoolExpr *) node;
		if (boolExpr->boolop != AND_EXPR)
		{
			/* Stop traversing on anything but AND */
			return false;
		}

		return expression_tree_walker(node, VisitIdFilterExpression, context);
	}
	else if (IsA(node, OpExpr))
	{
		OpExpr *opExpr = (OpExpr *) node;
		if (opExpr->opfuncid != InvalidOid && list_length(opExpr->args) == 2)
		{
			const MongoIndexOperatorInfo *indexOp =
				GetMongoIndexOperatorInfoByPostgresFuncId(opExpr->opfuncid);
			if (indexOp->indexStrategy != BSON_INDEX_STRATEGY_INVALID)
			{
				CheckAndAddIdFilter(opExpr->args, context, indexOp);
			}
		}

		return false;
	}
	else if (IsA(node, FuncExpr))
	{
		FuncExpr *funcExpr = (FuncExpr *) node;
		const MongoIndexOperatorInfo *indexOp =
			GetMongoIndexOperatorInfoByPostgresFuncId(funcExpr->funcid);

		if (indexOp->indexStrategy != BSON_INDEX_STRATEGY_INVALID &&
			list_length(funcExpr->args) == 2)
		{
			/* For $regex on _id, we can mutate the existing expression if applicable
			 * TODO: Expand this to more scenarios and operators.
			 */
			if (EnableObjectIdFuncExprConversion &&
				TryConvertExistingExpression(funcExpr, context, indexOp))
			{
				return false;
			}

			CheckAndAddIdFilter(funcExpr->args, context, indexOp);
		}

		return false;
	}
	else if (IsA(node, List))
	{
		return expression_tree_walker(node, VisitIdFilterExpression, context);
	}
	else
	{
		/* Don't try to handle any other type of clause (better safe than)
		 * extracting ID filters where we don't know.
		 */
		return false;
	}
}


/*
 * CreateIdFilterForQuery creates an _id = <documentIdValue> filter to include
 * in a query such that we can utilize the primary key index.
 */
Expr *
CreateIdFilterForQuery(List *existingQuals,
					   Index collectionVarno,
					   bool *isCollationAware,
					   bool *isPointRead)
{
	IdFilterWalkerContext walkerContext = { 0 };
	walkerContext.idQuals = NIL;
	walkerContext.collectionVarno = collectionVarno;
	expression_tree_walker((Node *) existingQuals, VisitIdFilterExpression,
						   &walkerContext);

	*isCollationAware = walkerContext.isCollationAware;
	*isPointRead = walkerContext.isPointReadQuery;
	if (walkerContext.idQuals == NIL)
	{
		return NULL;
	}

	return make_ands_explicit(walkerContext.idQuals);
}


/*
 * Validates the Orderby expression and returns whether or not
 * the order by is ascending.
 */
bool
ValidateOrderbyExpressionAndGetIsAscending(pgbson *orderby)
{
	pgbsonelement orderingElement;
	if (!TryGetSinglePgbsonElementFromPgbson(orderby,
											 &orderingElement))
	{
		ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg(
							"Multi-column order not supported yet")));
	}

	/* Validate field name (Copied from the GW )*/
	if (orderingElement.pathLength == 0 ||
		orderingElement.path[0] == '.' ||
		orderingElement.path[orderingElement.pathLength - 1] == '.' ||
		strstr(orderingElement.path, "..") != NULL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
						errmsg("Bad sort specification")));
	}

	/* Try to see if it's an order by $meta */
	if (TryCheckMetaScoreOrderBy(&orderingElement.bsonValue))
	{
		/*
		 * It's a search associated with $text to be dealt with
		 * The runtime function will evaluate the $meta.
		 */
		return false;
	}

	if (!BsonValueIsNumber(&orderingElement.bsonValue))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15974),
						errmsg("Sort direction value %s is not valid",
							   BsonValueToJsonForLogging(
								   &orderingElement.bsonValue))));
	}

	double sortOrderDouble = BsonValueAsDouble(&orderingElement.bsonValue);
	if (isnan(sortOrderDouble) || isinf(sortOrderDouble))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15975),
						errmsg("Sort direction value %s is not valid",
							   BsonValueToJsonForLogging(
								   &orderingElement.bsonValue))));
	}

	int64_t sortOrder = BsonValueAsInt64(&orderingElement.bsonValue);
	if (sortOrder != 1 && sortOrder != -1)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15975),
						errmsg("Sort direction value %s is not valid",
							   BsonValueToJsonForLogging(
								   &orderingElement.bsonValue))));
	}

	return sortOrder == 1;
}


/*
 * Create function expression for $regex after populating
 * the provided options for $regex.
 * This is specifically needed when we run the query from
 * from the mongo client where $regex and $options comes
 * as two different operators even though $options is not
 * a mongo operator. Here we fetch the value of $options
 * and put in the bson_value_t of $regex iterator */
static Expr *
CreateFuncExprForRegexOperator(const bson_value_t *options, const
							   bson_value_t *regexBsonValue,
							   BsonQueryOperatorContext *context,
							   const MongoQueryOperator *operator,
							   const char *path)
{
	ValidateRegexArgument(regexBsonValue);
	bson_value_t regexInputValue;

	if (options != NULL)
	{
		ValidateOptionsArgument(options);
		regexInputValue.value_type = BSON_TYPE_REGEX;
		regexInputValue.value.v_regex.regex = pstrdup(
			regexBsonValue->value.v_regex.regex);
		regexInputValue.value.v_regex.options = options->value.v_utf8.str;

		regexBsonValue = &regexInputValue;
	}

	/* Just need to call RegexCompile here to validate regex pattern.
	 * hence don't need options here */
	RegexCompileDuringPlanning(regexBsonValue->value.v_regex.regex, NULL);

	return CreateFuncExprForQueryOperator(
		context, path,
		operator, regexBsonValue);
}


/*
 * Creates expression for
 * {"path" : { "$regex": <pattern>, "$options" : ""} }.
 */
static Expr *
CreateExprForDollarRegex(bson_iter_t *currIter, bson_value_t **options,
						 BsonQueryOperatorContext *context,
						 const MongoQueryOperator *operator,
						 const char *path)
{
	const bson_value_t *regexBsonValue = bson_iter_value(currIter);
	bson_iter_t optionsIter = *currIter;

	/* This case occurs if $options is given ahead of $regex in the spec */
	if (*options != NULL)
	{
		if (regexBsonValue->value_type == BSON_TYPE_REGEX &&
			strlen(regexBsonValue->value.v_regex.options) != 0)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION51074), errmsg(
								"Options are specified in both the $regex operator and the $options operator")));
		}

		Expr *qual = CreateFuncExprForRegexOperator(*options, regexBsonValue, context,
													operator, path);
		pfree(*options);
		*options = NULL;
		return qual;
	}

	/*
	 * $regex is found. Now look for $options, either immediately following
	 * it or after another possible document iter entry. eg:
	 * t.find({ description: { "$regex": " line ",
	 *                         "$eq": "Single line description.",
	 *                         "$options": "i"
	 *                       }
	 *        });
	 */
	while (bson_iter_next(&optionsIter))
	{
		if (strcmp(bson_iter_key(&optionsIter), "$options") == 0)
		{
			if (regexBsonValue->value_type == BSON_TYPE_REGEX &&
				strlen(regexBsonValue->value.v_regex.options) != 0)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION51075), errmsg(
									"Options are specified in both the $regex and the $options")));
			}

			bson_value_t *optionsBsonVal = (bson_value_t *) bson_iter_value(&optionsIter);
			Expr *qual = CreateFuncExprForRegexOperator(optionsBsonVal, regexBsonValue,
														context, operator, path);
			return qual;
		}
	}

	return CreateFuncExprForRegexOperator(*options, regexBsonValue,
										  context, operator, path);
}


/*
 * Creates expression for
 * {"path" : { "$mod": [ divisor, remainder ]} }.
 */
static Expr *
CreateExprForDollarMod(bson_iter_t *operatorDocIterator,
					   BsonQueryOperatorContext *context,
					   const MongoQueryOperator *operator,
					   const char *path)
{
	if (!BSON_ITER_HOLDS_ARRAY(operatorDocIterator))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"malformed mod, needs to be an array")));
	}

	const bson_value_t *sourceArray = bson_iter_value(operatorDocIterator);
	bson_iter_t arrayIter;
	BsonValueInitIterator(sourceArray, &arrayIter);

	uint8_t numElements = 0;
	while (bson_iter_next(&arrayIter))
	{
		numElements++;
		if (numElements == 3)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
								"malformed mod, too many elements")));
		}
		if (!BSON_ITER_HOLDS_NUMBER(&arrayIter) &&
			!BSON_ITER_HOLDS_DECIMAL128(&arrayIter))
		{
			if (numElements == 1)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"malformed mod, divisor not a number")));
			}
			else
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"malformed mod, remainder not a number")));
			}
		}
		const bson_value_t *arrayVal = bson_iter_value(&arrayIter);
		if (arrayVal->value_type == BSON_TYPE_DECIMAL128 ||
			arrayVal->value_type == BSON_TYPE_DOUBLE)
		{
			bson_value_t dec128Val;
			dec128Val.value_type = BSON_TYPE_DECIMAL128;
			dec128Val.value.v_decimal128 = GetBsonValueAsDecimal128Quantized(arrayVal);

			if (!IsDecimal128Finite(&dec128Val))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(numElements == 1 ?
									   "malformed mod, divisor value is invalid :: caused by :: Unable to coerce NaN/Inf to integral type"
									   :
									   "malformed mod, remainder value is invalid :: caused by :: Unable to coerce NaN/Inf to integral type")));
			}
			if (!IsDecimal128InInt64Range(&dec128Val))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(numElements == 1 ?
									   "malformed mod, divisor value is invalid :: caused by :: Out of bounds coercing to integral value"
									   :
									   "malformed mod, remainder value is invalid :: caused by :: Out of bounds coercing to integral value")));
			}
		}

		if (numElements == 1)
		{
			int64_t divisor = BsonValueAsInt64(arrayVal);
			if (divisor == 0)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"divide by 0 is not allowed")));
			}
		}
	}

	if (numElements < 2)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"malformed mod, not enough elements")));
	}

	return CreateFuncExprForSimpleQueryOperator(operatorDocIterator,
												context, operator,
												path);
}


/*
 * EvaluateBoundParameters evaluates an expression based on the bound parameters specified,
 * expands the resulting values and returns the evaluated operator expression.
 */
Node *
EvaluateBoundParameters(Node *expression, ParamListInfo boundParams)
{
	/*
	 * evaluate constant expressions (e.g. casts)
	 */
	PlannerInfo *planner = NULL;
	if (boundParams != NULL)
	{
		/* Set up largely-dummy planner state */
		Query *query = makeNode(Query);
		query->commandType = CMD_SELECT;

		PlannerGlobal *glob = makeNode(PlannerGlobal);
		glob->boundParams = boundParams;

		planner = makeNode(PlannerInfo);
		planner->parse = query;
		planner->glob = glob;
		planner->query_level = 1;
		planner->planner_cxt = CurrentMemoryContext;
		planner->wt_param_id = -1;
	}

	return eval_const_expressions(planner, expression);
}


/*
 * comparator of qsort algorithm for sort in ascending order
 */
static inline int
qSortAscendingComparator(const void *p1, const void *p2)
{
	return (*(int *) p1 - *(int *) p2);
}


/*
 * this function sorts BSON_TYPE_ARRAY in ascending order only if it not already sorted.
 * if input array is already sorted return true and if it is not sorted apply qsort to sort it and return false.
 * traverse input array and create c array, apply stdlib qsort function and write sorted array to input writer.
 * Note: input array elements should be type of positive int32 otherwise will throw an error.
 * @array : input bson array
 * @writer : writer to write new sorted array
 * @opName : Operator name like $bitsAllClear, $bitsAnyClear (used in error messages)
 */
static bool
SortAndWriteInt32BsonTypeArray(const bson_value_t *bsonArray, pgbson_writer *writer, const
							   char *opName)
{
	bson_iter_t arrayIter;
	BsonValueInitIterator(bsonArray, &arrayIter);
	int arrayLength = 0;
	int prevValue = -1;
	bool isSorted = true;
	bool checkFixedInteger = false;
	while (bson_iter_next(&arrayIter))
	{
		const bson_value_t *element = bson_iter_value(&arrayIter);

		if (!IsBsonValue32BitInteger(element, checkFixedInteger))
		{
			switch (element->value_type)
			{
				case BSON_TYPE_DOUBLE:
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
									errmsg(
										"Expected value of integer type: %s: %s", opName,
										BsonValueToJsonForLogging(element)),
									errdetail_log(
										"Expected value of integer type in operator: %s, found:%s",
										opName,
										BsonTypeName(element->value_type))));
					break;
				}

				case BSON_TYPE_DECIMAL128:
				case BSON_TYPE_INT64:
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
									errmsg(
										"Expected an integer but got '%s' type for element %s.0",
										BsonTypeName(element->value_type),
										BsonValueToJsonForLogging(element)),
									errdetail_log(
										"Expected an integer but got '%s' type",
										BsonTypeName(element->value_type))));
					break;
				}

				default:
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
									errmsg(
										"bit positions must be an integer but got: %d: \\%s\\",
										arrayLength, BsonValueToJsonForLogging(element)),
									errdetail_log(
										"bit positions must be an integer but got: \\%s\\ arrayLength: %d",
										BsonTypeName(element->value_type), arrayLength)));
				}
			}
		}


		int elementValue = BsonValueAsInt32(element);
		if (prevValue > elementValue && isSorted)
		{
			isSorted = false;
		}

		if (elementValue < 0)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE), errmsg(
								"bit positions must be >= 0 but got: %d: \\%s\\",
								arrayLength, BsonValueToJsonForLogging(element))));
		}
		prevValue = elementValue;
		arrayLength++;
	}

	if (isSorted)
	{
		return true;
	}

	BsonValueInitIterator(bsonArray, &arrayIter);
	int *cArray = (int *) palloc(sizeof(int) * arrayLength);
	int currIndex = 0;

	while (bson_iter_next(&arrayIter))
	{
		const bson_value_t *leftIdKey = bson_iter_value(&arrayIter);
		cArray[currIndex++] = BsonValueAsInt32(leftIdKey);
	}

	qsort(cArray, arrayLength, sizeof(int), qSortAscendingComparator);

	pgbson_array_writer arrayWriter;
	PgbsonWriterStartArray(writer, "", 0, &arrayWriter);

	for (int i = 0; i < arrayLength; i++)
	{
		bson_value_t elementValue;
		elementValue.value_type = BSON_TYPE_INT32;
		elementValue.value.v_int32 = cArray[i];
		PgbsonArrayWriterWriteValue(&arrayWriter, &elementValue);
	}

	pfree(cArray);
	PgbsonWriterEndArray(writer, &arrayWriter);

	return false;
}


/*
 * Check for null byte and argument type in a given options argument.
 */
static void
ValidateOptionsArgument(const bson_value_t *argBsonValue)
{
	if (argBsonValue->value_type != BSON_TYPE_UTF8)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Expected 'string' type for $options parameter")));
	}

	/* Only options which mongo supports are i, m, x, s, u*/
	char *valueString = argBsonValue->value.v_utf8.str;
	for (uint32_t index = 0; index < argBsonValue->value.v_utf8.len; index++)
	{
		if (valueString[index] == 'i' || valueString[index] == 'm' ||
			valueString[index] == 'x' || valueString[index] == 's' ||
			valueString[index] == 'u')
		{
			continue;
		}
		else
		{
			ThrowInvalidRegexOptions(valueString[index]);
		}
	}
}


/*
 * Check for null byte and argument type in a given regex argument.
 */
static void
ValidateRegexArgument(const bson_value_t *argBsonValue)
{
	if (argBsonValue->value_type != BSON_TYPE_UTF8 &&
		argBsonValue->value_type != BSON_TYPE_REGEX)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"$regex must be provided with a string value")));
	}

	/* Validate options when input type is BSON_TYPE_REGEX. */
	/* This is different from options validation as we support less flags when bson type is regex as per mongo behaviour */
	if (argBsonValue->value_type == BSON_TYPE_REGEX)
	{
		char *optionStr = argBsonValue->value.v_regex.options;
		int lenOptionStr = strlen(optionStr);
		for (int index = 0; index < lenOptionStr; index++)
		{
			if (optionStr[index] == 'i' || optionStr[index] == 'm' ||
				optionStr[index] == 'u')
			{
				continue;
			}
			else
			{
				ThrowInvalidRegexOptions(optionStr[index]);
			}
		}
	}

	if (argBsonValue->value_type == BSON_TYPE_UTF8 &&
		strlen(argBsonValue->value.v_utf8.str) < argBsonValue->value.v_utf8.len)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"A regular expression is not allowed to include an embedded null byte")));
	}
}


static Expr *
TryProcessOrIntoDollarIn(BsonQueryOperatorContext *context,
						 List *orQuals)
{
	const MongoQueryOperator *op = GetMongoQueryOperatorByQueryOperatorType(
		QUERY_OPERATOR_EQ, context->inputType);

	/* First pass - validate all paths are the same and the $eq operator */
	StringView singlePath = { .length = 0, .string = "" };
	Expr *firstArg = NULL;
	ListCell *cell;
	pgbson_writer inValueWriter;
	PgbsonWriterInit(&inValueWriter);

	pgbson_element_writer elementWriter;
	PgbsonInitObjectElementWriter(&inValueWriter, &elementWriter, "", 0);

	pgbson_array_writer arrayWriter;
	PgbsonElementWriterStartArray(&elementWriter, &arrayWriter);
	foreach(cell, orQuals)
	{
		Expr *currentExpr = lfirst(cell);
		if (IsA(currentExpr, BoolExpr))
		{
			BoolExpr *boolExpr = (BoolExpr *) currentExpr;
			if (boolExpr->boolop == AND_EXPR &&
				list_length(boolExpr->args) == 1)
			{
				/* Flatten single $and */
				currentExpr = linitial(boolExpr->args);
			}
		}

		Oid funcId;
		List *args;
		if (IsA(currentExpr, FuncExpr))
		{
			FuncExpr *expr = (FuncExpr *) currentExpr;
			funcId = expr->funcid;
			args = expr->args;
		}
		else if (IsA(currentExpr, OpExpr))
		{
			OpExpr *opExpr = (OpExpr *) currentExpr;
			funcId = opExpr->opfuncid;
			args = opExpr->args;
		}
		else
		{
			return NULL;
		}

		/* Not a $eq - cannot convert to $in */
		if (funcId != op->postgresRuntimeFunctionOidLookup())
		{
			return NULL;
		}

		Expr *currentFirstArg = linitial(args);
		Expr *secondArg = lsecond(args);

		if (firstArg == NULL)
		{
			firstArg = currentFirstArg;
		}
		else if (!equal(firstArg, currentFirstArg))
		{
			return NULL;
		}

		/* not a const, cannot convert to $in */
		if (!IsA(secondArg, Const))
		{
			return NULL;
		}

		Const *argConst = (Const *) secondArg;
		if (argConst->constisnull)
		{
			return NULL;
		}

		pgbson *value = DatumGetPgBson(argConst->constvalue);
		pgbsonelement valueElement;

		/* we can safely ignore any appended collation string here */
		/* as it will be appended when creating the funcExpr */
		PgbsonToSinglePgbsonElement(value, &valueElement);

		StringView currentPath = {
			.length = valueElement.pathLength, .string = valueElement.path
		};
		if (singlePath.length == 0)
		{
			singlePath = currentPath;
		}
		else if (!StringViewEquals(&singlePath, &currentPath))
		{
			/* $or with different paths - cannot convert to $in */
			return NULL;
		}

		PgbsonArrayWriterWriteValue(&arrayWriter, &valueElement.bsonValue);
	}

	/* Pass 2: At this point all quals are $eq with the same path */
	if (firstArg == NULL || singlePath.length == 0)
	{
		return NULL;
	}

	PgbsonElementWriterEndArray(&elementWriter, &arrayWriter);

	bson_value_t inValue = PgbsonElementWriterGetValue(&elementWriter);

	const MongoQueryOperator *inOperator =
		GetMongoQueryOperatorByQueryOperatorType(QUERY_OPERATOR_IN, context->inputType);
	return CreateFuncExprForQueryOperator(context, singlePath.string, inOperator,
										  &inValue);
}


/*
 * Optimize for common expression scenarios for $or
 * Currently handled optimizations:
 * { PATH: { $exists: false } } OR { PATH: { $eq: null } } -> { $eq: null }
 */
static Expr *
TryOptimizeDollarOrExpr(BsonQueryOperatorContext *context,
						List *orQuals)
{
	if (list_length(orQuals) == 1)
	{
		/* $or of a single qual is the qual */
		return (Expr *) linitial(orQuals);
	}

	/* For now, only optimize when $or has 2 entries */
	if (list_length(orQuals) != 2)
	{
		return NULL;
	}

	const MongoQueryOperator *eqop = GetMongoQueryOperatorByQueryOperatorType(
		QUERY_OPERATOR_EQ, context->inputType);
	const MongoQueryOperator *existsop = GetMongoQueryOperatorByQueryOperatorType(
		QUERY_OPERATOR_EXISTS, context->inputType);

	Expr *firstArg = NULL;
	Expr *equalsNullExpr = NULL;
	StringView singlePathEqualsNull = { 0 };
	StringView singlePathExistsFalse = { 0 };
	ListCell *cell;
	foreach(cell, orQuals)
	{
		Expr *currentExpr = lfirst(cell);

		Oid funcId;
		List *args;
		if (IsA(currentExpr, FuncExpr))
		{
			FuncExpr *expr = (FuncExpr *) currentExpr;
			funcId = expr->funcid;
			args = expr->args;
		}
		else if (IsA(currentExpr, OpExpr))
		{
			OpExpr *opExpr = (OpExpr *) currentExpr;
			funcId = opExpr->opfuncid;
			args = opExpr->args;
		}
		else
		{
			return NULL;
		}

		if (funcId != eqop->postgresRuntimeFunctionOidLookup() &&
			funcId != existsop->postgresRuntimeFunctionOidLookup())
		{
			continue;
		}

		Expr *currentFirstArg = linitial(args);
		Expr *secondArg = lsecond(args);

		if (firstArg == NULL)
		{
			firstArg = currentFirstArg;
		}
		else if (!equal(firstArg, currentFirstArg))
		{
			return NULL;
		}

		/* not a const, cannot optimize */
		if (!IsA(secondArg, Const))
		{
			return NULL;
		}

		Const *argConst = (Const *) secondArg;
		if (argConst->constisnull)
		{
			return NULL;
		}

		pgbson *value = DatumGetPgBson(argConst->constvalue);
		pgbsonelement valueElement;

		/* Since, we are optimizing Or expression not br rewriting a the expression, but by getting rid of
		 * redundant quals from the Quals list, we can ignore the collation*/
		PgbsonToSinglePgbsonElement(value, &valueElement);

		if (funcId == eqop->postgresRuntimeFunctionOidLookup())
		{
			/* Ignore all except { $eq: null } */
			if (valueElement.bsonValue.value_type != BSON_TYPE_NULL)
			{
				continue;
			}

			StringView currentPath = {
				.length = valueElement.pathLength, .string = valueElement.path
			};
			if (singlePathEqualsNull.length == 0)
			{
				singlePathEqualsNull = currentPath;
			}
			else if (!StringViewEquals(&singlePathEqualsNull, &currentPath))
			{
				/* $or with different paths - cannot currently optimize */
				return NULL;
			}

			equalsNullExpr = currentExpr;
		}
		else if (funcId == existsop->postgresRuntimeFunctionOidLookup())
		{
			if (BsonValueAsBool(&valueElement.bsonValue))
			{
				/* Exists true scenarios can be ignored */
				continue;
			}

			StringView currentPath = {
				.length = valueElement.pathLength, .string = valueElement.path
			};
			if (singlePathExistsFalse.length == 0)
			{
				singlePathExistsFalse = currentPath;
			}
			else if (!StringViewEquals(&singlePathExistsFalse, &currentPath))
			{
				/* $or with different paths - cannot currently optimize */
				return NULL;
			}
		}
	}

	if (singlePathExistsFalse.length == 0 || singlePathEqualsNull.length == 0)
	{
		/* Both aren't present can bail */
		return NULL;
	}

	if (!StringViewEquals(&singlePathExistsFalse, &singlePathEqualsNull))
	{
		/* Paths do not match */
		return NULL;
	}

	/* Here we have $eq: null OR $exists: false
	 * return just $eq: null if it is not null.
	 * This is because $eq: null is a superset of $exists: false
	 * and can enable better index optimizations.
	 */
	return equalsNullExpr;
}


/*
 * WithIndexSupportExpression converts the document <geoOperator> query to
 * bson_validate_geometry(document, 'path') <geoOperator> query expression
 * so that this can be matched against Geospatial indexes.
 */
static Expr *
WithIndexSupportExpression(Expr *docExpr, Expr *geoOperatorExpr,
						   const char *path, bool isSpherical)
{
	FuncExpr *geoOperatorFuncExpr = (FuncExpr *) geoOperatorExpr;

	Const *pathConst = makeConst(TEXTOID, -1, InvalidOid, -1, CStringGetTextDatum(path),
								 false, false);

	Oid bsonValidateFunctionId = isSpherical ? BsonValidateGeographyFunctionId() :
								 BsonValidateGeometryFunctionId();
	Oid typeId = isSpherical ? GeographyTypeId() : GeometryTypeId();
	Expr *validateExpr = (Expr *) makeFuncExpr(bsonValidateFunctionId,
											   typeId,
											   list_make2(docExpr,
														  pathConst),
											   InvalidOid,
											   InvalidOid,
											   COERCE_EXPLICIT_CALL);
	List *argsList = list_make2(validateExpr, lsecond(geoOperatorFuncExpr->args));
	geoOperatorFuncExpr->args = argsList;
	return (Expr *) geoOperatorFuncExpr;
}


/*
 * Tries to get the negator for a given query operator if one is available.
 */
static const MongoQueryOperator *
GetNegationOperatorForQueryOperator(const MongoQueryOperator *queryOperator,
									Datum filterValue,
									BsonQueryOperatorContext *context)
{
	/* See if we can convert the Expr into an equivalent NOT version */
	switch (queryOperator->operatorType)
	{
		case QUERY_OPERATOR_EQ:
		{
			return GetMongoQueryOperatorByQueryOperatorType(QUERY_OPERATOR_NE,
															context->inputType);
		}

		case QUERY_OPERATOR_IN:
		{
			return GetMongoQueryOperatorByQueryOperatorType(QUERY_OPERATOR_NIN,
															context->inputType);
		}

		case QUERY_OPERATOR_NE:
		{
			return GetMongoQueryOperatorByQueryOperatorType(QUERY_OPERATOR_EQ,
															context->inputType);
		}

		case QUERY_OPERATOR_GT:
		{
			return GetMongoQueryOperatorByQueryOperatorType(QUERY_OPERATOR_NOT_GT,
															context->inputType);
		}

		case QUERY_OPERATOR_GTE:
		{
			pgbson *filterbson = DatumGetPgBsonPacked(filterValue);
			pgbsonelement greaterElement;
			PgbsonToSinglePgbsonElement(filterbson, &greaterElement);
			if (greaterElement.bsonValue.value_type == BSON_TYPE_MINKEY)
			{
				/* This is the { exists: true } query - don't optimize this */
				return NULL;
			}

			return GetMongoQueryOperatorByQueryOperatorType(QUERY_OPERATOR_NOT_GTE,
															context->inputType);
		}

		case QUERY_OPERATOR_LT:
		{
			return GetMongoQueryOperatorByQueryOperatorType(QUERY_OPERATOR_NOT_LT,
															context->inputType);
		}

		case QUERY_OPERATOR_LTE:
		{
			pgbson *filterbson = DatumGetPgBsonPacked(filterValue);
			pgbsonelement greaterElement;
			PgbsonToSinglePgbsonElement(filterbson, &greaterElement);
			if (greaterElement.bsonValue.value_type == BSON_TYPE_MAXKEY)
			{
				/* This is a cross-type comparison query - don't optimize this */
				return NULL;
			}

			return GetMongoQueryOperatorByQueryOperatorType(QUERY_OPERATOR_NOT_LTE,
															context->inputType);
		}

		default:
		{
			return NULL;
		}
	}
}


/*
 * Tries to optimize an expression that is in a $not operator to see if it
 * can be pushed into a child context.
 */
static Expr *
TryOptimizeNotInnerExpr(Expr *innerExpr, BsonQueryOperatorContext *context)
{
	if (context->inputType != MongoQueryOperatorInputType_Bson ||
		!context->simplifyOperators)
	{
		return NULL;
	}

	const MongoQueryOperator *queryOperator = NULL;
	List *args = NIL;
	if (IsA(innerExpr, FuncExpr))
	{
		FuncExpr *funcExpr = (FuncExpr *) innerExpr;
		queryOperator = GetMongoQueryOperatorByPostgresFuncId(funcExpr->funcid);
		args = funcExpr->args;
	}
	else if (IsA(innerExpr, OpExpr))
	{
		OpExpr *opExpr = (OpExpr *) innerExpr;
		queryOperator = GetMongoQueryOperatorByPostgresFuncId(opExpr->opfuncid);
		args = opExpr->args;
	}

	if (queryOperator == NULL ||
		queryOperator->operatorType == QUERY_OPERATOR_UNKNOWN ||
		list_length(args) != 2)
	{
		return NULL;
	}

	Node *second = lsecond(args);
	if (!IsA(second, Const))
	{
		return NULL;
	}

	Const *secondConst = (Const *) second;

	const MongoQueryOperator *negator = GetNegationOperatorForQueryOperator(queryOperator,
																			secondConst->
																			constvalue,
																			context);
	if (negator == NULL || negator->operatorType == QUERY_OPERATOR_UNKNOWN)
	{
		return NULL;
	}

	Oid negatorFunc = negator->postgresRuntimeFunctionOidLookup();

	if (negatorFunc == InvalidOid)
	{
		return NULL;
	}

	secondConst->consttype = negator->operandTypeOid();
	return (Expr *) makeFuncExpr(negatorFunc, BOOLOID,
								 args, InvalidOid, InvalidOid,
								 COERCE_EXPLICIT_CALL);
}


/* Create quals for $near, $nearSphere and $geoNear. */
static Expr *
ParseBsonValueForNearAndCreateOpExpr(bson_iter_t *operatorDocIterator,
									 BsonQueryOperatorContext *context, const char *path,
									 const char *mongoOperatorName)
{
	const pgbson *queryDoc = GetGeonearSpecFromNearQuery(operatorDocIterator, path,
														 mongoOperatorName);

	/* Check if this is not the 1st $near or $nearSphere occurrence in same query. */
	if (context->targetEntries)
	{
		bool isGeonear = TargetListContainsGeonearOp(context->targetEntries);

		if (isGeonear)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg("Excessive number of geoNear query expressions")));
		}
	}

	/*
	 * In mongo there are different unsupported errors,
	 * but we are throwing a generalized single error
	 * that is thrown by mongo in a majority of contexts.
	 */
	if (context->inputType == MongoQueryOperatorInputType_BsonValue)
	{
		ereport(ERROR,
				(errcode(ERRCODE_DOCUMENTDB_LOCATION5626500),
				 errmsg(
					 "$near, $nearSphere and $geoNear cannot be used here. Use $geoWithin instead.")));
	}

	GeonearRequest *request = ParseGeonearRequest(queryDoc);
	Expr *docExpr = context->documentExpr;

	TargetEntry *sortTargetEntry;
	SortGroupClause *sortGroupClause;
	List *quals = CreateExprForGeonearAndNearSphere(queryDoc, docExpr,
													request, &sortTargetEntry,
													&sortGroupClause);

	context->targetEntries = lappend(context->targetEntries, sortTargetEntry);
	context->sortClauses = lappend(context->sortClauses, sortGroupClause);

	return make_ands_explicit(quals);
}


/*
 * Updates the missing `resno` and `sortgroupref` fields for sortclause
 * based on the existing `query` structure
 */
void
UpdateQueryOperatorContextSortList(Query *query, List *sortClauses,
								   List *targetEntries)
{
	if (!sortClauses || !targetEntries)
	{
		return;
	}

	Assert(list_length(sortClauses) == list_length(targetEntries));

	if ((query->commandType == CMD_UPDATE || query->commandType == CMD_DELETE ||
		 query->hasForUpdate) && TargetListContainsGeonearOp(targetEntries))
	{
		/*
		 * Block geonear, near and nearSphere operators in update, delete operations.
		 * PG natively doesn't support ORDER BY queries with update and delete operations.
		 * Also SELECT ctid... FOR UPDATE isn't guaranteed to have valid ctid if the order by
		 * is pushed to index with recheck.
		 */
		ThrowGeoNearNotAllowedInContextError();
	}

	ParseState *pstate = make_parsestate(NULL);
	pstate->p_next_resno = list_length(query->targetList) + 1;
	pstate->p_expr_kind = EXPR_KIND_ORDER_BY;

	ListCell *targetCell = NULL;
	ListCell *sortCell = NULL;
	forboth(targetCell, targetEntries, sortCell, sortClauses)
	{
		TargetEntry *tle = (TargetEntry *) lfirst(targetCell);
		SortGroupClause *sortClause = (SortGroupClause *) lfirst(sortCell);

		/* update tle resno and ressortgroupref */
		tle->resno = pstate->p_next_resno++;
		sortClause->tleSortGroupRef = assignSortGroupRef(tle, query->targetList);

		query->sortClause = lappend(query->sortClause, sortClause);
		query->targetList = lappend(query->targetList, tle);
	}

	free_parsestate(pstate);
}
