/*-------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation.  All rights reserved.
 *
 * src/planner/bson_aggregation_metadata_queries.c
 *
 * Implementation of the backend query generation for metadata type queries
 * (e.g. listCollections, listIndexes).
 *
 *-------------------------------------------------------------------------
 */

#include <postgres.h>
#include <float.h>
#include <fmgr.h>
#include <miscadmin.h>

#include <access/table.h>
#include <utils/rel.h>
#include <catalog/namespace.h>
#include <optimizer/planner.h>
#include <nodes/nodes.h>
#include <nodes/makefuncs.h>
#include <nodes/nodeFuncs.h>
#include <parser/parser.h>
#include <parser/parse_agg.h>
#include <parser/parse_clause.h>
#include <parser/parse_param.h>
#include <parser/analyze.h>
#include <parser/parse_oper.h>
#include <utils/ruleutils.h>
#include <utils/builtins.h>
#include <catalog/pg_aggregate.h>
#include <catalog/pg_class.h>
#include <catalog/pg_collation.h>
#include <parser/parsetree.h>
#include <utils/array.h>
#include <utils/builtins.h>
#include <utils/lsyscache.h>
#include <parser/parse_relation.h>
#include <access/xact.h>

#include "io/bson_core.h"
#include "metadata/metadata_cache.h"
#include "query/query_operator.h"
#include "planner/documentdb_planner.h"
#include "aggregation/bson_aggregation_pipeline.h"
#include "commands/parse_error.h"
#include "commands/commands_common.h"
#include "utils/feature_counter.h"
#include "utils/version_utils.h"

#include "aggregation/bson_aggregation_pipeline_private.h"
#include "api_hooks.h"
#include "index_am/index_am_extend_create.h"

static Query * GenerateBaseListCollectionsQuery(Datum databaseDatum, bool nameOnly,
												bool addDistributedMetadata,
												AggregationPipelineBuildContext *context);
static Query * HandleListCollectionsProjector(Query *query,
											  AggregationPipelineBuildContext *context,
											  bool nameOnly, bool addDistributedMetadata);
static Query * GenerateBaseListIndexesQuery(text *databaseDatum, const
											StringView *collectionName,
											AggregationPipelineBuildContext *context);

static Query * BuildSingleFunctionQuery(Oid queryFunctionOid, List *queryArgs, bool
										isMultiRow);


/*
 * Generates the base query for collection agnostic aggregate queries
 * and populates the necessary state into the context object.
 * The query formed is
 * SELECT NULL::bson;
 * This is because any agnostic stage will overwrite this anyway.
 */
Query *
GenerateBaseAgnosticQuery(text *databaseDatum, AggregationPipelineBuildContext *context)
{
	StringView agnosticCollection = CreateStringViewFromString("$cmd.aggregate");
	Query *query = makeNode(Query);
	query->commandType = CMD_SELECT;
	query->querySource = QSRC_ORIGINAL;
	query->canSetTag = true;
	context->collectionNameView = agnosticCollection;
	context->namespaceName = CreateNamespaceName(databaseDatum,
												 &agnosticCollection);
	context->mongoCollection = NULL;

	query->rtable = NIL;

	/* Create an empty jointree structure */
	query->jointree = makeNode(FromExpr);

	/* Create the projector. We only project the NULL::bson in this type of query */
	Const *documentEntry = makeConst(BsonTypeId(), -1, InvalidOid, -1, (Datum) 0, true,
									 false);
	TargetEntry *baseTargetEntry = makeTargetEntry((Expr *) documentEntry, 1, "document",
												   false);
	query->targetList = list_make1(baseTargetEntry);
	return query;
}


/*
 * Generates a query that is akin to the MongoDB $listCollections query command
 */
Query *
GenerateListCollectionsQuery(text *databaseDatum, pgbson *listCollectionsSpec,
							 QueryData *queryData, bool setStatementTimeout)
{
	AggregationPipelineBuildContext context = { 0 };
	context.databaseNameDatum = databaseDatum;

	bson_iter_t listCollectionsIter;
	PgbsonInitIterator(listCollectionsSpec, &listCollectionsIter);

	bson_value_t filter = { 0 };
	bool nameOnly = false;
	bool distributedMetadata = false;

	while (bson_iter_next(&listCollectionsIter))
	{
		StringView keyView = bson_iter_key_string_view(&listCollectionsIter);
		const bson_value_t *value = bson_iter_value(&listCollectionsIter);

		if (StringViewEqualsCString(&keyView, "listCollections"))
		{
			continue;
		}
		else if (StringViewEqualsCString(&keyView, "filter"))
		{
			if (!BSON_ITER_HOLDS_NULL(&listCollectionsIter))
			{
				EnsureTopLevelFieldType("filter", &listCollectionsIter,
										BSON_TYPE_DOCUMENT);
				filter = *value;
			}
		}
		else if (StringViewEqualsCString(&keyView, "cursor"))
		{
			ParseCursorDocument(&listCollectionsIter, queryData);
		}
		else if (StringViewEqualsCString(&keyView, "nameOnly"))
		{
			nameOnly = BsonValueAsBool(value);
		}
		else if (StringViewEqualsCString(&keyView, "addDistributedMetadata"))
		{
			distributedMetadata = BsonValueAsBool(value);
		}
		else if (StringViewEqualsCString(&keyView, "authorizedCollections"))
		{
			/* TODO: Handle this */
		}
		else if (setStatementTimeout && StringViewEqualsCString(&keyView, "maxTimeMS"))
		{
			EnsureTopLevelFieldIsNumberLike("listCollections.maxTimeMS", value);
			SetExplicitStatementTimeout(BsonValueAsInt32(value));
		}
		else if (StringViewEqualsCString(&keyView, "$db"))
		{
			text *prevDb = context.databaseNameDatum;
			ValidateOrExtractDatabaseNameTextFromSpec(&listCollectionsIter,
													  &context.databaseNameDatum);
			if (prevDb == NULL)
			{
				databaseDatum = context.databaseNameDatum;
			}
		}
		else if (!IsCommonSpecIgnoredField(keyView.string))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_UNKNOWNBSONFIELD),
							errmsg(
								"The BSON field listCollections.%.*s is not recognized as a valid field",
								keyView.length, keyView.string),
							errdetail_log(
								"The BSON field listCollections.%.*s is not recognized as a valid field",
								keyView.length, keyView.string)));
		}
	}

	if (context.databaseNameDatum == NULL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Required field database must be valid")));
	}

	Query *query = GenerateBaseListCollectionsQuery(PointerGetDatum(databaseDatum),
													nameOnly,
													distributedMetadata, &context);
	queryData->namespaceName = context.namespaceName;

	query = HandleListCollectionsProjector(query, &context, nameOnly,
										   distributedMetadata);

	/* apply match */
	if (filter.value_type != BSON_TYPE_EOD)
	{
		query = HandleMatch(&filter, query, &context);
		context.stageNum++;
	}

	return query;
}


/*
 * Generates a query that is akin to the MongoDB $listIndexes query command
 */
Query *
GenerateListIndexesQuery(text *databaseDatum, pgbson *listIndexesSpec,
						 QueryData *queryData, bool setStatementTimeout)
{
	AggregationPipelineBuildContext context = { 0 };
	context.databaseNameDatum = databaseDatum;

	bson_iter_t listIndexesIter;
	PgbsonInitIterator(listIndexesSpec, &listIndexesIter);

	StringView collectionName = { 0 };
	while (bson_iter_next(&listIndexesIter))
	{
		StringView keyView = bson_iter_key_string_view(&listIndexesIter);
		if (StringViewEqualsCString(&keyView, "listIndexes") ||
			StringViewEqualsCString(&keyView, "listindexes"))
		{
			EnsureTopLevelFieldType("listIndexes", &listIndexesIter, BSON_TYPE_UTF8);
			collectionName.string = bson_iter_utf8(&listIndexesIter,
												   &collectionName.length);
		}
		else if (StringViewEqualsCString(&keyView, "cursor"))
		{
			ParseCursorDocument(&listIndexesIter, queryData);
		}
		else if (setStatementTimeout && StringViewEqualsCString(&keyView, "maxTimeMS"))
		{
			const bson_value_t *value = bson_iter_value(&listIndexesIter);
			EnsureTopLevelFieldIsNumberLike("listIndexes.maxTimeMS", value);
			SetExplicitStatementTimeout(BsonValueAsInt32(value));
		}
		else if (StringViewEqualsCString(&keyView, "$db"))
		{
			text *prevDb = context.databaseNameDatum;
			ValidateOrExtractDatabaseNameTextFromSpec(&listIndexesIter,
													  &context.databaseNameDatum);
			if (prevDb == NULL)
			{
				databaseDatum = context.databaseNameDatum;
			}
		}
		else if (!IsCommonSpecIgnoredField(keyView.string))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_UNKNOWNBSONFIELD),
							errmsg(
								"The BSON field listIndexes.%.*s is not recognized as a valid field",
								keyView.length, keyView.string),
							errdetail_log(
								"The BSON field listIndexes.%.*s is not recognized as a valid field",
								keyView.length, keyView.string)));
		}
	}

	if (context.databaseNameDatum == NULL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Required field database must be valid")));
	}

	if (collectionName.length == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Required field collection must be valid")));
	}

	Query *query = GenerateBaseListIndexesQuery(databaseDatum, &collectionName, &context);
	queryData->namespaceName = context.namespaceName;
	return query;
}


/*
 * Mutates the query to process the CurrentOp aggregation stage
 * Stage parameters:
 * { allUsers: <boolean>, idleConnections: <boolean>, idleCursors: <boolean>, idleSessions: <boolean>, localOps: <boolean> }
 * This stage will form the query
 * SELECT document FROM ApiSchema.current_op_aggregation({ currentOpSpec });
 * Requires this to be the first stage, so the prior query is discarded.
 */
Query *
HandleCurrentOp(const bson_value_t *existingValue, Query *query,
				AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_CURRENTOP);
	EnsureTopLevelFieldValueType("pipeline.$currentOp", existingValue,
								 BSON_TYPE_DOCUMENT);

	if (context->stageNum != 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40602),
						errmsg(
							"The $currentOp can only be used as the initial stage in the pipeline.")));
	}

	const char *databaseStr = text_to_cstring(context->databaseNameDatum);
	if (strcmp(databaseStr, "admin") != 0 ||
		query->jointree->fromlist != NULL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INVALIDNAMESPACE),
						errmsg(
							"$currentOp must be executed on the 'admin' database with parameter {aggregate: 1}")));
	}

	/* Any further validation done during processing of the currentOp aggregation */
	Assert(query->jointree->fromlist == NULL);
	Assert(query->rtable == NIL);

	/* Now create a function RTE */
	RangeTblEntry *rte = makeNode(RangeTblEntry);
	rte->rtekind = RTE_FUNCTION;
	rte->relid = InvalidOid;

	List *colNames = list_make1(makeString("document"));
	rte->alias = rte->eref = makeAlias("currentOp", colNames);
	rte->lateral = false;
	rte->inFromCl = true;
	rte->functions = NIL;
	rte->inh = false;
#if PG_VERSION_NUM >= 160000
	rte->perminfoindex = 0;
#else
	rte->requiredPerms = ACL_SELECT;
#endif
	rte->rellockmode = AccessShareLock;

	/* Now create the rtfunc*/
	List *args = list_make1(MakeBsonConst(PgbsonInitFromDocumentBsonValue(
											  existingValue)));
	FuncExpr *rangeFunc = makeFuncExpr(BsonCurrentOpAggregationFunctionId(), BsonTypeId(),
									   args,
									   InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
	rangeFunc->funcretset = true;
	RangeTblFunction *rangeTableFunction = makeNode(RangeTblFunction);
	rangeTableFunction->funccolcount = 1;
	rangeTableFunction->funccolnames = colNames;
	rangeTableFunction->funccoltypes = list_make1_oid(BsonTypeId());
	rangeTableFunction->funccoltypmods = list_make1_oid(-1);
	rangeTableFunction->funccolcollations = list_make1_oid(InvalidOid);
	rangeTableFunction->funcparams = NULL;
	rangeTableFunction->funcexpr = (Node *) rangeFunc;

	/* Add the RTFunc to the RTE */
	rte->functions = list_make1(rangeTableFunction);

	query->rtable = list_make1(rte);

	RangeTblRef *rtr = makeNode(RangeTblRef);
	rtr->rtindex = 1;
	query->jointree = makeFromExpr(list_make1(rtr), NULL);

	/* Create the projector. We only project the 'document' column in this type of query */
	Var *documentEntry = makeVar(1, 1, BsonTypeId(), -1, InvalidOid, 0);
	TargetEntry *baseTargetEntry = makeTargetEntry((Expr *) documentEntry, 1, "document",
												   false);
	query->targetList = list_make1(baseTargetEntry);

	return query;
}


/*
 * Generates the base query that queries the ApiCatalogSchemaName.collection_indexes
 * for a listIndexes scenario.
 */
static Query *
GenerateBaseListIndexesQuery(text *databaseDatum, const StringView *collectionName,
							 AggregationPipelineBuildContext *context)
{
	Query *query = makeNode(Query);
	query->commandType = CMD_SELECT;
	query->querySource = QSRC_ORIGINAL;
	query->canSetTag = true;
	context->namespaceName = CreateNamespaceName(databaseDatum,
												 collectionName);
	context->collectionNameView = *collectionName;

	MongoCollection *collection = GetMongoCollectionByNameDatum(PointerGetDatum(
																	databaseDatum),
																CStringGetTextDatum(
																	collectionName->string),
																NoLock);
	if (collection == NULL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_NAMESPACENOTFOUND),
						errmsg("Namespace does not currently exist: %s",
							   context->namespaceName)));
	}

	/* Add ApiInternalSchemaName.index_spec_as_bson(index_spec, TRUE) projector */

	/* create Var that references the index_spec column in ApiCatalogSchemaName.collection_indexes */
	Index varno = 1;
	Index varlevelsup = 0;
	Var *indexSpecVar = makeVar(varno, 3,
								IndexSpecTypeId(), -1,
								InvalidOid, varlevelsup);

	bool boolConstValue = true;
	List *args = list_make2(indexSpecVar,
							MakeBoolValueConst(boolConstValue));
	FuncExpr *indexSpecAsBsonExpr = makeFuncExpr(IndexSpecAsBsonFunctionId(),
												 BsonTypeId(),
												 args, InvalidOid, InvalidOid,
												 COERCE_EXPLICIT_CALL);

	TargetEntry *baseTargetEntry = makeTargetEntry((Expr *) indexSpecAsBsonExpr, 1,
												   "indexes", false);
	query->targetList = list_make1(baseTargetEntry);

	RangeTblEntry *rte = makeNode(RangeTblEntry);

	/* Match spec for ApiCatalogSchemaName.collection_indexes function */
	List *colNames = list_concat(list_make3(makeString("collection_id"),
											makeString("index_id"),
											makeString("index_spec")),
								 list_make1(makeString("index_is_valid")));
	rte->rtekind = RTE_RELATION;
	rte->alias = rte->eref = makeAlias("indexspec", colNames);
	rte->lateral = false;
	rte->inFromCl = true;
	rte->relkind = RELKIND_RELATION;
	rte->functions = NIL;
	rte->inh = true;
	rte->rellockmode = AccessShareLock;

	RangeVar *rangeVar = makeRangeVar(ApiCatalogSchemaName, "collection_indexes", -1);
	rte->relid = RangeVarGetRelid(rangeVar, AccessShareLock, false);

#if PG_VERSION_NUM >= 160000
	RTEPermissionInfo *permInfo = addRTEPermissionInfo(&query->rteperminfos, rte);
	permInfo->requiredPerms = ACL_SELECT;
#else
	rte->requiredPerms = ACL_SELECT;
#endif
	query->rtable = list_make1(rte);

	/* Register the RTE in the "FROM" clause and add where clause
	 *  collection_id = <id> AND (index_is_valid OR ApiInternalSchemaName.index_build_is_in_progress)*/
	RangeTblRef *rtr = makeNode(RangeTblRef);
	rtr->rtindex = 1;

	Var *indexIsValidIdVar = makeVar(varno, 4,
									 BOOLOID, -1,
									 InvalidOid, varlevelsup);
	Var *indexIdVar = makeVar(varno, 2, INT4OID, -1, InvalidOid, varlevelsup);

	FuncExpr *indexBuildIsInProgressExpr = makeFuncExpr(
		IndexBuildIsInProgressFunctionId(), BOOLOID,
		list_make1(indexIdVar),
		InvalidOid, InvalidOid,
		COERCE_EXPLICIT_CALL);

	/* index_is_valid OR ApiInternalSchemaName.index_build_is_in_progress */
	Expr *orClause = make_orclause(list_make2(indexIsValidIdVar,
											  indexBuildIsInProgressExpr));


	Var *collectionIdVar = makeVar(varno, 1, INT8OID, -1, InvalidOid, varlevelsup);

	/* collection_id = <id> */
	Expr *opExpr = make_opclause(BigintEqualOperatorId(), BOOLOID, false,
								 (Expr *) collectionIdVar,
								 (Expr *) makeConst(INT8OID, -1,
													InvalidOid,
													sizeof(int64_t),
													Int64GetDatum(
														collection->collectionId),
													false, true),
								 InvalidOid, InvalidOid);

	/* Filter out extended index info when extended indexes feature is enabled */
	Expr *andClause;
	if (EnableExtendedIndexes)
	{
		/* NOT (index_spec.index_options #= '{ "isExtendedIndex": true }') */
		Oid queryOpOid = BsonEqualMatchRuntimeOperatorId();

		FieldSelect *indexOptionsField = makeNode(FieldSelect);
		indexOptionsField->arg = (Expr *) indexSpecVar;
		indexOptionsField->fieldnum = 10;                  /* index_options is the 10th attribute */
		indexOptionsField->resulttype = BsonTypeId();       /* index_options is of type bson */
		indexOptionsField->resulttypmod = -1;
		indexOptionsField->resultcollid = InvalidOid;

		pgbson_writer filterWriter;
		PgbsonWriterInit(&filterWriter);
		PgbsonWriterAppendBool(&filterWriter,
							   EXTENDED_INDEX_SPEC_FLAG_FIELD_NAME,
							   EXTENDED_INDEX_SPEC_FLAG_FIELD_NAME_LENGTH,
							   true);
		pgbson *filterBson = PgbsonWriterGetPgbson(&filterWriter);
		Const *filterConst = MakeBsonConst(filterBson);

		Expr *indexOptionsMatchExpr = make_opclause(queryOpOid, BOOLOID, false,
													(Expr *) indexOptionsField,
													(Expr *) filterConst,
													InvalidOid, InvalidOid);

		Expr *notExtendedExpr = (Expr *) makeBoolExpr(NOT_EXPR,
													  list_make1(indexOptionsMatchExpr),
													  -1);

		/* (index_spec).index_options is null */
		NullTest *indexOptionsIsNullExpr = makeNode(NullTest);
		indexOptionsIsNullExpr->arg = (Expr *) indexOptionsField;
		indexOptionsIsNullExpr->nulltesttype = IS_NULL;
		indexOptionsIsNullExpr->argisrow = false;

		/* ((index_spec).index_options is null) or NOT (index_spec.index_options #= '{ "isExtendedIndex": true }') */
		Expr *notExtendedOrNullExpr = (Expr *) makeBoolExpr(OR_EXPR,
															list_make2(
																indexOptionsIsNullExpr,
																notExtendedExpr),
															-1);

		/* collection_id = <id> AND (index_is_valid OR ApiInternalSchemaName.index_build_is_in_progress)
		 * AND ((index_spec).index_options is null OR NOT (index_spec.index_options #= '{ "isExtendedIndex": true }'))
		 */
		andClause = make_andclause(list_make3(opExpr, orClause, notExtendedOrNullExpr));
	}
	else
	{
		andClause = make_andclause(list_make2(opExpr, orClause));
	}

	query->jointree = makeFromExpr(list_make1(rtr), (Node *) andClause);

	/* ORDER BY index_id */
	TargetEntry *entry = linitial(query->targetList);
	ParseState *parseState = make_parsestate(NULL);
	parseState->p_expr_kind = EXPR_KIND_ORDER_BY;

	/* set after what is already taken */
	parseState->p_next_resno = entry->resno + 1;

	SortBy *sortBy = makeNode(SortBy);
	sortBy->location = -1;
	sortBy->sortby_dir = SORTBY_ASC;
	sortBy->node = (Node *) indexIdVar;

	bool resjunk = true;
	TargetEntry *sortEntry = makeTargetEntry((Expr *) indexIdVar,
											 (AttrNumber) parseState->p_next_resno++,
											 "?sort?",
											 resjunk);
	query->targetList = lappend(query->targetList, sortEntry);
	List *sortlist = addTargetToSortList(parseState, sortEntry,
										 NIL, query->targetList, sortBy);

	pfree(parseState);
	query->sortClause = sortlist;

	return query;
}


/*
 * Generates the base table that queries the ApiCatalogSchemaName.collections
 * for a listCollections scenario.
 */
static Query *
GenerateBaseListCollectionsQuery(Datum databaseDatum, bool nameOnly,
								 bool addDistributedMetadata,
								 AggregationPipelineBuildContext *context)
{
	Query *query = makeNode(Query);
	query->commandType = CMD_SELECT;
	query->querySource = QSRC_ORIGINAL;
	query->canSetTag = true;
	StringView collectionsView = CreateStringViewFromString("$cmd.ListCollections");
	context->namespaceName = CreateNamespaceName(DatumGetTextP(databaseDatum),
												 &collectionsView);
	context->collectionNameView = collectionsView;

	RangeTblEntry *rte = makeNode(RangeTblEntry);

	/* Match spec for ApiCatalogSchemaName.collections function */
	List *colNames = list_concat(list_make3(makeString("database_name"), makeString(
												"collection_name"), makeString(
												"collection_id")),
								 list_make3(makeString("shard_key"), makeString(
												"collection_uuid"), makeString(
												"view_definition")));
	rte->rtekind = RTE_RELATION;
	rte->alias = rte->eref = makeAlias("collection", colNames);
	rte->lateral = false;
	rte->inFromCl = true;
	rte->relkind = RELKIND_RELATION;
	rte->functions = NIL;
	rte->inh = true;
	rte->rellockmode = AccessShareLock;

	RangeVar *rangeVar = makeRangeVar(ApiCatalogSchemaName, "collections", -1);
	rte->relid = RangeVarGetRelid(rangeVar, AccessShareLock, false);

#if PG_VERSION_NUM >= 160000
	RTEPermissionInfo *permInfo = addRTEPermissionInfo(&query->rteperminfos, rte);
	permInfo->requiredPerms = ACL_SELECT;
#else
	rte->requiredPerms = ACL_SELECT;
#endif
	query->rtable = list_make1(rte);

	/* Now register the RTE in the "FROM" clause with a single filter on the database_name */
	RangeTblRef *rtr = makeNode(RangeTblRef);
	rtr->rtindex = 1;

	Var *databaseVar = makeVar(1, 1, TEXTOID, -1, InvalidOid, 0);
	Expr *opExpr = make_opclause(TextEqualOperatorId(), BOOLOID, false,
								 (Expr *) databaseVar,
								 (Expr *) makeConst(TEXTOID, -1, InvalidOid, -1,
													databaseDatum, false, false),
								 InvalidOid, DEFAULT_COLLATION_OID);

	/* Remove system collections */
	Var *collectionVar = makeVar(1, 2, TEXTOID, -1, InvalidOid, 0);
	StringView sentinelCollection = CreateStringViewFromString("system.dbSentinel");
	Const *systemCollection = MakeTextConst(sentinelCollection.string,
											sentinelCollection.length);
	Expr *notExpr = make_opclause(TextNotEqualOperatorId(), BOOLOID, false,
								  (Expr *) collectionVar, (Expr *) systemCollection,
								  InvalidOid, DEFAULT_COLLATION_OID);

	query->jointree = makeFromExpr(list_make1(rtr), (Node *) make_ands_explicit(
									   list_make2(opExpr, notExpr)));

	/* Add a row_get_bson to make it a single bson document */
	Var *rowExpr = makeVar(1, 0, ApiCatalogCollectionsTypeOid(), -1, InvalidOid, 0);
	FuncExpr *funcExpr = makeFuncExpr(RowGetBsonFunctionOid(), BsonTypeId(),
									  list_make1(rowExpr), InvalidOid, InvalidOid,
									  COERCE_EXPLICIT_CALL);
	TargetEntry *baseTargetEntry = makeTargetEntry((Expr *) funcExpr, 1, "document",
												   false);
	query->targetList = list_make1(baseTargetEntry);

	if (addDistributedMetadata)
	{
		query = MutateListCollectionsQueryForDistribution(query);
	}

	return query;
}


/*
 * Modifies the query to handle the $collStats stage.
 */
Query *
HandleCollStats(const bson_value_t *existingValue, Query *query,
				AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_COLLSTATS);
	EnsureTopLevelFieldValueType("$collStats", existingValue, BSON_TYPE_DOCUMENT);

	if (context->stageNum != 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40602),
						errmsg(
							"$collStats can only be used as the initial stage within the processing pipeline.")));
	}

	/* Skip validate the collStats document: done in the function */
	/* Now create the rtfunc*/
	Const *databaseConst = makeConst(TEXTOID, -1, InvalidOid, -1,
									 PointerGetDatum(context->databaseNameDatum), false,
									 false);
	Const *collectionConst = MakeTextConst(context->collectionNameView.string,
										   context->collectionNameView.length);
	pgbson *bson = PgbsonInitFromDocumentBsonValue(existingValue);
	List *collStatsArgs = list_make3(databaseConst, collectionConst, MakeBsonConst(bson));

	/* Remove the collection (it's not on the base table) */
	context->mongoCollection = NULL;
	bool isMultiRow = false;
	return BuildSingleFunctionQuery(ApiCollStatsAggregationFunctionOid(),
									collStatsArgs, isMultiRow);
}


/*
 * Modifies the query to handle the $indexStats stage.
 */
Query *
HandleIndexStats(const bson_value_t *existingValue, Query *query,
				 AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_INDEXSTATS);
	EnsureTopLevelFieldValueType("$indexStats", existingValue, BSON_TYPE_DOCUMENT);

	if (!IsBsonValueEmptyDocument(existingValue))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION28803),
						errmsg(
							"The $indexStats stage specification is required to be provided as an empty object.")));
	}

	if (context->stageNum != 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40602),
						errmsg(
							"The $indexStats operator can only be used as the initial stage in the pipeline.")));
	}

	bool isTopLevel = true;
	if (IsInTransactionBlock(isTopLevel))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_OPERATIONNOTSUPPORTEDINTRANSACTION),
						errmsg(
							"$indexStats is not permitted for use within a transaction")));
	}

	Const *databaseConst = makeConst(TEXTOID, -1, InvalidOid, -1,
									 PointerGetDatum(context->databaseNameDatum), false,
									 false);
	Const *collectionConst = MakeTextConst(context->collectionNameView.string,
										   context->collectionNameView.length);
	List *indexStatsArgs = list_make2(databaseConst, collectionConst);

	/* Remove the collection (it's not on the base table) */
	context->mongoCollection = NULL;

	bool isMultiRow = true;
	return BuildSingleFunctionQuery(ApiIndexStatsAggregationFunctionOid(),
									indexStatsArgs, isMultiRow);
}


/*
 * Builds a single query that is the equivalent of
 * SELECT document FROM queryFunction(args);
 */
static Query *
BuildSingleFunctionQuery(Oid queryFunctionOid, List *queryArgs, bool isMultiRow)
{
	Query *query = makeNode(Query);
	query->commandType = CMD_SELECT;
	query->querySource = QSRC_ORIGINAL;
	query->canSetTag = true;

	List *colNames = list_make1(makeString("document"));
	RangeTblEntry *rte = makeNode(RangeTblEntry);
	rte->rtekind = RTE_FUNCTION;
	rte->relid = InvalidOid;

	rte->alias = rte->eref = makeAlias("collection", colNames);
	rte->lateral = false;
	rte->inFromCl = true;
	rte->functions = NIL;
	rte->inh = false;
#if PG_VERSION_NUM >= 160000
	rte->perminfoindex = 0;
#else
	rte->requiredPerms = ACL_SELECT;
#endif
	rte->rellockmode = AccessShareLock;

	/* Now create the rtfunc*/
	FuncExpr *rangeFunc = makeFuncExpr(queryFunctionOid, BsonTypeId(), queryArgs,
									   InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
	if (isMultiRow)
	{
		rangeFunc->funcretset = true;
	}

	RangeTblFunction *rangeTableFunction = makeNode(RangeTblFunction);
	rangeTableFunction->funccolcount = 1;
	rangeTableFunction->funccolnames = NIL;
	rangeTableFunction->funccoltypes = list_make1_oid(BsonTypeId());
	rangeTableFunction->funccoltypmods = list_make1_int(-1);
	rangeTableFunction->funccolcollations = list_make1_oid(InvalidOid);
	rangeTableFunction->funcparams = NULL;
	rangeTableFunction->funcexpr = (Node *) rangeFunc;

	/* Add the RTFunc to the RTE */
	rte->functions = list_make1(rangeTableFunction);

	query->rtable = list_make1(rte);

	RangeTblRef *rtr = makeNode(RangeTblRef);
	rtr->rtindex = 1;
	query->jointree = makeFromExpr(list_make1(rtr), NULL);

	Var *documentEntry = makeVar(1, 1, BsonTypeId(), -1, InvalidOid, 0);
	TargetEntry *baseTargetEntry = makeTargetEntry((Expr *) documentEntry, 1, "document",
												   false);
	query->targetList = list_make1(baseTargetEntry);
	return query;
}


/* build project to get all the options for list collection response
 * Uses $mergeObjects to combine schema validation fields from their dedicated
 * columns with all options from the options column as-is.
 */
static bson_value_t
WriteConditionForListCollectionOptions()
{
	pgbson_writer writer;
	PgbsonWriterInit(&writer);

	pgbson_array_writer mergeArrayWriter;
	PgbsonWriterStartArray(&writer, "$mergeObjects", 13, &mergeArrayWriter);

	/* First object: schema validation options from dedicated columns */
	pgbson_writer validationWriter;
	PgbsonArrayWriterStartDocument(&mergeArrayWriter, &validationWriter);
	PgbsonWriterAppendUtf8(&validationWriter, "validator", 9, "$validator");
	PgbsonWriterAppendUtf8(&validationWriter, "validationLevel", 15,
						   "$validation_level");
	PgbsonWriterAppendUtf8(&validationWriter, "validationAction", 16,
						   "$validation_action");
	PgbsonArrayWriterEndDocument(&mergeArrayWriter, &validationWriter);

	/* Second object: merge all options from the options column as-is */
	pgbson_writer ifNullWriter;
	PgbsonArrayWriterStartDocument(&mergeArrayWriter, &ifNullWriter);
	pgbson_array_writer ifNullArrayWriter;
	PgbsonWriterStartArray(&ifNullWriter, "$ifNull", 7, &ifNullArrayWriter);
	PgbsonArrayWriterWriteUtf8(&ifNullArrayWriter, "$options");
	pgbson_writer emptyDocWriter;
	PgbsonArrayWriterStartDocument(&ifNullArrayWriter, &emptyDocWriter);
	PgbsonArrayWriterEndDocument(&ifNullArrayWriter, &emptyDocWriter);
	PgbsonWriterEndArray(&ifNullWriter, &ifNullArrayWriter);
	PgbsonArrayWriterEndDocument(&mergeArrayWriter, &ifNullWriter);

	PgbsonWriterEndArray(&writer, &mergeArrayWriter);

	return ConvertPgbsonToBsonValue(PgbsonWriterGetPgbson(&writer));
}


/* writes the condition
 * path: { "$cond": [ { "$toBool": "$view_definition" }, value1, value2 ]}
 * if view_definition is null and ignoreSchemaValidation is false, value2 should be an array as ["$validator", "$validation_level", "$validation_action"] to get schema validation information
 */
static void
WriteConditionWithIfViewsNull(pgbson_writer *writer,
							  const char *path, uint32_t pathLength,
							  const bson_value_t *trueValue,
							  const bson_value_t *falseValue)
{
	pgbson_writer childWriter;
	pgbson_array_writer arrayWriter;
	PgbsonWriterStartDocument(writer, path, pathLength, &childWriter);
	PgbsonWriterStartArray(&childWriter, "$cond", 5, &arrayWriter);

	pgbson_writer toBoolWriter;
	PgbsonArrayWriterStartDocument(&arrayWriter, &toBoolWriter);
	PgbsonWriterAppendUtf8(&toBoolWriter, "$toBool", 7, "$view_definition");
	PgbsonArrayWriterEndDocument(&arrayWriter, &toBoolWriter);

	PgbsonArrayWriterWriteValue(&arrayWriter, trueValue);
	PgbsonArrayWriterWriteValue(&arrayWriter, falseValue);

	PgbsonWriterEndArray(&childWriter, &arrayWriter);
	PgbsonWriterEndDocument(writer, &childWriter);
}


/*
 * Modifies the ListCollections base table to match the mongo syntax.
 */
static Query *
HandleListCollectionsProjector(Query *query, AggregationPipelineBuildContext *context,
							   bool nameOnly, bool addDistributedMetadata)
{
	pgbson_writer writer;
	PgbsonWriterInit(&writer);

	/* "name": "collection_name "*/
	PgbsonWriterAppendUtf8(&writer, "name", 4, "$collection_name");

	/* "type": { "$cond": [ { "$toBool": "$view_definition" }, "view", "collection" ]} */
	{
		bson_value_t collectionValue = { 0 };
		collectionValue.value_type = BSON_TYPE_UTF8;
		collectionValue.value.v_utf8.str = "collection";
		collectionValue.value.v_utf8.len = 10;
		bson_value_t viewValue = { 0 };
		viewValue.value_type = BSON_TYPE_UTF8;
		viewValue.value.v_utf8.str = "view";
		viewValue.value.v_utf8.len = 4;

		WriteConditionWithIfViewsNull(&writer, "type", 4, &viewValue, &collectionValue);
	}

	if (!nameOnly)
	{
		/* "options": { "$cond": [ { "$toBool": "$view_definition" }, "$view_definition",  {"validator": "$validator", "validationLevel":"$validation_level", "validationAction":"$validation_action"} ] } */
		bson_value_t collectionValue = WriteConditionForListCollectionOptions();

		bson_value_t viewValue = { 0 };
		viewValue.value_type = BSON_TYPE_UTF8;
		viewValue.value.v_utf8.str = "$view_definition";
		viewValue.value.v_utf8.len = 16;

		WriteConditionWithIfViewsNull(&writer, "options", 7, &viewValue,
									  &collectionValue);

		/* "info": { "readOnly": { "$ifNull": [ { "$toBool": "$view_definition"}, false] }, "uuid": "$collection_uuid", "shardKey": "$shard_key" } } */
		pgbson_writer infoWriter;
		PgbsonWriterStartDocument(&writer, "info", 4, &infoWriter);

		pgbson_writer readOnlyWriter;
		PgbsonWriterStartDocument(&infoWriter, "readOnly", 8, &readOnlyWriter);

		pgbson_array_writer ifNullWriter;
		PgbsonWriterStartArray(&readOnlyWriter, "$ifNull", 7, &ifNullWriter);

		pgbson_writer toBoolWriter;
		PgbsonArrayWriterStartDocument(&ifNullWriter, &toBoolWriter);
		PgbsonWriterAppendUtf8(&toBoolWriter, "$toBool", 7, "$view_definition");
		PgbsonArrayWriterEndDocument(&ifNullWriter, &toBoolWriter);

		bson_value_t falseValue = { 0 };
		falseValue.value_type = BSON_TYPE_BOOL;
		falseValue.value.v_bool = false;
		PgbsonArrayWriterWriteValue(&ifNullWriter, &falseValue);

		PgbsonWriterEndArray(&readOnlyWriter, &ifNullWriter);
		PgbsonWriterEndDocument(&infoWriter, &readOnlyWriter);

		PgbsonWriterAppendUtf8(&infoWriter, "uuid", 4, "$collection_uuid");
		PgbsonWriterAppendUtf8(&infoWriter, "shardKey", 8, "$shard_key");

		PgbsonWriterEndDocument(&writer, &infoWriter);

		/* "idIndex": { "$cond": [ { "$toBool": "$view_definition" }, null, { "v": 2, "key": { "_id": 1 }, "name": "_id_" } ] } */
		pgbson_writer idIndexWriter;
		pgbson_writer keyWriter;
		PgbsonWriterInit(&idIndexWriter);
		PgbsonWriterAppendInt32(&idIndexWriter, "v", 1, 2);
		PgbsonWriterAppendUtf8(&idIndexWriter, "name", 4, "_id_");
		PgbsonWriterStartDocument(&idIndexWriter, "key", 3, &keyWriter);
		PgbsonWriterAppendInt32(&keyWriter, "_id", 3, 1);
		PgbsonWriterEndDocument(&idIndexWriter, &keyWriter);

		collectionValue = ConvertPgbsonToBsonValue(PgbsonWriterGetPgbson(
													   &idIndexWriter));
		viewValue = (bson_value_t) {
			0
		};
		viewValue.value_type = BSON_TYPE_NULL;

		WriteConditionWithIfViewsNull(&writer, "idIndex", 7, &viewValue,
									  &collectionValue);
	}

	if (addDistributedMetadata)
	{
		PgbsonWriterAppendInt32(&writer, "shardCount", 10, 1);
		PgbsonWriterAppendInt32(&writer, "colocationId", 12, 1);
	}

	pgbson *bson = PgbsonWriterGetPgbson(&writer);
	bson_value_t bsonValue = ConvertPgbsonToBsonValue(bson);

	/* no use for *WithLet or *WithLetAndCollation projection functions here, so we set them to NULL */
	Oid (*projectWithLetFuncOid) (void) = NULL;
	Oid (*projectWithLetAndCollationFuncOid) (void) = NULL;

	return HandleSimpleProjectionStage(&bsonValue, query, context, "$project",
									   BsonDollarProjectFunctionOid(),
									   projectWithLetFuncOid,
									   projectWithLetAndCollationFuncOid);
}
