/*-------------------------------------------------------------------------
 * Copyright (c) Microsoft Corporation.  All rights reserved.
 *
 * src/aggregation/bson_aggregation_pipeline.c
 *
 * Implementation of the backend query generation for pipelines.
 *
 *-------------------------------------------------------------------------
 */

#include <postgres.h>
#include <float.h>
#include <math.h>
#include <fmgr.h>
#include <miscadmin.h>
#include <optimizer/optimizer.h>

#include <access/table.h>
#include <access/reloptions.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_operator.h>
#include <catalog/pg_class.h>
#include <parser/parsetree.h>
#include <utils/array.h>
#include <utils/builtins.h>
#include <utils/lsyscache.h>
#include <utils/numeric.h>
#include <utils/lsyscache.h>
#include <utils/fmgroids.h>
#include <nodes/supportnodes.h>
#include <parser/parse_relation.h>
#include <parser/parse_func.h>
#include <funcapi.h>

#include "io/bson_core.h"
#include "types/decimal128.h"
#include "metadata/metadata_cache.h"
#include "query/query_operator.h"
#include "planner/documentdb_planner.h"
#include "planner/mongo_query_operator.h"
#include "aggregation/bson_aggregation_pipeline.h"
#include "aggregation/bson_aggregation_window_operators.h"
#include "commands/parse_error.h"
#include "commands/commands_common.h"
#include "commands/defrem.h"
#include "utils/feature_counter.h"
#include "utils/version_utils.h"
#include "aggregation/bson_query.h"
#include "aggregation/bson_query_common.h"
#include "customscan/bson_custom_query_scan.h"
#include "metadata/index.h"

#include "aggregation/bson_aggregation_pipeline_private.h"
#include "aggregation/bson_bucket_auto.h"
#include "api_hooks.h"
#include "vector/vector_common.h"
#include "aggregation/bson_project.h"
#include "operators/bson_expression.h"
#include "operators/bson_expression_operators.h"
#include "operators/bson_expression_bucket_operator.h"
#include "geospatial/bson_geospatial_common.h"
#include "geospatial/bson_geospatial_geonear.h"
#include "aggregation/bson_densify.h"
#include "collation/collation.h"
#include "utils/hashset_utils.h"
#include "api_hooks.h"

extern bool EnableCursorsOnAggregationQueryRewrite;
extern bool EnableCollation;
extern bool EnableDynamicCursors;
extern bool SkipFailOnCollation;
extern bool DefaultInlineWriteOperations;
extern int MaxAggregationStagesAllowed;

extern bool FailOnNonEmptyGroupCountArg;
extern bool FailOnGroupIdDuplicate;
extern bool ForceGroupSubqueryElimination;
extern bool EnableTailableCursorMaxAwaitTime;
extern bool RemoveMatchNamespaceFilters;
extern bool EnableOrderByIndexTerm;
extern bool EnableGroupByCompoundIdIndexPushdown;
extern bool EnableSortGroupStage;
extern bool EnableProjectPushUpBeforeUnwindWithGroup;
extern bool EnableSortPushToAccumulatorWithPrefix;
extern bool EnableSampleScanFixOnSharded;
extern bool EnableDistinctIndexPushdown;
extern bool EnableDistinctExistsFilterPushdown;
extern bool EnableSubqueryPushdownForMatch;
extern bool EnableDollarSampleReservoirScan;

/* GUC to config tdigest compression */
extern int TdigestCompressionAccuracy;

/*
 * The mutation function that modifies a given query with a pipeline stage's value.
 */
typedef Query *(*MutateQueryForStageFunc)(const bson_value_t *existingValue,
										  Query *query,
										  AggregationPipelineBuildContext *context);

/*
 * This function checks the compatibility of stages in the pipeline.
 */
typedef void (*PipelineStagesPreCheckFunc)(const bson_value_t *existingValue,
										   const AggregationPipelineBuildContext *context);

/*
 * Whether or not the stage requires persistence on the cursor.
 */
typedef bool (*RequiresPersistentCursorFunc)(const bson_value_t *existingValue,
											 bool *isSingleRowResult);

/*
 * Whether or not the stage can be inlined for a lookup stage
 */
typedef bool (*CanInlineLookupStage)(const bson_value_t *stageValue, const
									 StringView *lookupPath, bool hasLet);


/*
 * Declaration of a given aggregation pipeline stage.
 */
typedef struct AggregationStageDefinition
{
	/* The stage name (e.g. $addFields, $project) */
	const char *stage;

	/* The function that will modify the pipeline for that stage - NULL if unsupported */
	MutateQueryForStageFunc mutateFunc;

	/* Whether or not the stage requires persistence on cursors */
	RequiresPersistentCursorFunc requiresPersistentCursor;

	/* Optional function for whether lookup stages can be inlined in the substage */
	CanInlineLookupStage canInlineLookupStageFunc;

	/* Does the stage maintain a stable sort order or modify the input stream */
	bool preservesStableSortOrder;

	/* Whether or not the stage supports collection agnostic queries
	 */
	bool canHandleAgnosticQueries;

	/* Whether or not the stage is a linear transform of the prior stage.
	 * i.e. whether or not the stage is purely a projection transform of its input.
	 */
	bool isProjectTransform;

	/* Check whether given pipeline stages are compatible. */
	PipelineStagesPreCheckFunc pipelineCheckFunc;

	/* Whether or not the stage is an output stage. $merge and $out are output stages */
	bool isOutputStage;

	/* whether or not the stage is a multi-collection stage */
	bool isMultiJoinUnionStage;

	/* Allow Base shard table pushdown */
	bool allowBaseShardTablePushdown;

	Stage stageEnum;
} AggregationStageDefinition;

/*
 * Result of analyzing $sort + $group accumulators to decide
 * whether the explicit $sort can be eliminated or pushed into
 * the accumulator's ORDER BY clause.
 */
typedef struct SortGroupAnalysisResult
{
	/* All accumulators are order-insensitive ($sum, $avg, etc.), so the sort is unnecessary */
	bool canDropSort;

	/* When group keys form a non-dotted prefix of the sort keys, holds the
	 * suffix-only sort spec (remaining keys after the prefix). EOD when
	 * either no prefix match exists or the full sort is pushed. */
	bson_value_t suffixSortSpec;

	/* True when group keys form a valid non-dotted prefix of (or exactly match)
	 * the sort keys.  Distinguishes successful prefix/exact match from
	 * bail-out (dotted paths, mismatch, etc.).  Check suffixSortSpec to
	 * tell prefix-with-suffix apart from exact match. */
	bool groupKeysFormSortPrefix;
} SortGroupAnalysisResult;

/*
 * Parsed representation of a find command spec. Produced by ParseFindQuery and
 * consumed by ApplyFindSpec / ApplyFindSpecCore to build the underlying query.
 */
typedef struct FindSpec
{
	/* Database name datum the find targets. */
	text *databaseDatum;

	/* Target collection name. */
	StringView collectionName;

	/* Optional collection UUID to validate against (NULL when not provided). */
	pg_uuid_t *collectionUuid;

	/* The filter ($match) spec. */
	bson_value_t filter;

	/* The limit spec (normalized for negative/zero values). */
	bson_value_t limit;

	/* The projection spec. */
	bson_value_t projection;

	/* The sort spec. */
	bson_value_t sort;

	/* The skip spec. */
	bson_value_t skip;

	/* The let (top-level variables) spec. */
	bson_value_t let;

	/* The index hint spec. */
	bson_value_t indexHint;

	/* Parsed top-level variable spec derived from let. */
	pgbson *parsedVariables;
} FindSpec;


/*
 * Opaque handle produced by ParseAggregationQueryAndLookupCollection and consumed
 * by ApplyParsedAggregationQuery. Carries the parsed aggregation spec, the build
 * context, the looked-up collection, and the associated query data so the parse and
 * apply phases can be invoked separately by callers that need to branch on the
 * collection topology before generating the local query (mirrors FindQueryPlan).
 */
typedef struct AggregationQueryPlan
{
	AggregationPipelineBuildContext context;
	MongoCollection *collection;
	QueryData *queryData;

	/* Parsed fields needed by the apply phase. */
	StringView collectionName;
	List *aggregationStages;
	pg_uuid_t *collectionUuid;
	bson_value_t indexHint;
	bool isCollectionAgnosticQuery;
	bool hasCursor;
	bool explain;
	CursorParamKind cursorParamKind;
} AggregationQueryPlan;


/*
 * Opaque handle produced by ParseFindQueryAndLookupCollection and consumed by
 * ApplyParsedFindQuery. Carries the parsed find spec, the build context, the
 * looked-up collection, and the associated query data so the parse and apply
 * phases can be invoked separately by callers that need to branch on the
 * collection topology before generating the local query.
 */
typedef struct FindQueryPlan
{
	FindSpec spec;
	AggregationPipelineBuildContext context;
	MongoCollection *collection;
	QueryData *queryData;
} FindQueryPlan;

#define MAX_SUFFIX_SORT_GROUP_KEYS (INDEX_MAX_KEYS * 2)

/* Cap on distinct top-level fields in the synthetic $project. Avoids
 * defeating the optimization by emitting a project that retains the whole
 * document. */
#define PROJECT_PUSHUP_MAX_FIELDS 64

static void AddCursorFunctionsToQuery(Query *query, Query *baseQuery,
									  QueryData *queryData,
									  AggregationPipelineBuildContext *context,
									  bool addCursorAsConst);
static void AddQualifierForTailableQuery(Query *query, Query *baseQuery,
										 QueryData *queryData,
										 AggregationPipelineBuildContext *
										 context);
static void SetBatchSize(const char *fieldName, const bson_value_t *value,
						 QueryData *queryData);

static int CompareStageByStageName(const void *a, const void *b);
static bool IsDefaultJoinTree(Node *node);
static List * AddShardKeyAndIdFilters(const bson_value_t *existingValue, Query *query,
									  AggregationPipelineBuildContext *context,
									  TargetEntry *entry, List *existingQuals);

/* Stage functions */
static Query * HandleAddFields(const bson_value_t *existingValue, Query *query,
							   AggregationPipelineBuildContext *context);
static Query * HandleBucket(const bson_value_t *existingValue, Query *query,
							AggregationPipelineBuildContext *context);
static Query * HandleCount(const bson_value_t *existingValue, Query *query,
						   AggregationPipelineBuildContext *context);
static Query * HandleCountCore(const bson_value_t *existingValue, Query *query,
							   AggregationPipelineBuildContext *context, bool
							   isCountCommand);
static Query * HandleFill(const bson_value_t *existingValue, Query *query,
						  AggregationPipelineBuildContext *context);
static Query * HandleLimit(const bson_value_t *existingValue, Query *query,
						   AggregationPipelineBuildContext *context);
static Query * HandleProject(const bson_value_t *existingValue, Query *query,
							 AggregationPipelineBuildContext *context);
static Query * HandleProjectFind(const bson_value_t *existingValue,
								 const bson_value_t *queryValue, Query *query,
								 AggregationPipelineBuildContext *context);
static Query * HandleRedact(const bson_value_t *existingValue, Query *query,
							AggregationPipelineBuildContext *context);
static Query * HandleReplaceRoot(const bson_value_t *existingValue, Query *query,
								 AggregationPipelineBuildContext *context);
static Query * HandleReplaceWith(const bson_value_t *existingValue, Query *query,
								 AggregationPipelineBuildContext *context);
static Query * HandleSample(const bson_value_t *existingValue, Query *query,
							AggregationPipelineBuildContext *context);
static int64_t ParseSampleSize(const bson_value_t *sizeValue);
static Query * HandleSkip(const bson_value_t *existingValue, Query *query,
						  AggregationPipelineBuildContext *context);
static Query * HandleSort(const bson_value_t *existingValue, Query *query,
						  AggregationPipelineBuildContext *context);
static Query * HandleSortByCount(const bson_value_t *existingValue, Query *query,
								 AggregationPipelineBuildContext *context);
static Query * HandleUnset(const bson_value_t *existingValue, Query *query,
						   AggregationPipelineBuildContext *context);
static Query * HandleUnwind(const bson_value_t *existingValue, Query *query,
							AggregationPipelineBuildContext *context);
static Query * HandleDistinct(const StringView *existingValue, Query *query,
							  AggregationPipelineBuildContext *context);
static Query * HandleGeoNear(const bson_value_t *existingValue, Query *query,
							 AggregationPipelineBuildContext *context);
static Query * HandleMatchAggregationStage(const bson_value_t *existingValue,
										   Query *query,
										   AggregationPipelineBuildContext *context);
static Query * HandleSortGroup(const bson_value_t *existingValue, Query *query,
							   AggregationPipelineBuildContext *context);
static Query * HandleGroupCore(const bson_value_t *existingValue, Query *query,
							   AggregationPipelineBuildContext *context,
							   const bson_value_t *accumulatorSortSpec);

static bool RequiresPersistentCursorFalse(const bson_value_t *pipelineValue,
										  bool *isSingleRowResult);
static bool RequiresPersistentCursorTrue(const bson_value_t *pipelineValue,
										 bool *isSingleRowResult);
static bool RequiresPersistentCursorLimit(const bson_value_t *pipelineValue,
										  bool *isSingleRowResult);
static bool RequiresPersistentCursorSkip(const bson_value_t *pipelineValue,
										 bool *isSingleRowResult);
static bool RequiresPersistentCursorFalseNoSingleRow(const bson_value_t *pipelineValue,
													 bool *isSingleRowResult);
static bool RequiresPersistentCursorTrueSingleRow(const bson_value_t *pipelineValue,
												  bool *isSingleRowResult);

static bool CanInlineLookupStageSetAddFields(const bson_value_t *stageValue, const
											 StringView *lookupPath, bool hasLet);
static bool CanInlineLookupStageProject(const bson_value_t *stageValue, const
										StringView *lookupPath, bool hasLet);
static bool CanInlineLookupStageUnset(const bson_value_t *stageValue, const
									  StringView *lookupPath, bool hasLet);
static bool CanInlineLookupStageUnwind(const bson_value_t *stageValue, const
									   StringView *lookupPath, bool hasLet);
static bool CanInlineLookupStageTrue(const bson_value_t *stageValue, const
									 StringView *lookupPath, bool hasLet);
static void PreCheckChangeStreamPipelineStages(const bson_value_t *pipelineValue,
											   const AggregationPipelineBuildContext *
											   context);
static bool CanInlineLookupStageMatch(const bson_value_t *stageValue, const
									  StringView *lookupPath, bool hasLet);

static bool CheckFuncExprBsonDollarProjectGeonear(const FuncExpr *funcExpr);

static void ValidateQueryTreeForMatchStage(const Query *query);
static void RewriteFillToSetWindowFieldsSpec(const bson_value_t *fillSpec,
											 bool *hasSortBy,
											 bool *onlyHasValueFill,
											 bson_value_t *sortSpec,
											 bson_value_t *addFieldsForValueFill,
											 bson_value_t *setWindowFieldsSpec,
											 bson_value_t *partitionByFields);
static List * TryOptimizeAggregationPipelines(List *aggregationStages,
											  AggregationPipelineBuildContext *context);
static bool IsPipelineStageFollowedByOtherStage(Stage firstStage, Stage secondStage,
												int curIndx, List *stagesList);
static List * TryInjectProjectBeforeUnwindForGroup(List *aggregationStages,
												   int unwindIdx, int groupIdx,
												   AggregationPipelineBuildContext *
												   context);
static List * TryInjectProjectBeforeUnwindForGroupCore(List *aggregationStages,
													   int unwindIdx, int groupIdx,
													   AggregationPipelineBuildContext *
													   context,
													   HTAB *consumedFields);
static AggregationStage * BuildSyntheticInclusionProjectStage(HTAB *consumedFields,
															  bool includeId);
static bool ValueReferencesNoSourceFields(const bson_value_t *value);
static bool TryInlineChangeStreamNamespaceFilters(AggregationStage *changeStreamStage,
												  AggregationStage *matchStage,
												  bool *inlinedCompletely);
static void SetAccumulatorSortOrder(TargetEntry *accumulatorTle, Expr *documentExpr,
									const bson_value_t *sortSpec,
									const char *collationString);
static bool TryBuildSuffixSortSpec(const bson_value_t *groupValue,
								   const bson_value_t *sortValue,
								   bson_value_t *suffixSortSpec);
static bool CanPushSortFilterToIndex(Query *query,
									 AggregationPipelineBuildContext *context);
static FindSpec ParseFindQuery(pgbson *findSpec,
							   QueryData *queryData, bool setStatementTimeout,
							   AggregationPipelineBuildContext *context);
static Query * ApplyFindSpec(const FindSpec *spec, MongoCollection *collection,
							 QueryData *queryData, CursorParamKind cursorParamKind,
							 AggregationPipelineBuildContext *context);
static Query * ApplyFindSpecCore(const FindSpec *spec, Query *query, Query *baseQuery,
								 QueryData *queryData, CursorParamKind cursorParamKind,
								 AggregationPipelineBuildContext *context);
static Const * AddCollationToSortSpec(const pgbsonelement *sortElement,
									  const char *collationString);

#define COMPATIBLE_CHANGE_STREAM_STAGES_COUNT 8
const char *CompatibleChangeStreamPipelineStages[COMPATIBLE_CHANGE_STREAM_STAGES_COUNT] =
{
	"$match",
	"$project",
	"$addFields",
	"$replaceRoot",
	"$replaceWith",
	"$set",
	"$unset",
	"$redact"
};

static const AggregationStageDefinition LookupUnwindStageDefinition = {
	.stage = "$lookupUnwind",
	.mutateFunc = &HandleLookupUnwind,
	.requiresPersistentCursor = &RequiresPersistentCursorTrue,
	.canInlineLookupStageFunc = &CanInlineLookupStageLookup,

	/* Lookup preserves order of the Left collection */
	.preservesStableSortOrder = true,
	.canHandleAgnosticQueries = false,
	.isProjectTransform = false,
	.isOutputStage = false,
	.pipelineCheckFunc = NULL,
	.allowBaseShardTablePushdown = false,

	/* $lookup may touch multiple collections */
	.isMultiJoinUnionStage = true,
	.stageEnum = Stage_LookupUnwind,
};

static const AggregationStageDefinition SortGroupStageDefinition = {
	.stage = "$sortGroup",
	.mutateFunc = &HandleSortGroup,
	.requiresPersistentCursor = &RequiresPersistentCursorTrue,

	/* Group and sort changes the output format (group) so no prior sorts
	 * are valid after this*/
	.preservesStableSortOrder = false,
	.canHandleAgnosticQueries = false,
	.isProjectTransform = false,
	.isOutputStage = false,
	.isMultiJoinUnionStage = false,
	.pipelineCheckFunc = NULL,
	.allowBaseShardTablePushdown = true,
	.stageEnum = Stage_SortGroup,
};

/*
 * Synthetic $project stage definition used when the pipeline optimizer injects
 * an inclusion projection (see BuildSyntheticInclusionProjectStage). Mirrors the
 * $project entry.
 */
static const AggregationStageDefinition ProjectStageDefinition = {
	.stage = "$project",
	.mutateFunc = &HandleProject,
	.requiresPersistentCursor = &RequiresPersistentCursorFalse,
	.canInlineLookupStageFunc = &CanInlineLookupStageProject,

	/* Project does not change the output format */
	.preservesStableSortOrder = true,
	.canHandleAgnosticQueries = false,
	.isProjectTransform = true,
	.isOutputStage = false,
	.isMultiJoinUnionStage = false,
	.pipelineCheckFunc = NULL,
	.allowBaseShardTablePushdown = true,
	.stageEnum = Stage_Project,
};

/* Stages and their definitions sorted by name.
 * Please keep this list sorted.
 */
static const AggregationStageDefinition StageDefinitions[] =
{
	{
		.stage = "$_internalInhibitOptimization",
		.mutateFunc = &HandleInternalInhibitOptimization,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,

		/* can always be inlined since it doesn't change the projector */
		.canInlineLookupStageFunc = &CanInlineLookupStageTrue,
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = true,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Internal_InhibitOptimization,
	},
	{
		.stage = "$addFields",
		.mutateFunc = &HandleAddFields,
		.requiresPersistentCursor = &RequiresPersistentCursorFalse,
		.canInlineLookupStageFunc = &CanInlineLookupStageSetAddFields,
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = true,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_AddFields,
	},
	{
		.stage = "$bucket",
		.mutateFunc = &HandleBucket,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Bucket,
	},
	{
		.stage = "$bucketAuto",
		.mutateFunc = &HandleBucketAuto,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_BucketAuto,
	},
	{
		.stage = "$changeStream",
		.mutateFunc = &HandleChangeStream,
		.requiresPersistentCursor = &RequiresPersistentCursorFalseNoSingleRow,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = true,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* $changeStream may touch multiple collections or cluster level */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = PreCheckChangeStreamPipelineStages,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_ChangeStream,
	},
	{
		.stage = "$collStats",
		.mutateFunc = &HandleCollStats,
		.requiresPersistentCursor = &RequiresPersistentCursorTrueSingleRow,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_CollStats,
	},
	{
		.stage = "$count",
		.mutateFunc = &HandleCount,
		.requiresPersistentCursor = &RequiresPersistentCursorTrueSingleRow,

		/* Changes the projector - can't be inlined */
		.canInlineLookupStageFunc = NULL,

		/* Count changes the output format */
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Count,
	},
	{
		.stage = "$currentOp",
		.mutateFunc = &HandleCurrentOp,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,

		/* Changes the projector - can't be inlined */
		.canInlineLookupStageFunc = NULL,

		/* currentOp changes the output format */
		.preservesStableSortOrder = false,

		.canHandleAgnosticQueries = true,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* $currentOp may touch multiple collections via joins etc */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_CurrentOp,
	},
	{
		.stage = "$densify",
		.mutateFunc = &HandleDensify,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Densify,
	},
	{
		.stage = "$documents",
		.mutateFunc = &HandleDocumentsStage,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = true,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* $documents is a non-collection, but can be used in conjunction
		 * in lookups, unionWiths etc, just be safe here.
		 */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_Documents,
	},
	{
		.stage = "$facet",
		.mutateFunc = &HandleFacet,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,

		/* Changes the projector - can't be inlined */
		.canInlineLookupStageFunc = NULL,

		/* Count changes the output format (object_agg) so no prior sorts */
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* $facet may handle sub-pipelines with joins */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_Facet,
	},
	{
		.stage = "$fill",
		.mutateFunc = &HandleFill,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Fill,
	},
	{
		.stage = "$geoNear",
		.mutateFunc = &HandleGeoNear,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* Geonear creates a custom projection which we don't want to
		 * interfere with match pushdown. Just skip for $geoNear.
		 */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_GeoNear,
	},
	{
		.stage = "$graphLookup",
		.mutateFunc = &HandleGraphLookup,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* Recursive CTE has joins with self */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_GraphLookup,
	},
	{
		.stage = "$group",
		.mutateFunc = &HandleGroup,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,

		/* Changes the projector - can't be inlined */
		.canInlineLookupStageFunc = NULL,

		/* Count changes the output format (group) so no prior sorts
		 * are valid after this*/
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Group,
	},
	{
		.stage = "$indexStats",
		.mutateFunc = &HandleIndexStats,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_IndexStats,
	},
	{
		.stage = "$inverseMatch",
		.mutateFunc = &HandleInverseMatch,
		.requiresPersistentCursor = &RequiresPersistentCursorFalseNoSingleRow,

		/* can always be inlined since it doesn't change the projector */
		.canInlineLookupStageFunc = &CanInlineLookupStageTrue,

		/* inverse match does not change the output format */
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = true,
		.isOutputStage = false,

		/* $inverseMatch joins with another collection */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_InverseMatch,
	},
	{
		.stage = "$limit",
		.mutateFunc = &HandleLimit,
		.requiresPersistentCursor = &RequiresPersistentCursorLimit,

		/* Cannot be inlined - needs to happen *after* the join */
		.canInlineLookupStageFunc = NULL,

		/* Limit does not change the output format */
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Limit,
	},
	{
		.stage = "$listLocalSessions",
		.mutateFunc = NULL,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = true,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* Defensive - assume that this joins */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_ListLocalSessions,
	},
	{
		.stage = "$listSearchIndexes",
		.mutateFunc = &HandleListExtendedIndexes,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_ListSearchIndexes,
	},
	{
		.stage = "$listSessions",
		.mutateFunc = NULL,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* Defensive - assume that this joins */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_ListSessions,
	},
	{
		.stage = "$lookup",
		.mutateFunc = &HandleLookup,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = &CanInlineLookupStageLookup,

		/* Lookup preserves order of the Left collection */
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* Lookup is a left join */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_Lookup,
	},
	{
		.stage = "$match",
		.mutateFunc = &HandleMatchAggregationStage,
		.requiresPersistentCursor = &RequiresPersistentCursorFalse,

		/* can always be inlined since it doesn't change the projector */
		.canInlineLookupStageFunc = &CanInlineLookupStageMatch,

		/* Match does not change the output format */
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = true,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Match,
	},
	{
		.stage = "$merge",
		.mutateFunc = &HandleMerge,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = true,

		/* $merge outputs into same or different collection */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_Merge,
	},
	{
		.stage = "$out",
		.mutateFunc = &HandleOut,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = true,

		/* $out outputs to a new collection */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_Out,
	},
	{
		.stage = "$project",
		.mutateFunc = &HandleProject,
		.requiresPersistentCursor = &RequiresPersistentCursorFalse,
		.canInlineLookupStageFunc = &CanInlineLookupStageProject,

		/* Project does not change the output format */
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = true,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Project,
	},
	{
		.stage = "$redact",
		.mutateFunc = &HandleRedact,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = true,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Redact,
	},
	{
		.stage = "$replaceRoot",
		.mutateFunc = &HandleReplaceRoot,
		.requiresPersistentCursor = &RequiresPersistentCursorFalse,

		/* Changes document structure - can never be inlined */
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = true,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_ReplaceRoot,
	},
	{
		.stage = "$replaceWith",
		.mutateFunc = &HandleReplaceWith,
		.requiresPersistentCursor = &RequiresPersistentCursorFalse,

		/* Changes document structure - can never be inlined */
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = true,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_ReplaceWith,
	},
	{
		.stage = "$sample",
		.mutateFunc = &HandleSample,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,

		/* Needs to be applied post-join - should not be inlined */
		.canInlineLookupStageFunc = NULL,

		/* Sample is effectively a Random orderby - Prior sorts are invalid */
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Sample,
	},
	{
		.stage = "$search",
		.mutateFunc = &HandleSearch,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,

		/* can always be inlined since it doesn't change the projector */
		.canInlineLookupStageFunc = &CanInlineLookupStageTrue,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* Defensive: Presume it *may* join */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Search,
	},
	{
		.stage = "$searchMeta",
		.mutateFunc = NULL,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* Defensive: Presume it *may* join */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_SearchMeta,
	},
	{
		.stage = "$set",
		.mutateFunc = &HandleAddFields,
		.requiresPersistentCursor = &RequiresPersistentCursorFalse,
		.canInlineLookupStageFunc = &CanInlineLookupStageSetAddFields,
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = true,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Set,
	},
	{
		.stage = "$setWindowFields",
		.mutateFunc = &HandleSetWindowFields,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_SetWindowFields,
	},
	{
		.stage = "$skip",
		.mutateFunc = &HandleSkip,
		.requiresPersistentCursor = &RequiresPersistentCursorSkip,

		/* Cannot be inlined - needs to happen *after* the join */
		.canInlineLookupStageFunc = NULL,

		/* Skip does not change the output format */
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Skip,
	},
	{
		.stage = "$sort",
		.mutateFunc = &HandleSort,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,

		/* can always be inlined since it doesn't change the projector */
		.canInlineLookupStageFunc = &CanInlineLookupStageTrue,

		/* Sorting determines the current ordering state */
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Sort,
	},
	{
		.stage = "$sortByCount",
		.mutateFunc = &HandleSortByCount,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_SortByCount,
	},
	{
		.stage = "$unionWith",
		.mutateFunc = &HandleUnionWith,
		.requiresPersistentCursor = &RequiresPersistentCursorFalseNoSingleRow,
		.canInlineLookupStageFunc = NULL,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,

		/* $unionWith does consider multiple collections
		 * Even though there's no join, presume we need the join
		 * path.
		 */
		.isMultiJoinUnionStage = true,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_UnionWith,
	},
	{
		.stage = "$unset",
		.mutateFunc = &HandleUnset,
		.requiresPersistentCursor = &RequiresPersistentCursorFalse,
		.canInlineLookupStageFunc = &CanInlineLookupStageUnset,
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = true,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Unset,
	},
	{
		.stage = "$unwind",
		.mutateFunc = &HandleUnwind,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,
		.canInlineLookupStageFunc = &CanInlineLookupStageUnwind,
		.preservesStableSortOrder = true,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,
		.allowBaseShardTablePushdown = true,
		.stageEnum = Stage_Unwind,
	},
	{
		.stage = "$vectorSearch",
		.mutateFunc = &HandleNativeVectorSearch,
		.requiresPersistentCursor = &RequiresPersistentCursorTrue,

		/* can always be inlined since it doesn't change the projector */
		.canInlineLookupStageFunc = &CanInlineLookupStageTrue,
		.preservesStableSortOrder = false,
		.canHandleAgnosticQueries = false,
		.isProjectTransform = false,
		.isOutputStage = false,
		.isMultiJoinUnionStage = false,
		.pipelineCheckFunc = NULL,

		/* $vectorSearch is needed to be executed withing custom scan boundaries see EvaluateMetaSearchScore in vector/vector_utilities.c */
		.allowBaseShardTablePushdown = false,
		.stageEnum = Stage_VectorSearch,
	}
};

static const int AggregationStageCount = sizeof(StageDefinitions) /
										 sizeof(AggregationStageDefinition);

static const int MaxEvenFunctionArguments = ((int) (FUNC_MAX_ARGS / 2)) * 2;

PG_FUNCTION_INFO_V1(command_bson_aggregation_pipeline);
PG_FUNCTION_INFO_V1(command_bson_aggregation_getmore);
PG_FUNCTION_INFO_V1(command_api_collection);
PG_FUNCTION_INFO_V1(command_aggregation_support);
PG_FUNCTION_INFO_V1(documentdb_core_bson_to_bson);


inline static void
EnsureTopLevelNumberFieldType(const char *fieldName, const bson_value_t *value)
{
	if (!BsonValueIsNumber(value))
	{
		ThrowTopLevelTypeMismatchError(fieldName, BsonTypeName(
										   value->value_type), "number");
	}
}


/*
 * Creates a Var in the query representing the 'document' column of a documentdb_data table.
 */
inline static Var *
CreateDocumentVar(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);
}


/*
 * Creates a Var in the query representing the 'document' column of a documentdb_data table.
 */
inline static Var *
CreateChangeStreamDocumentVar(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_CHANGE_STREAM_TABLE_DOCUMENT_VAR_ATTR_NUMBER,
				   BsonTypeId(), DOCUMENT_DATA_TABLE_DOCUMENT_VAR_TYPMOD,
				   DOCUMENT_DATA_TABLE_DOCUMENT_VAR_COLLATION, varlevelsup);
}


/*
 * Creates a Var in the query representing the 'document' column of a documentdb_data table.
 */
inline static Var *
CreateChangeStreamContinuationtVar(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_CHANGE_STREAM_TABLE_CONTINUATION_VAR_ATTR_NUMBER,
				   BsonTypeId(), DOCUMENT_DATA_TABLE_DOCUMENT_VAR_TYPMOD,
				   DOCUMENT_DATA_TABLE_DOCUMENT_VAR_COLLATION, varlevelsup);
}


/*
 * Helper function to limit the number of aggregation stages in a pipeline.
 */
inline static void
CheckMaxAllowedAggregationStages(int numberOfStages)
{
	if (numberOfStages > MaxAggregationStagesAllowed)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION7749501),
						errmsg(
							"The pipeline length cannot exceed a maximum of %d stages.",
							MaxAggregationStagesAllowed)));
	}
}


/*
 * command_bson_aggregation_pipeline is a wrapper function that carries with it the
 * aggregation pipeline for a query. This is replaced in the planner and so shouldn't
 * ever be called in the runtime.
 */
Datum
command_bson_aggregation_pipeline(PG_FUNCTION_ARGS)
{
	ereport(ERROR, (errmsg(
						"bson_aggregation function should have been processed by the planner. This is an internal error")));
	PG_RETURN_BOOL(false);
}


/*
 * command_bson_aggregation_pipeline is a wrapper function that carries with it the
 * aggregation pipeline for a getmore. This is replaced in the planner and so shouldn't
 * ever be called in the runtime.
 */
Datum
command_bson_aggregation_getmore(PG_FUNCTION_ARGS)
{
	ereport(ERROR, (errmsg(
						"bson_aggregation function should have been processed by the planner. This is an internal error")));
	PG_RETURN_BOOL(false);
}


/*
 * The core Collection function. Represents a query:
 * e.g. SELECT * from ApiSchema.collection().
 */
Datum
command_api_collection(PG_FUNCTION_ARGS)
{
	ereport(ERROR, (errmsg(
						"Collection function should have been simplified during planning. "
						"Collection function should be only used in a FROM clause")));
}


/*
 * This function currently is pseudo-deprecated
 */
Datum
command_aggregation_support(PG_FUNCTION_ARGS)
{
	PG_RETURN_POINTER(NIL);
}


/*
 * Converts a documentdb_core.bson to a bson.
 */
Datum
documentdb_core_bson_to_bson(PG_FUNCTION_ARGS)
{
	return PG_GETARG_DATUM(0);
}


/*
 * Common utility function for parsing a batchSize argument for find/aggregate/getMore
 */
void
SetBatchSize(const char *fieldName, const bson_value_t *value, QueryData *queryData)
{
	EnsureTopLevelNumberFieldType(fieldName, value);

	/* Batchsize can be double - as long as it converts to int (which it treats as floor) */
	queryData->batchSize = BsonValueAsInt32(value);

	if (queryData->batchSize < 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
						errmsg("BatchSize value must be non-negative, but received: %d",
							   queryData->batchSize)));
	}
}


/*
 * Common utility function for parsing a the namespace (Collection) argument for find/aggregate
 * Updates the cursor state with the namespaceName 'db.coll'
 */
const char *
CreateNamespaceName(text *databaseName, const StringView *collectionName)
{
	char *databaseString = VARDATA_ANY(databaseName);
	uint32_t databaseLength = VARSIZE_ANY_EXHDR(databaseName);

	/* Format for database.collection */
	uint32_t totalLength = databaseLength + 1 + collectionName->length + 1;

	char *finalString = palloc(totalLength);
	memcpy(finalString, databaseString, databaseLength);
	finalString[databaseLength] = '.';
	memcpy(&finalString[databaseLength + 1], collectionName->string,
		   collectionName->length);
	finalString[totalLength - 1] = 0;

	return finalString;
}


static void
ProcessIndexHint(bson_iter_t *iterator, bson_value_t *targetHintValue)
{
	ReportFeatureUsage(FEATURE_INDEX_HINT);
	const bson_value_t *value = bson_iter_value(iterator);
	if (value->value_type == BSON_TYPE_UTF8 ||
		value->value_type == BSON_TYPE_DOCUMENT)
	{
		/* The mongo index is specified as a utf8 string index name */
		*targetHintValue = *value;
	}
	else
	{
		EnsureTopLevelFieldType("hint", iterator,
								BSON_TYPE_UTF8);
	}
}


/*
 * Validates the sanity of an aggregation pipeline.
 * For this we simply create the query from it and let the validation take place.
 */
void
ValidateAggregationPipeline(text *databaseDatum, const StringView *baseCollection,
							const bson_value_t *pipelineValue)
{
	AggregationPipelineBuildContext validationContext = { 0 };
	validationContext.databaseNameDatum = databaseDatum;

	pg_uuid_t *collectionUuid = NULL;
	bson_value_t *indexHint = NULL;
	Query *validationQuery = GenerateBaseTableQuery(databaseDatum,
													baseCollection,
													collectionUuid,
													indexHint,
													&validationContext);
	List *stages = ExtractAggregationStages(pipelineValue,
											&validationContext);
	validationQuery = MutateQueryWithPipeline(validationQuery, stages,
											  &validationContext);
	pfree(validationQuery);
}


/*
 * Core logic that takes a pipeline array specified by pipelineValue,
 * and updates the "query" with the pipeline stages' effects.
 * The mutated query is returned as an output.
 */
Query *
MutateQueryWithPipeline(Query *query, List *aggregationStages,
						AggregationPipelineBuildContext *context)
{
	/* Apply stage transformations now */
	ListCell *stageCell = NULL;
	foreach(stageCell, aggregationStages)
	{
		AggregationStage *stage = (AggregationStage *) lfirst(stageCell);
		const AggregationStageDefinition *definition = stage->stageDefinition;
		const char *stageName = definition->stage;

		if (query->jointree->fromlist == NIL && !definition->canHandleAgnosticQueries)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INVALIDNAMESPACE),
							errmsg(
								"The value '{aggregate: 1}' is invalid for the '%s'; a collection input is necessary.",
								stageName),
							errdetail_log(
								"The value '{aggregate: 1}' is invalid for the '%s'; a collection input is necessary.",
								stageName)));
		}

		/* If the prior stage needed to be pushed to a sub-query before the next
		 * stage is processed, then add a subquery stage
		 */
		if (context->requiresSubQuery)
		{
			query = MigrateQueryToSubQuery(query, context);
		}

		if (context->requiresSubQueryAfterProject)
		{
			context->requiresSubQueryAfterProject = false;
			if (definition->isProjectTransform)
			{
				/* Push to subquery on the next stage */
				context->requiresSubQuery = true;
			}
			else
			{
				if (context->requiresTailableCursor)
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_COMMANDNOTSUPPORTED),
									errmsg(
										"Cannot use tailable cursor with stage %s",
										stageName)));
				}

				/* Not a project, so push to a subquery */
				query = MigrateQueryToSubQuery(query, context);
			}
		}

		query = definition->mutateFunc(&stage->stageValue, query,
									   context);

		context->requiresPersistentCursor =
			context->requiresPersistentCursor ||
			definition->requiresPersistentCursor(&stage->stageValue,
												 &context->isSingleRowResult);

		if (!definition->preservesStableSortOrder)
		{
			context->sortSpec.value_type = BSON_TYPE_EOD;
		}
		context->stageNum++;
	}

	/* Agnostic query with no stages */
	if (context->stageNum == 0 && query->jointree->fromlist == NULL)
	{
		/* make the query Select NULL::bson LIMIT 0 */
		query->limitCount = (Node *) makeConst(INT8OID, -1, InvalidOid, sizeof(int64_t),
											   Int64GetDatum(0), false, true);
	}
	return query;
}


/*
 * Sets the cursor topology on the QueryData based on the collection metadata
 * and the shard key filter type applied during query generation.
 */
inline static void
SetCursorTopology(QueryData *queryData,
				  const AggregationPipelineBuildContext *context)
{
	if (context->mongoCollection == NULL)
	{
		return;
	}

	if (context->mongoCollection->shardKey == NULL)
	{
		/*
		 * Use isRemoteUnsharded (set only when the shard physically lives on a
		 * remote worker) rather than !allowShardBaseTable.  allowShardBaseTable
		 * can be false even for a local-shard collection — for example when
		 * called from inside a PL/pgSQL function (IsTransactionBlock()=true) —
		 * and we must not dispatch those queries to a remote worker.
		 *
		 * Topology is set unconditionally here; the EnableDynamicCursors GUC is
		 * enforced at the usage sites (HandleFirstPageRequest and
		 * TryAddDynamicCursorQuery) so that cursor_tracker injection and worker
		 * dispatch are both suppressed when the GUC is off.
		 */
		queryData->cursorTopology =
			context->mongoCollection->isShardRemote ?
			CursorTopology_RemoteUnsharded :
			CursorTopology_LocalUnsharded;
	}
	else
	{
		switch (context->shardKeyFilterAppliedType)
		{
			case ShardKeyFilter_Equality:
			{
				queryData->cursorTopology =
					CursorTopology_ShardedWithShardKeyEquality;
				break;
			}

			case ShardKeyFilter_In:
			{
				queryData->cursorTopology =
					CursorTopology_ShardedWithInOnShardKey;
				break;
			}

			default:
			{
				queryData->cursorTopology = CursorTopology_GeneralSharded;
				break;
			}
		}
	}
}


inline static bool
TryAddDynamicCursorQuery(CursorParamKind cursorParamKind, QueryData *queryData,
						 Query *query, AggregationPipelineBuildContext *context)
{
	if (!IsClusterVersionAtleast(DocDB_V0, 112, 1))
	{
		return false;
	}

	/*
	 * Allow dynamic cursor injection when either:
	 * 1. The shard base table is directly accessible (normal local path), or
	 * 2. We are in the planner aggregation query rewrite path (explain with
	 *    enableCursorsOnAggregationQueryRewrite) and the collection is unsharded.
	 *    In case (2), the query may execute on a remote worker via Citus where
	 *    allowShardBaseTable is false due to the transaction isolation guard in
	 *    TryGetCollectionShardTable. The cursor_tracker injection must still
	 *    proceed so the worker's planner produces a DocumentDBApiCursorScan plan.
	 */
	bool allowDynamicCursor = context->allowShardBaseTable ||
							  (queryData->isAggregationQueryCursorRewrite &&
							   context->mongoCollection != NULL &&
							   context->mongoCollection->shardKey == NULL);

	if (cursorParamKind == CursorParamKind_Dynamic &&
		queryData->cursorKind == QueryCursorType_Unspecified &&
		allowDynamicCursor)
	{
		/* Before processing anything add the cursor params to the base table
		 * if able. Dynamic cursors only work on base shard table RTEs currently.
		 */
		if (list_length(query->rtable) == 1 &&
			context->mongoCollection != NULL)
		{
			RangeTblEntry *rte = linitial(query->rtable);
			if (rte->rtekind == RTE_RELATION)
			{
				/* We're still on the base table (views etc haven't pushed down)
				 * to subqueries. Add the dynamic cursor state expr.
				 */
				List *quals = make_ands_implicit((Expr *) query->jointree->quals);

				pgbson *cursorValue = queryData->cursorStateConst != NULL ?
									  queryData->cursorStateConst : PgbsonInitEmpty();

				Const *cursorConst = MakeBsonConst(cursorValue);
				FuncExpr *cursorStateExpr = makeFuncExpr(
					ApiCursorTrackerFunctionId(), BOOLOID, list_make2(
						MakeSimpleDocumentVar(), cursorConst),
					InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
				quals = lappend(quals, cursorStateExpr);
				query->jointree->quals = (Node *) make_ands_explicit(quals);

				queryData->cursorKind = QueryCursorType_Dynamic;
				return true;
			}
		}
	}

	return false;
}


/*
 * Parses an aggregation command spec and looks up the target collection, WITHOUT
 * generating the underlying SQL query (no base-table query, no pipeline mutation,
 * no cursor-function injection). Determines whether the collection is
 * remote-unsharded so callers may dispatch to a worker instead of generating and
 * discarding a local query.
 *
 * Note: pipeline stages ARE extracted here (via ExtractAggregationStages) because
 * the resulting context->allowShardBaseTable is required to decide remote-unsharded
 * eligibility. This is parse/plan work only; the Query AST is built in the apply
 * phase.
 *
 * On return:
 *   *outCollection - the looked-up collection, or NULL if it doesn't exist or is agnostic.
 */
AggregationQueryPlan *
ParseAggregationQueryAndLookupCollection(text *database, pgbson *aggregationSpec,
										 QueryData *queryData,
										 CursorParamKind cursorParamKind,
										 bool setStatementTimeout,
										 MongoCollection **outCollection)
{
	AggregationQueryPlan *plan = palloc0(sizeof(AggregationQueryPlan));
	AggregationPipelineBuildContext *context = &plan->context;
	context->databaseNameDatum = database;
	context->optimizePipelineStages = true;
	context->joinStatus = JoinStageStatus_Unknown;
	queryData->cursorKind = QueryCursorType_Unspecified;

	bson_iter_t aggregationIterator;
	PgbsonInitIterator(aggregationSpec, &aggregationIterator);

	StringView collectionName = { 0 };
	bson_value_t pipelineValue = { 0 };
	bson_value_t let = { 0 };
	bson_value_t indexHint = { 0 };
	pg_uuid_t *collectionUuid = NULL;

	bool explain = false;
	bool hasCursor = false;
	bool isCollectionAgnosticQuery = false;
	while (bson_iter_next(&aggregationIterator))
	{
		StringView keyView = bson_iter_key_string_view(&aggregationIterator);
		const bson_value_t *value = bson_iter_value(&aggregationIterator);

		if (StringViewEqualsCString(&keyView, "aggregate"))
		{
			if (BSON_ITER_HOLDS_NUMBER(&aggregationIterator) &&
				BsonValueAsDouble(value) == 1.0)
			{
				/* Collection agnostic aggregate */
				isCollectionAgnosticQuery = true;
			}
			else
			{
				EnsureTopLevelFieldType("aggregate", &aggregationIterator,
										BSON_TYPE_UTF8);
				collectionName.string = bson_iter_utf8(&aggregationIterator,
													   &collectionName.length);
			}
		}
		else if (StringViewEqualsCString(&keyView, "pipeline"))
		{
			EnsureTopLevelFieldType("pipeline", &aggregationIterator, BSON_TYPE_ARRAY);
			pipelineValue = *value;
		}
		else if (StringViewEqualsCString(&keyView, "cursor"))
		{
			hasCursor = true;
			ParseCursorDocument(&aggregationIterator, queryData);
		}
		else if (StringViewEqualsCString(&keyView, "allowDiskUse"))
		{
			/* We otherwise ignore this for now (TODO Support this) */
			EnsureTopLevelFieldType("allowDiskUse", &aggregationIterator, BSON_TYPE_BOOL);
		}
		else if (StringViewEqualsCString(&keyView, "explain"))
		{
			/* We otherwise ignore this for now */
			EnsureTopLevelFieldType("explain", &aggregationIterator, BSON_TYPE_BOOL);
			explain = bson_iter_bool(&aggregationIterator);
		}
		else if (StringViewEqualsCString(&keyView, "hint"))
		{
			ProcessIndexHint(&aggregationIterator, &indexHint);
		}
		else if (StringViewEqualsCString(&keyView, "let"))
		{
			ReportFeatureUsage(FEATURE_LET_TOP_LEVEL);

			bool hasValue = EnsureTopLevelFieldTypeNullOkUndefinedOK("let",
																	 &aggregationIterator,
																	 BSON_TYPE_DOCUMENT);
			if (hasValue)
			{
				let = *value;
			}
		}
		else if (StringViewEqualsCString(&keyView, "collectionUUID"))
		{
			EnsureTopLevelFieldType("collectionUUID", &aggregationIterator,
									BSON_TYPE_BINARY);
			if (value->value.v_binary.subtype != BSON_SUBTYPE_UUID ||
				value->value.v_binary.data_len != 16)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
									"field collectionUUID must be a UUID")));
			}

			collectionUuid = palloc(sizeof(pg_uuid_t));
			memcpy(&collectionUuid->data, value->value.v_binary.data, 16);
		}
		else if (StringViewEqualsCString(&keyView, "collation"))
		{
			ReportFeatureUsage(FEATURE_COLLATION);
			if (EnableCollation)
			{
				EnsureTopLevelFieldType("collation", &aggregationIterator,
										BSON_TYPE_DOCUMENT);
				ParseAndGetCollationString(value, context->collationString);
			}
			else if (!SkipFailOnCollation)
			{
				ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
								errmsg(
									"collation is not supported in the aggregate command yet.")));
			}
		}
		else if (setStatementTimeout &&
				 StringViewEqualsCString(&keyView, "maxTimeMS"))
		{
			EnsureTopLevelFieldIsNumberLike("find.maxTimeMS", value);
			SetExplicitStatementTimeout(BsonValueAsInt32(value));
		}
		else if (StringViewEqualsCString(&keyView, "$db"))
		{
			ValidateOrExtractDatabaseNameTextFromSpec(&aggregationIterator,
													  &context->databaseNameDatum);
			database = context->databaseNameDatum;
		}
		else if (!IsCommonSpecIgnoredField(keyView.string))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg("%*s is not a recognized field",
								   keyView.length, keyView.string)));
		}
	}

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

	if (pipelineValue.value_type != BSON_TYPE_ARRAY)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Required variable pipeline must be valid")));
	}

	if (collectionName.length == 0 && !isCollectionAgnosticQuery)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Required variables aggregate must be valid")));
	}

	pgbson *parsedVariables = ParseAndGetTopLevelVariableSpec(&let,
															  &queryData->
															  timeSystemVariables);

	context->variableSpec = (Expr *) MakeBsonConst(parsedVariables);

	List *aggregationStages = ExtractAggregationStages(&pipelineValue,
													   context);

	if (context->requiresTailableCursor)
	{
		queryData->cursorKind = QueryCursorType_Tailable;
	}

	/*
	 * Look up the collection (non-agnostic only) so we can detect the
	 * remote-unsharded case before generating the query. The looked-up
	 * collection is reused by the apply phase via preFetchedCollection.
	 */
	MongoCollection *collection = NULL;
	if (!isCollectionAgnosticQuery)
	{
		Datum collectionNameDatum = PointerGetDatum(
			cstring_to_text_with_len(collectionName.string, collectionName.length));
		collection = GetMongoCollectionOrViewByNameDatum(
			PointerGetDatum(context->databaseNameDatum), collectionNameDatum,
			AccessShareLock);

		/*
		 * Set the namespace name now so the remote-dispatch path (which skips the
		 * apply phase) still has it for the cursor response preamble. For the
		 * local path this is harmlessly recomputed inside the apply phase.
		 */
		queryData->namespaceName = CreateNamespaceName(context->databaseNameDatum,
													   &collectionName);
	}

	/*
	 * Validate the cursor option after the collection lookup (which acquires the
	 * table lock), so both the remote-dispatch path (which skips the apply phase)
	 * and the local path enforce it once. Validating after the lookup preserves
	 * the legacy ordering where a lock-contended command blocks (and can hit its
	 * statement timeout) before this parse-level check fires.
	 */
	if (!hasCursor && !explain && cursorParamKind != CursorParamKind_Persistent)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
						errmsg(
							"The 'cursor' option is required, except for aggregate with explain")));
	}

	if (outCollection != NULL)
	{
		*outCollection = collection;
	}

	plan->collection = collection;
	plan->queryData = queryData;
	plan->collectionName = collectionName;
	plan->aggregationStages = aggregationStages;
	plan->collectionUuid = collectionUuid;
	plan->indexHint = indexHint;
	plan->isCollectionAgnosticQuery = isCollectionAgnosticQuery;
	plan->hasCursor = hasCursor;
	plan->explain = explain;
	plan->cursorParamKind = cursorParamKind;

	return plan;
}


/*
 * Generates the local SQL query from a previously parsed aggregation plan. This is
 * the mutation phase (base table + dynamic cursor + pipeline + cursor functions)
 * and mirrors what the single-shot GenerateAggregationQuery does after parsing.
 */
Query *
ApplyParsedAggregationQuery(AggregationQueryPlan *plan)
{
	AggregationPipelineBuildContext *context = &plan->context;
	QueryData *queryData = plan->queryData;
	CursorParamKind cursorParamKind = plan->cursorParamKind;
	List *aggregationStages = plan->aggregationStages;

	Query *query;
	if (plan->isCollectionAgnosticQuery)
	{
		query = GenerateBaseAgnosticQuery(context->databaseNameDatum, context);
	}
	else
	{
		context->mongoCollection = plan->collection;
		query = GenerateBaseTableQuery(context->databaseNameDatum,
									   &plan->collectionName,
									   plan->collectionUuid, &plan->indexHint,
									   context);
	}

	/* Remember the base query - this will be needed since we need to update the cursor function on the base RTE */
	Query *baseQuery = query;

	if (cursorParamKind == CursorParamKind_Dynamic &&
		!TryAddDynamicCursorQuery(cursorParamKind, queryData, query, context))
	{
		/* Fall back to streaming cursor if dynamic cursor cannot be added */
		cursorParamKind = CursorParamKind_Streaming;

		/* If we couldn't add dynamic cursors then fall back to not allowing this */
		context->joinStatus = JoinStageStatus_Unknown;
	}

	query = MutateQueryWithPipeline(query, aggregationStages, context);

	if (context->requiresTailableCursor)
	{
		/*
		 * change stream is the only stage that requires a tailable cursor.
		 * Since change stream manages the continuation handling by itself,
		 * there is no need to add cursor params to the query.
		 */
		queryData->cursorKind = QueryCursorType_Tailable;
	}
	else if (query->commandType == CMD_MERGE)
	{
		/* CMD_MERGE is case when pipeline has output stage ($merge or $out) result will be always single batch. */
		ThrowIfServerOrTransactionReadOnly();
		queryData->cursorKind = QueryCursorType_SingleBatch;
	}
	else if (queryData->cursorKind == QueryCursorType_Unspecified)
	{
		queryData->cursorKind =
			context->requiresPersistentCursor || plan->isCollectionAgnosticQuery ?
			QueryCursorType_Persistent : QueryCursorType_Streamable;
	}

	if ((queryData->cursorKind == QueryCursorType_Streamable ||
		 queryData->cursorKind == QueryCursorType_Dynamic) &&
		context->isSingleRowResult &&
		queryData->batchSize >= 1)
	{
		queryData->cursorKind = QueryCursorType_SingleBatch;
	}

	queryData->namespaceName = context->namespaceName;
	SetCursorTopology(queryData, context);

	if (cursorParamKind == CursorParamKind_Streaming &&
		queryData->cursorKind == QueryCursorType_Streamable)
	{
		bool addCursorAsConst = false;
		AddCursorFunctionsToQuery(query, baseQuery, queryData, context,
								  addCursorAsConst);
	}
	else if (queryData->cursorKind == QueryCursorType_Tailable)
	{
		AddQualifierForTailableQuery(query, baseQuery, queryData,
									 context);
	}
	else if (EnableCursorsOnAggregationQueryRewrite)
	{
		bool addCursorAsConst = true;
		AddCursorFunctionsToQuery(query, baseQuery, queryData, context,
								  addCursorAsConst);
	}

	return query;
}


/*
 * Given a query and an aggregation pipeline mutates the query
 * to match the contents of the provided aggregation pipeline.
 *
 * Thin composition of the parse and apply phases. Callers that need to branch on
 * the collection topology before generating the query (e.g. aggregate first-page
 * to dispatch remote-unsharded queries to a worker) call the two phases directly.
 */
Query *
GenerateAggregationQuery(text *database, pgbson *aggregationSpec, QueryData *queryData,
						 CursorParamKind cursorParamKind, bool setStatementTimeout)
{
	AggregationQueryPlan *plan = ParseAggregationQueryAndLookupCollection(
		database, aggregationSpec, queryData, cursorParamKind, setStatementTimeout,
		NULL);

	return ApplyParsedAggregationQuery(plan);
}


inline static bool
IsNaturalSortHint(const bson_value_t *hintValue)
{
	if (hintValue == NULL ||
		hintValue->value_type != BSON_TYPE_DOCUMENT)
	{
		return false;
	}

	pgbsonelement indexHintElement;
	bson_iter_t indexHintIterator;
	BsonValueInitIterator(hintValue, &indexHintIterator);
	return hintValue->value_type == BSON_TYPE_DOCUMENT &&
		   TryGetSinglePgbsonElementFromBsonIterator(&indexHintIterator,
													 &indexHintElement) &&
		   strcmp(indexHintElement.path, "$natural") == 0 &&
		   BsonValueAsInt32(&indexHintElement.bsonValue) != 0;
}


/*
 * Parses a find command spec into a FindSpec struct.
 * Owns the entire bson_iter parsing switch, post-parse validation,
 * limit normalisation, variable parsing, and index-hint sanity check.
 */
static FindSpec
ParseFindQuery(pgbson *findSpec, QueryData *queryData,
			   bool setStatementTimeout, AggregationPipelineBuildContext *context)
{
	bson_iter_t findIterator;
	PgbsonInitIterator(findSpec, &findIterator);

	StringView collectionName = { 0 };
	bool hasFind = false;
	bool hasNtoreturn = false;
	bool hasBatchSize = false;
	bool isNtoReturnSupported = IsNtoReturnSupported();
	bson_value_t filter = { 0 };
	bson_value_t limit = { 0 };
	bson_value_t projection = { 0 };
	bson_value_t sort = { 0 };
	bson_value_t skip = { 0 };
	bson_value_t let = { 0 };
	bson_value_t indexHint = { 0 };
	pg_uuid_t *collectionUuid = NULL;

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

		/*
		 * Key off of the first character to avoid the several "if/else" checks for every
		 * key. If any letter becomes to long, we should add another lookup table
		 * block to speed up the comparison.
		 */
		switch (keyView.string[0])
		{
			case '$':
			{
				if (StringViewEqualsCString(&keyView, "$db"))
				{
					/*
					 * Extracts and validates the database name from the spec into
					 * context->databaseNameDatum (the authoritative value used to
					 * build the FindSpec). The databaseDatum parameter is only the
					 * initial default and is not updated here.
					 */
					ValidateOrExtractDatabaseNameTextFromSpec(&findIterator,
															  &context->databaseNameDatum);
					continue;
				}

				goto default_find_case;
			}

			case 'a':
			{
				if (StringViewEqualsCString(&keyView, "allowDiskUse") ||
					StringViewEqualsCString(&keyView, "allowPartialResults"))
				{
					/* We ignore this for now (TODO Support this?) */
					continue;
				}

				goto default_find_case;
			}

			case 'b':
			{
				if (StringViewEqualsCString(&keyView, "batchSize"))
				{
					/* In case ntoreturn is present and has been parsed already we throw this error */
					if (!isNtoReturnSupported && hasNtoreturn)
					{
						ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
										errmsg(
											"'limit' or 'batchSize' fields can not be set with 'ntoreturn' field")));
					}
					SetBatchSize("batchSize", value, queryData);
					hasBatchSize = !isNtoReturnSupported;
					continue;
				}

				goto default_find_case;
			}

			case 'c':
			{
				if (StringViewEqualsCString(&keyView, "collation"))
				{
					ReportFeatureUsage(FEATURE_COLLATION);
					if (EnableCollation)
					{
						EnsureTopLevelFieldType("collation", &findIterator,
												BSON_TYPE_DOCUMENT);
						ParseAndGetCollationString(value, context->collationString);
					}
					else if (!SkipFailOnCollation)
					{
						ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
										errmsg(
											"collation is not supported in the find command yet.")));
					}
					continue;
				}

				goto default_find_case;
			}

			case 'f':
			{
				if (StringViewEqualsCString(&keyView, "find"))
				{
					hasFind = true;
					EnsureTopLevelFieldType("find", &findIterator, BSON_TYPE_UTF8);
					collectionName.string = bson_iter_utf8(&findIterator,
														   &collectionName.length);
					continue;
				}
				else if (StringViewEqualsCString(&keyView, "filter"))
				{
					EnsureTopLevelFieldType("filter", &findIterator, BSON_TYPE_DOCUMENT);
					filter = *value;
					continue;
				}

				goto default_find_case;
			}

			case 'h':
			{
				if (StringViewEqualsCString(&keyView, "hint"))
				{
					ProcessIndexHint(&findIterator, &indexHint);
					continue;
				}

				goto default_find_case;
			}

			case 'l':
			{
				if (StringViewEqualsCString(&keyView, "limit"))
				{
					/* In case ntoreturn is present and has been parsed already we throw this error */
					if (!isNtoReturnSupported && hasNtoreturn)
					{
						ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
										errmsg(
											"'limit' or 'batchSize' fields can not be set with 'ntoreturn' field")));
					}

					/* Validation handled in the stage processing */
					limit = *value;
					continue;
				}
				else if (StringViewEqualsCString(&keyView, "let"))
				{
					ReportFeatureUsage(FEATURE_LET_TOP_LEVEL);

					EnsureTopLevelFieldType("let", &findIterator, BSON_TYPE_DOCUMENT);

					let = *value;
					continue;
				}
				else if (StringViewEqualsCString(&keyView, "lsid"))
				{
					/* Commonly ignored spec, add here to not pay cost of bsearch for hotpaths */
					continue;
				}

				goto default_find_case;
			}

			case 'm':
			{
				if (StringViewEqualsCString(&keyView, "min") ||
					StringViewEqualsCString(&keyView, "max"))
				{
					/* We ignore this for now (TODO Support this?) */
					continue;
				}
				else if (setStatementTimeout &&
						 StringViewEqualsCString(&keyView, "maxTimeMS"))
				{
					EnsureTopLevelFieldIsNumberLike("find.maxTimeMS", value);
					SetExplicitStatementTimeout(BsonValueAsInt32(value));
					continue;
				}

				goto default_find_case;
			}

			case 'n':
			{
				if (StringViewEqualsCString(&keyView, "ntoreturn"))
				{
					/* In case hook requests, we support ntoreturn */
					if (isNtoReturnSupported)
					{
						SetBatchSize("ntoreturn", value, queryData);
					}

					/* In case ntoreturn is the last option in the find command we first check if batchSize or limit is present */
					if (limit.value_type != BSON_TYPE_EOD || hasBatchSize)
					{
						ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
										errmsg(
											"'limit' or 'batchSize' fields can not be set with 'ntoreturn' field")));
					}
					hasNtoreturn = !isNtoReturnSupported;
					continue;
				}
				else if (StringViewEqualsCString(&keyView, "noCursorTimeout"))
				{
					/* We ignore this for now (TODO Support this?) */
					continue;
				}

				goto default_find_case;
			}

			case 'p':
			{
				if (StringViewEqualsCString(&keyView, "projection"))
				{
					/* Validation handled in the stage processing */
					/* TODO - Protocol behavior validates projection even if collection is not present */
					/* to align with that we may need to validate projection here, like $elemMatch envolve $jsonSchema */
					projection = *value;
					continue;
				}

				goto default_find_case;
			}

			case 'r':
			{
				if (StringViewEqualsCString(&keyView, "returnKey"))
				{
					if (BsonValueAsBool(value))
					{
						/* fail if returnKey or showRecordId are present and with boolean value true, else ignore */
						ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_COMMANDNOTSUPPORTED),
										errmsg("The key %.*s is currently not supported",
											   keyView.length, keyView.string),
										errdetail_log(
											"The key %.*s is currently not supported",
											keyView.length, keyView.string)));
					}

					continue;
				}

				goto default_find_case;
			}

			case 's':
			{
				if (StringViewEqualsCString(&keyView, "skip"))
				{
					/* Validation handled in the stage processing */
					skip = *value;
					continue;
				}
				else if (StringViewEqualsCString(&keyView, "sort"))
				{
					EnsureTopLevelFieldType("sort", &findIterator, BSON_TYPE_DOCUMENT);
					sort = *value;
					continue;
				}
				else if (StringViewEqualsCString(&keyView, "singleBatch"))
				{
					EnsureTopLevelFieldType("singleBatch", &findIterator, BSON_TYPE_BOOL);
					if (value->value.v_bool)
					{
						queryData->cursorKind = QueryCursorType_SingleBatch;
					}
					continue;
				}
				else if (StringViewEqualsCString(&keyView, "showRecordId"))
				{
					if (BsonValueAsBool(value))
					{
						/* fail if returnKey or showRecordId are present and with boolean value true, else ignore */
						ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_COMMANDNOTSUPPORTED),
										errmsg("The key %.*s is currently not supported",
											   keyView.length, keyView.string),
										errdetail_log(
											"The key %.*s is currently not supported",
											keyView.length, keyView.string)));
					}

					continue;
				}

				goto default_find_case;
			}

default_find_case:
			default:
			{
				if (!IsCommonSpecIgnoredField(keyView.string))
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
									errmsg("%.*s is an unknown field",
										   keyView.length, keyView.string),
									errdetail_log("%.*s is an unknown field",
												  keyView.length, keyView.string)));
				}
			}
		}
	}

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

	if (!hasFind)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
						errmsg("Required element \"find\" missing.")));
	}
	else if (collectionName.length == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INVALIDNAMESPACE),
						errmsg("Collection name must not be left empty.")));
	}

	/* In case only ntoreturn is present we give a different error.*/
	if (!isNtoReturnSupported && hasNtoreturn)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5746102),
						errmsg("Command is not supported for cluster version >= 5.1")));
	}

	/* Find supports negative limit (as well as count) */
	if (BsonValueIsNumber(&limit))
	{
		int64_t intValue = BsonValueAsInt64(&limit);
		if (intValue < 0)
		{
			limit.value.v_int64 = labs(intValue);
			limit.value_type = BSON_TYPE_INT64;
		}
		else if (intValue == 0)
		{
			limit.value_type = BSON_TYPE_EOD;
		}
	}

	pgbson *parsedVariables = ParseAndGetTopLevelVariableSpec(&let,
															  &queryData->
															  timeSystemVariables);

	if (indexHint.value_type != BSON_TYPE_EOD)
	{
		/* Validate hint */
		if (!IsNaturalSortHint(&indexHint) &&
			IsNaturalSortHint(&sort))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg(
								"Cannot provide natural sort with a non-natural index hint")));
		}
	}

	FindSpec spec = { 0 };
	spec.databaseDatum = context->databaseNameDatum;
	spec.collectionName = collectionName;
	spec.collectionUuid = collectionUuid;
	spec.filter = filter;
	spec.limit = limit;
	spec.projection = projection;
	spec.sort = sort;
	spec.skip = skip;
	spec.let = let;
	spec.indexHint = indexHint;
	spec.parsedVariables = parsedVariables;
	return spec;
}


/*
 * Looks up the collection, sets context fields, and calls ApplyFindSpecCore.
 */
static Query *
ApplyFindSpec(const FindSpec *spec, MongoCollection *collection,
			  QueryData *queryData, CursorParamKind cursorParamKind,
			  AggregationPipelineBuildContext *context)
{
	context->variableSpec = (Expr *) MakeBsonConst(spec->parsedVariables);

	context->isSingleRowResult = false;
	if (spec->sort.value_type != BSON_TYPE_EOD)
	{
		context->requiresPersistentCursor = true;
	}

	if (RequiresPersistentCursorSkip(&spec->skip, &context->isSingleRowResult))
	{
		context->requiresPersistentCursor = true;
	}

	if (RequiresPersistentCursorLimit(&spec->limit, &context->isSingleRowResult))
	{
		context->requiresPersistentCursor = true;
	}

	context->mongoCollection = collection;
	Query *query = GenerateBaseTableQuery(spec->databaseDatum, &spec->collectionName,
										  spec->collectionUuid, &spec->indexHint,
										  context);
	Query *baseQuery = query;

	return ApplyFindSpecCore(spec, query, baseQuery, queryData, cursorParamKind, context);
}


/*
 * Mutates *query* with the find-spec stages (match / sort / skip / limit /
 * project) and attaches cursor functions.
 */
static Query *
ApplyFindSpecCore(const FindSpec *spec, Query *query, Query *baseQuery,
				  QueryData *queryData, CursorParamKind cursorParamKind,
				  AggregationPipelineBuildContext *context)
{
	if (cursorParamKind == CursorParamKind_Dynamic &&
		!TryAddDynamicCursorQuery(cursorParamKind, queryData, query, context))
	{
		/* Fall back to streaming cursor if dynamic cursor cannot be added */
		cursorParamKind = CursorParamKind_Streaming;

		/* If we couldn't add dynamic cursors then fall back to not allowing this */
		context->joinStatus = JoinStageStatus_Unknown;
	}

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

	/* Then apply sort */
	if (spec->sort.value_type != BSON_TYPE_EOD)
	{
		query = HandleSort(&spec->sort, query, context);
		context->stageNum++;
	}

	/* Then do skip and then limit */
	if (spec->skip.value_type != BSON_TYPE_EOD)
	{
		query = HandleSkip(&spec->skip, query, context);
		context->stageNum++;
	}

	if (spec->limit.value_type != BSON_TYPE_EOD)
	{
		query = HandleLimit(&spec->limit, query, context);
		context->stageNum++;
	}

	/* $near and $nearSphere add sort clause to query, for them we need persistent cursor. */
	if (query->sortClause)
	{
		context->requiresPersistentCursor = true;
	}

	/* finally update projection */
	if (spec->projection.value_type != BSON_TYPE_EOD)
	{
		/* Before applying projection - check if we need to
		 * push to a subquery. We do this only if we have
		 * skip to avoid projecting on documents we won't need.
		 */
		if (context->requiresSubQuery &&
			context->requiresPersistentCursor &&
			query->limitOffset != NULL)
		{
			query = MigrateQueryToSubQuery(query, context);
		}

		query = HandleProjectFind(&spec->projection, &spec->filter, query, context);
	}

	if (rt_fetch(1, query->rtable)->rtekind != RTE_RELATION)
	{
		/* Any attempts to push to a subquery should invalidate point read plans */
		context->isPointReadQuery = false;
	}

	if (queryData->cursorKind == QueryCursorType_Unspecified)
	{
		queryData->cursorKind = context->requiresPersistentCursor ?
								QueryCursorType_Persistent : QueryCursorType_Streamable;
	}

	queryData->namespaceName = context->namespaceName;
	SetCursorTopology(queryData, context);
	if (context->isPointReadQuery &&
		context->allowShardBaseTable && queryData->batchSize >= 1)
	{
		/* If we're still targeting the local shard && we have a point read
		 * Mark the query for a point read plan.
		 */
		queryData->cursorKind = QueryCursorType_PointRead;
	}

	if ((queryData->cursorKind == QueryCursorType_Streamable ||
		 queryData->cursorKind == QueryCursorType_Dynamic) &&
		context->isSingleRowResult &&
		queryData->batchSize >= 1)
	{
		queryData->cursorKind = QueryCursorType_SingleBatch;
	}

	if (cursorParamKind == CursorParamKind_Streaming)
	{
		bool addCursorAsConst = false;
		AddCursorFunctionsToQuery(query, baseQuery, queryData, context,
								  addCursorAsConst);
	}
	else if (EnableCursorsOnAggregationQueryRewrite)
	{
		bool addCursorAsConst = true;
		AddCursorFunctionsToQuery(query, baseQuery, queryData, context,
								  addCursorAsConst);
	}

	return query;
}


/*
 * Parses a find command spec and looks up the target collection, WITHOUT
 * generating the underlying SQL query. Determines whether the collection is
 * remote-unsharded (so callers may dispatch to a worker instead of generating
 * and discarding a local query).
 *
 * On return:
 *   *outIsRemoteUnsharded  - true iff the collection's single shard lives on a
 *                            remote worker (no usable local shard table).
 *   *outDistributedTableOid - the OID of the distributed table for the
 *                            collection (used for remote dispatch routing);
 *                            InvalidOid when the collection does not exist.
 *
 * The returned handle is allocated in the current memory context and is passed
 * to ApplyParsedFindQuery to finish generating the local query.
 */
FindQueryPlan *
ParseFindQueryAndLookupCollection(text *database, pgbson *findSpec,
								  QueryData *queryData, bool setStatementTimeout,
								  MongoCollection **outCollection)
{
	FindQueryPlan *plan = palloc0(sizeof(FindQueryPlan));
	plan->queryData = queryData;

	/* Queries start out as persistent cursor */
	queryData->cursorKind = QueryCursorType_Unspecified;

	/* For finds, we can generally query the shard directly if available. */
	plan->context.allowShardBaseTable = true;
	plan->context.databaseNameDatum = database;

	/* Find queries have no joins */
	plan->context.joinStatus = JoinStageStatus_NoJoinsOrUnions;
	plan->spec = ParseFindQuery(findSpec, queryData,
								setStatementTimeout, &plan->context);

	plan->collection = GetMongoCollectionOrViewByNameDatum(
		PointerGetDatum(plan->spec.databaseDatum),
		PointerGetDatum(cstring_to_text_with_len(plan->spec.collectionName.string,
												 plan->spec.collectionName.length)),
		AccessShareLock);

	/*
	 * Set the namespace name now so the remote-dispatch path (which skips
	 * ApplyFindSpec / GenerateBaseTableQuery) still has it for the cursor
	 * response preamble. For the local path this is harmlessly recomputed to the
	 * same value inside ApplyFindSpecCore.
	 */
	queryData->namespaceName = CreateNamespaceName(plan->spec.databaseDatum,
												   &plan->spec.collectionName);

	if (outCollection != NULL)
	{
		*outCollection = plan->collection;
	}

	return plan;
}


/*
 * Generates the local SQL query from a previously parsed find plan. This is the
 * mutation phase (base table + stages + cursor functions) and mirrors what the
 * single-shot GenerateFindQuery does after parsing.
 */
Query *
ApplyParsedFindQuery(FindQueryPlan *plan, CursorParamKind cursorParamKind)
{
	return ApplyFindSpec(&plan->spec, plan->collection, plan->queryData,
						 cursorParamKind, &plan->context);
}


/*
 * Applies a find spec against a query and expands it into the underlying SQL AST.
 */
Query *
GenerateFindQuery(text *databaseDatum, pgbson *findSpec, QueryData *queryData,
				  CursorParamKind cursorParamKind, bool setStatementTimeout)
{
	FindQueryPlan *plan = ParseFindQueryAndLookupCollection(
		databaseDatum, findSpec, queryData, setStatementTimeout, NULL);

	return ApplyParsedFindQuery(plan, cursorParamKind);
}


Query *
BuildAggregationCursorGetMoreQuery(text *database, pgbson *getMoreSpec,
								   pgbson *continuationSpec)
{
	/* Form a funcExpr query that is just calling the getMore function
	 * these cursor kinds don't really execute a brand new query on the getMore
	 * and they simply drain what was persisted in the first phase.
	 */
	Query *query = makeNode(Query);
	query->commandType = CMD_SELECT;
	query->querySource = QSRC_ORIGINAL;
	query->canSetTag = true;

	List *colNames = list_make2(makeString("cursorpage"), makeString("continuation"));
	RangeTblEntry *rte = makeNode(RangeTblEntry);
	rte->rtekind = RTE_FUNCTION;
	rte->relid = InvalidOid;

	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;
	rte->coltypes = list_make2_oid(BsonTypeId(), BsonTypeId());
	rte->coltypmods = list_make2_int(-1, -1);
	rte->colcollations = list_make2_oid(InvalidOid, InvalidOid);
	rte->ctename = NULL;
	rte->ctelevelsup = 0;

	Const *databaseConst = makeConst(TEXTOID, -1, DEFAULT_COLLATION_OID, -1,
									 PointerGetDatum(database),
									 false, false);
	List *queryArgs = list_make3(databaseConst, MakeBsonConst(getMoreSpec), MakeBsonConst(
									 continuationSpec));

	/* Now create the rtfunc*/
	FuncExpr *rangeFunc = makeFuncExpr(CursorGetMoreFunctionOid(), RECORDOID, queryArgs,
									   InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);

	RangeTblFunction *rangeTableFunction = makeNode(RangeTblFunction);
	rangeTableFunction->funccolcount = 2;
	rangeTableFunction->funccolnames = colNames;
	rangeTableFunction->funccoltypes = list_make2_oid(BsonTypeId(), BsonTypeId());
	rangeTableFunction->funccoltypmods = list_make2_int(-1, -1);
	rangeTableFunction->funccolcollations = list_make2_oid(InvalidOid, 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);
	Var *continuationEntry = makeVar(1, 2, BsonTypeId(), -1, InvalidOid, 0);
	TargetEntry *documentTargetEntry = makeTargetEntry((Expr *) documentEntry, 1,
													   "cursorpage",
													   false);
	TargetEntry *continuationTargetEntry = makeTargetEntry((Expr *) continuationEntry, 2,
														   "continuation",
														   false);
	query->targetList = list_make2(documentTargetEntry, continuationTargetEntry);
	return query;
}


/*
 * Generates a query that is akin to $count command protocol.
 */
Query *
GenerateCountQuery(text *databaseDatum, pgbson *countSpec, bool setStatementTimeout)
{
	AggregationPipelineBuildContext context = { 0 };
	context.databaseNameDatum = databaseDatum;
	context.joinStatus = JoinStageStatus_Unknown;

	bson_iter_t countIterator;
	PgbsonInitIterator(countSpec, &countIterator);

	StringView collectionName = { 0 };
	bson_value_t filter = { 0 };
	bson_value_t limit = { 0 };
	bson_value_t skip = { 0 };
	bson_value_t indexHint = { 0 };
	pg_uuid_t *collectionUuid = NULL;

	bool appendOkResult = false;
	bool hasQueryModifier = false;
	context.allowShardBaseTable = true;
	while (bson_iter_next(&countIterator))
	{
		StringView keyView = bson_iter_key_string_view(&countIterator);
		const bson_value_t *value = bson_iter_value(&countIterator);

		if (StringViewEqualsCString(&keyView, "count"))
		{
			EnsureTopLevelFieldType("count", &countIterator, BSON_TYPE_UTF8);
			collectionName.string = bson_iter_utf8(&countIterator,
												   &collectionName.length);
		}
		else if (StringViewEqualsCString(&keyView, "query"))
		{
			EnsureTopLevelFieldType("query", &countIterator, BSON_TYPE_DOCUMENT);
			filter = *value;

			/* Only do runtime count if there's a non-empty filter */
			hasQueryModifier = hasQueryModifier || !IsBsonValueEmptyDocument(value);
		}
		else if (StringViewEqualsCString(&keyView, "limit"))
		{
			/* Validation handled in the stage processing */
			limit = *value;
			hasQueryModifier = true;
		}
		else if (StringViewEqualsCString(&keyView, "skip"))
		{
			/* Validation handled in the stage processing */
			skip = *value;
			hasQueryModifier = true;
		}
		else if (StringViewEqualsCString(&keyView, "hint"))
		{
			hasQueryModifier = true;
			ProcessIndexHint(&countIterator, &indexHint);
		}
		else if (StringViewEqualsCString(&keyView, "fields"))
		{
			/* We ignore this for now (TODO Support this?) */
		}
		else if (setStatementTimeout && StringViewEqualsCString(&keyView, "maxTimeMS"))
		{
			EnsureTopLevelFieldIsNumberLike("distinct.maxTimeMS", value);
			SetExplicitStatementTimeout(BsonValueAsInt32(value));
		}
		else if (StringViewEqualsCString(&keyView, "collation"))
		{
			ReportFeatureUsage(FEATURE_COLLATION);
			if (!SkipFailOnCollation)
			{
				ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
								errmsg(
									"collation is not supported in the count command yet.")));
			}
		}
		else if (StringViewEqualsCString(&keyView, "$db"))
		{
			text *prevDb = context.databaseNameDatum;
			ValidateOrExtractDatabaseNameTextFromSpec(&countIterator,
													  &context.databaseNameDatum);
			if (prevDb == NULL)
			{
				databaseDatum = context.databaseNameDatum;
			}
		}
		else if (!IsCommonSpecIgnoredField(keyView.string))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg("%.*s is an unknown field",
								   keyView.length, keyView.string)));
		}
	}

	if (context.databaseNameDatum == NULL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
						errmsg("Required element \"$db\" missing.")));
	}

	if (collectionName.length == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INVALIDNAMESPACE),
						errmsg("Collection name must not be left empty.")));
	}

	Query *query = GenerateBaseTableQuery(databaseDatum, &collectionName, collectionUuid,
										  &indexHint, &context);

	/*
	 * the count() query which has no filter/skip/limit/etc can be done via an estimatedDocumentCount
	 * In this case, we rewrite the query as a collStats aggregation query with a project to make it the appropriate output.
	 */
	if (!hasQueryModifier && context.mongoCollection != NULL)
	{
		/* Collection exists, get a collStats: { "count": {} } */
		pgbson_writer collStatsWriter;
		pgbson_writer countWriter;
		PgbsonWriterInit(&collStatsWriter);
		PgbsonWriterStartDocument(&collStatsWriter, "count", 5, &countWriter);
		PgbsonWriterEndDocument(&collStatsWriter, &countWriter);
		pgbson *collStatsSpec = PgbsonWriterGetPgbson(&collStatsWriter);
		bson_value_t collStatsSpecValue = ConvertPgbsonToBsonValue(collStatsSpec);
		query = HandleCollStats(&collStatsSpecValue, query, &context);
		context.stageNum++;

		/* Add a $project where the count field is written as 'n' */
		pgbson_writer projectWriter;
		PgbsonWriterInit(&projectWriter);
		PgbsonWriterAppendUtf8(&projectWriter, "n", 1, "$count");
		pgbson *projectSpec = PgbsonWriterGetPgbson(&projectWriter);
		bson_value_t projectSpecValue = ConvertPgbsonToBsonValue(projectSpec);
		query = HandleProject(&projectSpecValue, query, &context);
		appendOkResult = true;
	}
	else
	{
		/* First apply match */
		if (filter.value_type != BSON_TYPE_EOD)
		{
			query = HandleMatch(&filter, query, &context);
			context.stageNum++;
		}

		/* Then do skip and then limit */
		if (skip.value_type != BSON_TYPE_EOD)
		{
			if (skip.value_type != BSON_TYPE_NULL)
			{
				if (!BsonValueIsNumber(&skip))
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_TYPEMISMATCH)),
							errmsg(
								"The BSON field 'skip' has an incorrect type '%s'; it should be one of the following types: [long, int, decimal, double].",
								BsonTypeName(skip.value_type)),
							errdetail_log(
								"The BSON field 'skip' has an incorrect type '%s'; it should be one of the following types: [long, int, decimal, double].",
								BsonTypeName(skip.value_type)));
				}

				int64_t skipValue = BsonValueAsInt64(&skip);
				if (skipValue < 0)
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION51024)),
							errmsg(
								"The BSON field 'skip' requires a value greater than or equal to 0, but the given value is '%ld'.",
								skipValue),
							errdetail_log(
								"The BSON field 'skip' requires a value greater than or equal to 0, but the given value is '%ld'.",
								skipValue));
				}

				query = HandleSkip(&skip, query, &context);
			}
			context.stageNum++;
			context.requiresPersistentCursor = true;
		}

		/* Count supports negative limit */
		if (BsonValueIsNumber(&limit))
		{
			int64_t intValue = BsonValueAsInt64(&limit);
			if (intValue < 0)
			{
				limit.value.v_int64 = labs(intValue);
				limit.value_type = BSON_TYPE_INT64;
			}
			else if (intValue == 0)
			{
				limit.value_type = BSON_TYPE_EOD;
			}
		}

		if (limit.value_type != BSON_TYPE_EOD)
		{
			query = HandleLimit(&limit, query, &context);
			context.stageNum++;
			if (RequiresPersistentCursorLimit(&limit, &context.isSingleRowResult))
			{
				context.requiresPersistentCursor = true;
			}
		}

		/* Now add a count stage that writes to the field "n" */
		bson_value_t countValue = { 0 };
		countValue.value_type = BSON_TYPE_UTF8;
		countValue.value.v_utf8.len = 1;
		countValue.value.v_utf8.str = "n";

		bool isCountCommand = true;
		query = HandleCountCore(&countValue, query, &context, isCountCommand);
	}

	if (appendOkResult)
	{
		/* Now add the "ok": 1 as an add fields stage. */
		pgbson_writer addFieldsWriter;
		PgbsonWriterInit(&addFieldsWriter);
		PgbsonWriterAppendDouble(&addFieldsWriter, "ok", 2, 1);
		pgbson *addFieldsSpec = PgbsonWriterGetPgbson(&addFieldsWriter);
		bson_value_t addFieldsValue = ConvertPgbsonToBsonValue(addFieldsSpec);
		query = HandleSimpleProjectionStage(&addFieldsValue, query, &context,
											"$addFields",
											BsonDollaMergeDocumentsFunctionOid(), NULL,
											NULL);
	}

	return query;
}


/*
 * Generates a query that is akin to the $distinct command protocol.
 */
Query *
GenerateDistinctQuery(text *databaseDatum, pgbson *distinctSpec, bool setStatementTimeout)
{
	AggregationPipelineBuildContext context = { 0 };
	context.databaseNameDatum = databaseDatum;
	context.joinStatus = JoinStageStatus_Unknown;

	bson_iter_t distinctIter;
	PgbsonInitIterator(distinctSpec, &distinctIter);

	StringView collectionName = { 0 };
	bool hasDistinct = false;
	bson_value_t filter = { 0 };
	bson_value_t indexHint = { 0 };
	StringView distinctKey = { 0 };
	pg_uuid_t *collectionUuid = NULL;

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

		if (StringViewEqualsCString(&keyView, "distinct"))
		{
			hasDistinct = true;
			EnsureTopLevelFieldType("distinct", &distinctIter, BSON_TYPE_UTF8);
			collectionName.string = bson_iter_utf8(&distinctIter, &collectionName.length);
		}
		else if (StringViewEqualsCString(&keyView, "query"))
		{
			if (!BSON_ITER_HOLDS_NULL(&distinctIter))
			{
				EnsureTopLevelFieldType("query", &distinctIter, BSON_TYPE_DOCUMENT);
				filter = *value;
			}
		}
		else if (StringViewEqualsCString(&keyView, "hint"))
		{
			ProcessIndexHint(&distinctIter, &indexHint);
		}
		else if (StringViewEqualsCString(&keyView, "key"))
		{
			/* Validation handled in the stage processing */
			EnsureTopLevelFieldType("key", &distinctIter, BSON_TYPE_UTF8);
			distinctKey.string = bson_iter_utf8(&distinctIter, &distinctKey.length);
		}
		else if (setStatementTimeout && StringViewEqualsCString(&keyView, "maxTimeMS"))
		{
			EnsureTopLevelFieldIsNumberLike("distinct.maxTimeMS", value);
			SetExplicitStatementTimeout(BsonValueAsInt32(value));
		}
		else if (StringViewEqualsCString(&keyView, "collation"))
		{
			ReportFeatureUsage(FEATURE_COLLATION);
			if (!SkipFailOnCollation)
			{
				ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
								errmsg(
									"collation is not supported in the distinct command yet.")));
			}
		}
		else if (StringViewEqualsCString(&keyView, "$db"))
		{
			text *prevDb = context.databaseNameDatum;
			ValidateOrExtractDatabaseNameTextFromSpec(&distinctIter,
													  &context.databaseNameDatum);
			if (prevDb == NULL)
			{
				databaseDatum = context.databaseNameDatum;
			}
		}
		else if (!IsCommonSpecIgnoredField(keyView.string))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg("%.*s is an unknown field",
								   keyView.length, keyView.string)));
		}
	}

	if (context.databaseNameDatum == NULL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
						errmsg("Required element \"$db\" missing.")));
	}

	if (!hasDistinct)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
						errmsg("Required element \"distinct\" missing.")));
	}
	else if (collectionName.length == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INVALIDNAMESPACE),
						errmsg("Collection name must not be left empty.")));
	}

	if (distinctKey.length == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
						errmsg("A distinct key value must not be left empty.")));
	}

	if (strlen(distinctKey.string) != distinctKey.length)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_KEYCANNOTCONTAINNULLBYTE),
						errmsg(
							"A distinct key value cannot contain any embedded null characters")));
	}

	context.allowShardBaseTable = true;
	Query *query = GenerateBaseTableQuery(databaseDatum, &collectionName, collectionUuid,
										  &indexHint, &context);

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

	query = HandleDistinct(&distinctKey, query, &context);

	return query;
}


/*
 * Parses the GetMore wire protocol spec and returns the cursorId associated
 * with it. Also updates the queryData with cursor related information.
 */
int64_t
ParseGetMore(text **databaseName, pgbson *getMoreSpec, QueryData *queryData, bool
			 setStatementTimeout, bool isTailableCursor)
{
	bson_iter_t cursorSpecIter;
	PgbsonInitIterator(getMoreSpec, &cursorSpecIter);
	int64_t cursorId = 0;
	StringView nameView = { 0 };
	while (bson_iter_next(&cursorSpecIter))
	{
		const char *pathKey = bson_iter_key(&cursorSpecIter);

		if (strcmp(pathKey, "getMore") == 0)
		{
			EnsureTopLevelFieldType("getMore", &cursorSpecIter, BSON_TYPE_INT64);
			const bson_value_t *value = bson_iter_value(&cursorSpecIter);
			cursorId = BsonValueAsInt64(value);
		}
		else if (strcmp(pathKey, "batchSize") == 0)
		{
			const bson_value_t *value = bson_iter_value(&cursorSpecIter);
			SetBatchSize("batchSize", value, queryData);
		}
		else if (strcmp(pathKey, "collection") == 0)
		{
			const bson_value_t *value = bson_iter_value(&cursorSpecIter);
			EnsureTopLevelFieldValueType("collection", value, BSON_TYPE_UTF8);
			nameView = (StringView) {
				.string = value->value.v_utf8.str,
				.length = value->value.v_utf8.len
			};
		}
		else if (setStatementTimeout && strcmp(pathKey, "maxTimeMS") == 0)
		{
			const bson_value_t *value = bson_iter_value(&cursorSpecIter);
			EnsureTopLevelFieldIsNumberLike("getMore.maxTimeMS", value);
			int32_t maxTimeMS = BsonValueAsInt32(value);

			if (EnableTailableCursorMaxAwaitTime && isTailableCursor)
			{
				/*
				 * For tailable cursors such as change streams, drivers stash the
				 * application-specified awaitData timeout on the client
				 * side and copy it into maxTimeMS on every getMore; the
				 * literal "maxAwaitTimeMS" field is never sent on the
				 * wire.  Here it indicates the maximum time the server
				 * should block waiting for new data — NOT a statement
				 * timeout.  Hand it to the tailable cursor handler so it
				 * can return the polling interval to the gateway.
				 */
				queryData->maxAwaitTimeMS = (int64_t) maxTimeMS;
			}
			else
			{
				/*
				 * For regular cursors, maxTimeMS is a statement timeout.
				 * Also the fallback path when the maxAwaitTimeMS feature is
				 * disabled via the EnableTailableCursorMaxAwaitTime kill switch:
				 * we revert to the pre-feature behavior of treating maxTimeMS
				 * as a statement timeout for tailable cursors as well.
				 */
				SetExplicitStatementTimeout(maxTimeMS);
			}
		}
		else if (strcmp(pathKey, "$db") == 0)
		{
			ValidateOrExtractDatabaseNameTextFromSpec(&cursorSpecIter, databaseName);
		}
		else if (!IsCommonSpecIgnoredField(pathKey))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg("%s is an unrecognized field name",
								   pathKey)));
		}
	}

	if (nameView.length == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INVALIDNAMESPACE),
						errmsg("Collection name must not be left empty.")));
	}

	if (*databaseName == NULL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
						errmsg("Required element \"$db\" missing.")));
	}

	queryData->namespaceName = CreateNamespaceName(*databaseName,
												   &nameView);

	if (cursorId == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
						errmsg("Required element \"getMore\" missing.")));
	}

	return cursorId;
}


/*
 * Given an aggregation pipeline that is specified for a
 * $lookup Stage, validates whether the stages can be
 * safely pulled up to the top level join.
 * This is true if there's no intersection between those
 * stages and the lookup's localField which is specified
 * on the right table.
 */
bool
CanInlineLookupPipeline(const bson_value_t *pipeline,
						const StringView *lookupPath,
						bool hasLet,
						pgbson **inlinedPipeline,
						pgbson **nonInlinedPipeline,
						bool *pipelineIsValid)
{
	bson_iter_t pipelineIter;
	BsonValueInitIterator(pipeline, &pipelineIter);

	bool canInline = true;
	pgbson_writer inlineWriter;
	pgbson_writer nonInlineWriter;

	PgbsonWriterInit(&inlineWriter);
	PgbsonWriterInit(&nonInlineWriter);

	pgbson_array_writer inlineArrayWriter;
	pgbson_array_writer nonInlineArrayWriter;

	PgbsonWriterStartArray(&inlineWriter, "", 0, &inlineArrayWriter);
	PgbsonWriterStartArray(&nonInlineWriter, "", 0, &nonInlineArrayWriter);
	int pipelineSize = 0;
	while (bson_iter_next(&pipelineIter))
	{
		pipelineSize++;
		if (!BSON_ITER_HOLDS_DOCUMENT(&pipelineIter))
		{
			canInline = false;
			*pipelineIsValid = false;
			continue;
		}

		const bson_value_t *stageValue = bson_iter_value(&pipelineIter);
		pgbsonelement stageElement;
		if (!TryGetBsonValueToPgbsonElement(stageValue, &stageElement))
		{
			canInline = false;
			*pipelineIsValid = false;
			continue;
		}

		/* Now handle each stage */
		const AggregationStageDefinition *definition = bsearch(
			stageElement.path, StageDefinitions,
			AggregationStageCount,
			sizeof(AggregationStageDefinition),
			CompareStageByStageName);
		if (definition == NULL)
		{
			canInline = false;
			*pipelineIsValid = false;
			continue;
		}

		/* Prior stage can't be inlined */
		if (!canInline)
		{
			PgbsonArrayWriterWriteValue(&nonInlineArrayWriter, stageValue);
			continue;
		}

		/* The inline function is not specified for this stage,
		 * We can't inline.
		 */
		if (definition->canInlineLookupStageFunc == NULL)
		{
			canInline = false;
			PgbsonArrayWriterWriteValue(&nonInlineArrayWriter, stageValue);
			continue;
		}

		if (!definition->canInlineLookupStageFunc(&stageElement.bsonValue, lookupPath,
												  hasLet))
		{
			canInline = false;
			PgbsonArrayWriterWriteValue(&nonInlineArrayWriter, stageValue);
			continue;
		}

		PgbsonArrayWriterWriteValue(&inlineArrayWriter, stageValue);
	}

	/*
	 * Validate number of stages for lookup nested pipeline, because after this we divide it
	 * into a set of inline and non-inlined pipelines.
	 */
	CheckMaxAllowedAggregationStages(pipelineSize);

	PgbsonWriterEndArray(&inlineWriter, &inlineArrayWriter);
	PgbsonWriterEndArray(&nonInlineWriter, &nonInlineArrayWriter);

	*pipelineIsValid = true;
	*inlinedPipeline = PgbsonWriterGetPgbson(&inlineWriter);
	*nonInlinedPipeline = PgbsonWriterGetPgbson(&nonInlineWriter);
	return canInline;
}


/*
 * Processes simple projections (dollar_project, dollar_add_fields etc).
 * This just updates the targetEntry with the new projector.
 * functionOidWithLet can be NULL if the stage doesn't support let
 */
Query *
HandleSimpleProjectionStage(const bson_value_t *existingValue, Query *query,
							AggregationPipelineBuildContext *context,
							const char *stageName, Oid functionOid,
							Oid (*functionOidWithLet)(void),
							Oid (*functionOidWithLetAndCollation)(void))
{
	if (existingValue->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40272),
						errmsg("The %s specification stage must correspond to an object",
							   stageName)));
	}

	/* The first projector is the document */
	TargetEntry *firstEntry = linitial(query->targetList);

	Expr *currentProjection = firstEntry->expr;

	/* Projection followed by projection - try to inline */
	if (TryInlineProjection((Node *) currentProjection, functionOid, existingValue))
	{
		return query;
	}

	pgbson *docBson = PgbsonInitFromBuffer(
		(char *) existingValue->value.v_doc.data,
		existingValue->value.v_doc.data_len);
	Const *addFieldsProcessed = MakeBsonConst(docBson);

	List *args;

	/* if valid collation is specified, we use the function with both variables and collation support, if applicable */
	/* else if only variableSpec is given, we use the function with let support, if applicable */
	/* else the base function */
	Const *collationConst = IsCollationApplicable(context->collationString) ?
							MakeTextConst(context->collationString,
										  strlen(context->collationString)) : NULL;

	if (collationConst && functionOidWithLetAndCollation)
	{
		args = list_make4(currentProjection,
						  addFieldsProcessed,
						  context->variableSpec ? context->variableSpec :
						  (Expr *) MakeBsonConst(PgbsonInitEmpty()),
						  collationConst);
		functionOid = functionOidWithLetAndCollation();
	}
	else if (context->variableSpec && functionOidWithLet)
	{
		args = list_make3(currentProjection, addFieldsProcessed, context->variableSpec);
		functionOid = functionOidWithLet();
	}
	else
	{
		args = list_make2(currentProjection, addFieldsProcessed);
		if (functionOid == BsonDollaMergeDocumentsFunctionOid())
		{
			bool overrideArrays = false;
			args = lappend(args, MakeBoolValueConst(overrideArrays));
		}
	}

	FuncExpr *resultExpr = makeFuncExpr(
		functionOid, BsonTypeId(), args, InvalidOid,
		InvalidOid, COERCE_EXPLICIT_CALL);

	firstEntry->expr = (Expr *) resultExpr;
	return query;
}


/**
 * Parses the input document for FirstN and LastN group expression operator and extracts the value for input and n.
 * @param inputDocument: input document for the $firstN operator
 * @param input:  this is a pointer which after parsing will hold array expression
 * @param elementsToFetch: this is a pointer which after parsing will hold n i.e. how many elements to fetch for result
 * @param opName: this contains the name of the operator for error msg formatting purposes. This value is supposed to be $firstN/$lastN.
 */
void
ParseInputForNGroupAccumulators(const bson_value_t *inputDocument,
								bson_value_t *input,
								bson_value_t *elementsToFetch, const char *opName)
{
	if (inputDocument->value_type != BSON_TYPE_DOCUMENT)
	{
		if (strcmp(opName, "$maxN") == 0 || strcmp(opName, "$minN") == 0)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5787900),
							errmsg(
								"Specification should be an object type; encountered %s: %s",
								opName, BsonValueToJsonForLogging(inputDocument)),
							errdetail_log(
								"specification must be defined as an object; opname: %s type found: %s",
								opName, BsonTypeName(inputDocument->value_type))));
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5787801),
							errmsg(
								"specification must be defined as an object, but the provided input was %s :%s",
								opName, BsonValueToJsonForLogging(inputDocument)),
							errdetail_log(
								"pecification must be defined as an object; opname: %s type found :%s",
								opName, BsonTypeName(inputDocument->value_type))));
		}
	}
	bson_iter_t docIter;
	BsonValueInitIterator(inputDocument, &docIter);

	while (bson_iter_next(&docIter))
	{
		const char *key = bson_iter_key(&docIter);
		if (strcmp(key, "input") == 0)
		{
			*input = *bson_iter_value(&docIter);
		}
		else if (strcmp(key, "n") == 0)
		{
			*elementsToFetch = *bson_iter_value(&docIter);
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5787901),
							errmsg("%s encountered an unrecognized argument value: %s",
								   opName, key),
							errdetail_log(
								"%s encountered an unrecognized argument", opName)));
		}
	}

	/**
	 * Validation check to see if input and elements to fetch are present otherwise throw error.
	 */
	if (input->value_type == BSON_TYPE_EOD)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5787907),
						errmsg("%s needs to have an 'input' field", opName),
						errdetail_log(
							"%s needs to have an 'input' field", opName)));
	}

	if (elementsToFetch->value_type == BSON_TYPE_EOD)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5787906),
						errmsg("%s requires an 'n' field", opName),
						errdetail_log(
							"%s requires an 'n' field", opName)));
	}
}


/**
 * Parses the input document for $top(N) and $bottom(N) group expression operator and extracts the value for output, sortBy and n.
 * @param inputDocument: input document for the operator
 * @param output:  this is a pointer which after parsing will hold array expression
 * @param sortSpec:  this is a pointer which after parsing will hold the sortBy expression
 * @param elementsToFetch: this is a pointer which after parsing will hold n i.e. how many elements to fetch for result
 * @param opName: this contains the name of the operator for error msg formatting purposes. This value is supposed to be $firstN/$lastN.
 */
void
ParseInputDocumentForTopAndBottom(const bson_value_t *inputDocument, bson_value_t *output,
								  bson_value_t *elementsToFetch, bson_value_t *sortSpec,
								  const char *opName)
{
	if (inputDocument->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5788001),
						errmsg(
							"specification must be defined as an object, but the provided input was %s :%s",
							opName, BsonValueToJsonForLogging(inputDocument)),
						errdetail_log(
							"specification must be defined as an object; opname: %s type found :%s",
							opName, BsonTypeName(inputDocument->value_type))));
	}
	bson_iter_t docIter;
	BsonValueInitIterator(inputDocument, &docIter);

	while (bson_iter_next(&docIter))
	{
		const char *key = bson_iter_key(&docIter);
		if (strcmp(key, "output") == 0)
		{
			*output = *bson_iter_value(&docIter);
		}
		else if (strcmp(key, "n") == 0)
		{
			*elementsToFetch = *bson_iter_value(&docIter);
		}
		else if (strcmp(key, "sortBy") == 0)
		{
			*sortSpec = *bson_iter_value(&docIter);
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5788002),
							errmsg("Unrecognized argument passed to %s: '%s'", opName,
								   key),
							errdetail_log(
								"%s found an unknown argument", opName)));
		}
	}

	/**
	 * Validation check to see if input and elements to fetch are present otherwise throw error.
	 */
	if (elementsToFetch->value_type == BSON_TYPE_EOD && (strcmp(opName, "$topN") == 0 ||
														 strcmp(opName, "$bottomN") == 0))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5788003),
						errmsg("Required value for 'n' is not provided"),
						errdetail_log(
							"%s requires an 'n' field", opName)));
	}

	if (elementsToFetch->value_type != BSON_TYPE_EOD && (strcmp(opName, "$top") == 0 ||
														 strcmp(opName, "$bottom") == 0))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5788002),
						errmsg("Unrecognized argument provided to %s 'n'", opName),
						errdetail_log(
							"Unrecognized argument provided to %s 'n'", opName)));
	}

	if (output->value_type == BSON_TYPE_EOD)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5788004),
						errmsg("The 'output' parameter is missing a required value"),
						errdetail_log(
							"%s requires an 'output' field", opName)));
	}

	if (sortSpec->value_type == BSON_TYPE_EOD)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5788005),
						errmsg("No value provided for 'sortBy'"),
						errdetail_log(
							"%s needs a 'sortBy' parameter", opName)));
	}
	else if (sortSpec->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5788604),
						errmsg(
							"Expected 'sortBy' parameter to already contain an object within the provided arguments to %s",
							opName),
						errdetail_log(
							"Expected 'sortBy' parameter to already contain an object within the provided arguments to %s",
							opName)));
	}
}


/**
 * Parses the input document for $median and $percentile accumulator operator, extracts input, p and methods.
 * @param inputDocument: input document for the operator
 * @param input:  this is a pointer which after parsing will hold input expression
 * @param p:  this is a pointer which after parsing will hold the p, i.e. the percentile array value
 * @param method: this is a pointer which after parsing will hold method
 * @param isMedianOp: this contains the name of the operator for error msg formatting purposes.
 */
void
ParseInputDocumentForMedianAndPercentile(const bson_value_t *inputDocument,
										 bson_value_t *input,
										 bson_value_t *p, bson_value_t *method,
										 bool isMedianOp)
{
	const char *opName = isMedianOp ? "$median" : "$percentile";
	if (inputDocument->value_type != BSON_TYPE_DOCUMENT)
	{
		int errorcode = isMedianOp ? ERRCODE_DOCUMENTDB_LOCATION7436100 :
						ERRCODE_DOCUMENTDB_LOCATION7429703;
		ereport(ERROR, (errcode(errorcode),
						errmsg(
							"Specification must be an object type, but instead received %s with type: %s",
							opName, BsonTypeName(inputDocument->value_type)),
						errdetail_log(
							"The %s format requires an object", opName)));
	}
	bson_iter_t docIter;
	BsonValueInitIterator(inputDocument, &docIter);

	while (bson_iter_next(&docIter))
	{
		const char *key = bson_iter_key(&docIter);
		if (strcmp(key, "input") == 0)
		{
			*input = *bson_iter_value(&docIter);
		}
		else if (strcmp(key, "method") == 0)
		{
			*method = *bson_iter_value(&docIter);
		}
		/* only evaluate p for $percentile */
		else if (!isMedianOp && strcmp(key, "p") == 0)
		{
			*p = *bson_iter_value(&docIter);
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_UNKNOWNBSONFIELD), errmsg(
								"The BSON field named with operators '$%s.%s' is not recognized.",
								opName, key),
							errdetail_log("%s found an unknown argument", opName)));
		}
	}

	/* Verify that all necessary fields exist */
	char *keyName;
	if (((input->value_type == BSON_TYPE_EOD) && (keyName = "input")) ||
		(!isMedianOp && (p->value_type == BSON_TYPE_EOD) && (keyName = "p")) ||
		((method->value_type == BSON_TYPE_EOD) && (keyName = "method")))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40414), errmsg(
							"The BSON field '$%s.%s' is required but is currently missing from the data structure",
							opName, keyName)));
	}

	/* validate method: can only be 'approximate' for now */
	if (method->value_type != BSON_TYPE_UTF8)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_TYPEMISMATCH), errmsg(
							"BSON field %s.method has an incorrect type %s; it should be of type 'string'",
							opName, BsonTypeName(method->value_type)),
						errdetail_log(
							"BSON field '$%s.method' expects type 'string'", opName)));
	}
	if (strcmp(method->value.v_utf8.str, "approximate") != 0)
	{
		/* Same error message for both $median and $percentile */
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE), errmsg(
							"Currently only 'approximate' can be used as percentile 'method'")));
	}

	/* set p for $median to 0.5 */
	if (isMedianOp)
	{
		p->value_type = BSON_TYPE_DOUBLE;
		p->value.v_double = 0.5;
	}
}


/**
 * This function validates and throws error in case bson type is not a numeric > 0 and less than max value of int64 i.e. 9223372036854775807
 */
void
ValidateElementForNGroupAccumulators(bson_value_t *elementsToFetch, const
									 char *opName)
{
	switch (elementsToFetch->value_type)
	{
		case BSON_TYPE_INT32:
		case BSON_TYPE_INT64:
		case BSON_TYPE_DOUBLE:
		case BSON_TYPE_DECIMAL128:
		{
			if (IsBsonValueNaN(elementsToFetch))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION31109),
								errmsg(
									"Unable to convert out-of-range value %s into a long type",
									BsonValueToJsonForLogging(elementsToFetch))));
			}

			if (IsBsonValueInfinity(elementsToFetch) != 0)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION31109),
								errmsg(
									"Unable to convert out-of-range value %s into a long type",
									BsonValueToJsonForLogging(elementsToFetch))));
			}

			if (!IsBsonValueFixedInteger(elementsToFetch))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5787903),
								errmsg(
									"Expected 'integer' type for 'n' but found '%s' for value '%s'",
									BsonTypeName(elementsToFetch->value_type),
									BsonValueToJsonForLogging(elementsToFetch)),
								errdetail_log(
									"Expected 'integer' type for 'n' but found '%s' for value '%s'",
									BsonTypeName(elementsToFetch->value_type),
									BsonValueToJsonForLogging(elementsToFetch))));
			}

			/* This is done as elements to fetch must only be int64. */
			bool throwIfFailed = true;
			elementsToFetch->value.v_int64 = BsonValueAsInt64WithRoundingMode(
				elementsToFetch, ConversionRoundingMode_Floor, throwIfFailed);
			elementsToFetch->value_type = BSON_TYPE_INT64;

			if (elementsToFetch->value.v_int64 <= 0)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5787908),
								errmsg(
									"The value of 'n' must be strictly greater than 0, but the current value is %s",
									BsonValueToJsonForLogging(elementsToFetch)),
								errdetail_log(
									"'n' cannot be less than 0, but the current value is %ld",
									elementsToFetch->value.v_int64)));
			}
			if (elementsToFetch->value.v_int64 > 10)
			{
				if (strncmp(opName, "$lastN", 6) == 0)
				{
					ReportFeatureUsage(FEATURE_STAGE_GROUP_ACC_LASTN_GT10);
				}
				else if ((strncmp(opName, "$firstN", 7) == 0))
				{
					ReportFeatureUsage(FEATURE_STAGE_GROUP_ACC_FIRSTN_GT10);
				}
			}
			break;
		}

		default:
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5787902),
							errmsg(
								"Expected 'integer' type for 'n' but found '%s' for value '%s'",
								BsonTypeName(elementsToFetch->value_type),
								BsonValueToJsonForLogging(elementsToFetch)),
							errdetail_log(
								"Expected 'integer' type for 'n' but found '%s' for value '%s'",
								BsonTypeName(elementsToFetch->value_type),
								BsonValueToJsonForLogging(elementsToFetch))));
		}
	}
}


/*
 * Given valid aggregation pipeline stages, this function returns the stage enum at the given position.
 */
Stage
GetAggregationStageAtPosition(const List *aggregationStages, int position)
{
	if (list_length(aggregationStages) <= position)
	{
		return Stage_Invalid;
	}

	AggregationStage *stage = list_nth(aggregationStages, position);
	return stage->stageDefinition->stageEnum;
}


/*
 * Mutates the query for the $addFields stage
 */
static Query *
HandleAddFields(const bson_value_t *existingValue, Query *query,
				AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_ADD_FIELDS);

	return HandleSimpleProjectionStage(existingValue, query, context, "$addFields",
									   BsonDollarAddFieldsFunctionOid(),
									   BsonDollarAddFieldsWithLetFunctionOid,
									   BsonDollarAddFieldsWithLetAndCollationFunctionOid);
}


/*
 * Handles the $bucket stage.
 * Converts to a $group stage with $_bucketInternal operator to handle bucket specific logics.
 */
static Query *
HandleBucket(const bson_value_t *existingValue, Query *query,
			 AggregationPipelineBuildContext *context)
{
	if (IsCollationApplicable(context->collationString))
	{
		ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						errmsg(
							"Collation is currently unsupported in the $bucket stage.")));
	}

	ReportFeatureUsage(FEATURE_STAGE_BUCKET);

	bson_value_t groupSpec = { 0 };
	RewriteBucketGroupSpec(existingValue, &groupSpec);
	query = HandleGroup(&groupSpec, query, context);

	return query;
}


/**
 * Processes the $fill Pipeine stage.
 * $fill will be rewritten to $setWindowFields and $addFields. $setWindowFields will be used to process `linear` and `locf`;
 * $addFields will be used to process const based fill. The rewritten query will be divided into two cases:
 *
 * 1) If method-fill is specified in output, the query will be rewritten with $setWindowFields.
 * 2) If only value-fill is specified in output, the query will be rewritten with $addFields, $sort.
 *
 * Example 1:
 *
 *   $fill: {
 *       partitionBy: { part : "$part"},
 *       sortBy: { key: 1 },
 *       output: {
 *           field1 : { method: "linear" },
 *           field2 : { method: "locf" },
 *           field3 : { value: "<expr>"}
 *       }
 *   }
 *
 *   ==>
 *
 *   [
 *		{
 *			$setWindowFields: {
 *				partitionBy: { part : "$part"},
 *				sortBy: { key: 1 },
 *				output: {
 *					field1: {
 *						$linearFill: "$field1"
 *					},
 *					field2: {
 *						$locf: "$field2"
 *					}
 *					field3: {
 *						$_internal_constFill: {
 *							path: "$field3",
 *							value: "<expr>"
 *						}
 *					}
 *				}
 *			}
 *		},
 *	]
 *
 * Example 2:
 *
 *  $fill: {
 *		partitionBy: { part : "$part" },
 *	    sortBy: { key : 1},
 *		output: {
 *			field1 : { value: "<expr>"}
 *		}
 *	}
 *
 *	==>
 *  [
 *      { $sort: { key : 1}},
 *      { $addFields: { field1: { $ifNull: ["$field1", "<expr>"]}}},
 *  ]
 *
 */
static Query *
HandleFill(const bson_value_t *existingValue, Query *query,
		   AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_FILL);

	if (IsCollationApplicable(context->collationString))
	{
		ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						errmsg(
							"collation is not supported in the $fill stage yet.")));
	}

	bool hasSortBy = false;
	bool onlyHasValueFill = true;
	bool enableInternalWindowOperator = true;
	bson_value_t sortSpec = { 0 };
	bson_value_t addFieldsForValueFill = { 0 };

	bson_value_t setWindowFieldsSpec = { 0 };
	bson_value_t partitionByFields = { 0 };

	RewriteFillToSetWindowFieldsSpec(existingValue,
									 &hasSortBy,
									 &onlyHasValueFill,
									 &sortSpec,
									 &addFieldsForValueFill,
									 &setWindowFieldsSpec,
									 &partitionByFields);

	/* if only const based fill exists, we rewrite it into $addFields directly */
	if (onlyHasValueFill)
	{
		if (hasSortBy)
		{
			query = HandleSort(&sortSpec, query, context);
		}
		query = HandleAddFields(&addFieldsForValueFill, query, context);
		return query;
	}

	/*
	 * partitionByFields is the $fill-specific multi-field partition key. The
	 * partition expression is built inside HandleSetWindowFieldsCore (after any
	 * migration to a subquery) so that the embedded document Var references the
	 * correct range-table level.
	 */
	const bson_value_t *partitionByFieldsArg =
		partitionByFields.value_type != BSON_TYPE_EOD ? &partitionByFields : NULL;

	query = HandleSetWindowFieldsCore(&setWindowFieldsSpec, query, context,
									  partitionByFieldsArg,
									  enableInternalWindowOperator);

	return query;
}


static void
RewriteFillToSetWindowFieldsSpec(const bson_value_t *fillSpec,
								 bool *hasSortBy,
								 bool *onlyHasValueFill,
								 bson_value_t *sortSpec,
								 bson_value_t *addFieldsForValueFill,
								 bson_value_t *setWindowFieldsSpec,
								 bson_value_t *partitionByFields)
{
	if (fillSpec->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40201),
						errmsg(
							"The operator $fill requires an object as its argument, but a value of type %s was provided instead.",
							BsonTypeName(fillSpec->value_type)),
						errdetail_log(
							"The operator $fill requires an object as its argument, but a value of type %s was provided instead.",
							BsonTypeName(fillSpec->value_type))));
	}


	bson_iter_t fillIter;
	BsonValueInitIterator(fillSpec, &fillIter);
	bson_value_t partitionBy = { 0 };
	bson_value_t sortBy = { 0 };
	bson_value_t output = { 0 };

	bool hasPartitionBy = false;
	bool hasPartitionByFields = false;

	while (bson_iter_next(&fillIter))
	{
		const char *key = bson_iter_key(&fillIter);
		const bson_value_t *value = bson_iter_value(&fillIter);

		if (strcmp(key, "partitionBy") == 0)
		{
			partitionBy = *value;
			hasPartitionBy = true;
		}
		else if (strcmp(key, "partitionByFields") == 0)
		{
			*partitionByFields = *value;
			hasPartitionByFields = true;
		}
		else if (strcmp(key, "sortBy") == 0)
		{
			sortBy = *value;
			*hasSortBy = true;
		}
		else if (strcmp(key, "output") == 0)
		{
			output = *value;
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_UNKNOWNBSONFIELD),
							errmsg(
								"The BSON field named '$fill.%s' is not recognized as a valid field.",
								key),
							errdetail_log(
								"The BSON field named '$fill.%s' is not recognized as a valid field.",
								key)));
		}
	}

	if (hasPartitionBy && hasPartitionByFields)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION6050204),
						errmsg(
							"Only one of 'partitionBy' or 'partitionByFields' is allowed when using the '$fill'."),
						errdetail_log(
							"Only one of 'partitionBy' or 'partitionByFields' is allowed when using the '$fill'.")));
	}

	/* output is a required field in $fill, check it */

	/* Required fields check */
	if (output.value_type == BSON_TYPE_EOD)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40414),
						errmsg(
							"Required field '$fill.output' is missing")));
	}


	/* validate partitionByFields spec */
	if (hasPartitionByFields)
	{
		bson_iter_t partitionByFieldsIter;
		BsonValueInitIterator(partitionByFields, &partitionByFieldsIter);

		while (bson_iter_next(&partitionByFieldsIter))
		{
			const bson_value_t *fieldBson = bson_iter_value(&partitionByFieldsIter);
			EnsureTopLevelFieldValueType("partitionByFields", fieldBson, BSON_TYPE_UTF8);
			EnsureStringValueNotDollarPrefixed(fieldBson->value.v_utf8.str,
											   fieldBson->value.v_utf8.len);
		}
	}

	pgbson_writer swfWriter;
	PgbsonWriterInit(&swfWriter);

	if (partitionBy.value_type != BSON_TYPE_EOD)
	{
		PgbsonWriterAppendValue(&swfWriter, "partitionBy", 11, &partitionBy);
	}
	if (sortBy.value_type != BSON_TYPE_EOD)
	{
		PgbsonWriterAppendValue(&swfWriter, "sortBy", 6, &sortBy);
	}

	pgbson_writer addFieldsSpecWriter;
	PgbsonWriterInit(&addFieldsSpecWriter);

	pgbson_writer outputWriter;
	PgbsonWriterStartDocument(&swfWriter, "output", 6, &outputWriter);

	bson_iter_t outputIter;
	BsonValueInitIterator(&output, &outputIter);
	while (bson_iter_next(&outputIter))
	{
		const char *fieldName = bson_iter_key(&outputIter);

		StringInfo dollarFieldName = makeStringInfo();
		appendStringInfo(dollarFieldName, "$%s", fieldName);

		const bson_value_t *fieldSpec = bson_iter_value(&outputIter);

		bson_iter_t fieldSpecIter;
		BsonValueInitIterator(fieldSpec, &fieldSpecIter);
		while (bson_iter_next(&fieldSpecIter))
		{
			const char *key = bson_iter_key(&fieldSpecIter);
			const bson_value_t *expr = bson_iter_value(&fieldSpecIter);

			pgbson_writer outputFieldSpecWriter;
			PgbsonWriterStartDocument(&outputWriter, fieldName, strlen(fieldName),
									  &outputFieldSpecWriter);
			if (strcmp(key, "method") == 0)
			{
				*onlyHasValueFill = false;
				EnsureTopLevelFieldValueType("method", expr, BSON_TYPE_UTF8);
				if (strcmp(expr->value.v_utf8.str, "linear") == 0)
				{
					PgbsonWriterAppendUtf8(&outputFieldSpecWriter, "$linearFill", 11,
										   dollarFieldName->data);
				}
				else if (strcmp(expr->value.v_utf8.str, "locf") == 0)
				{
					PgbsonWriterAppendUtf8(&outputFieldSpecWriter, "$locf", 5,
										   dollarFieldName->data);
				}
				else
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION6050202),
									errmsg(
										"The method must be specified as either locf or linear"),
									errdetail_log(
										"The method must be specified as either locf or linear")));
				}
			}
			else if (strcmp(key, "value") == 0)
			{
				/**
				 * convert into output of $setWindowFields.
				 * We will do this in current iteration to avoid multiple iterations, even if it may not be used if only value fill exists.
				 */
				pgbson_writer constFillSpecWriter;
				PgbsonWriterStartDocument(&outputFieldSpecWriter, "$_internal_constFill",
										  20, &constFillSpecWriter);
				PgbsonWriterAppendUtf8(&constFillSpecWriter, "path", 4,
									   dollarFieldName->data);
				PgbsonWriterAppendValue(&constFillSpecWriter, "value", 5, expr);
				PgbsonWriterEndDocument(&outputFieldSpecWriter, &constFillSpecWriter);

				/**
				 * convert value fill to $addFields.
				 * We will do this in current iteration to avoid multiple iterations, even if it may not be used if method fill exists as well.
				 */
				pgbson_writer addFieldItemWriter;
				PgbsonWriterStartDocument(&addFieldsSpecWriter, fieldName, strlen(
											  fieldName), &addFieldItemWriter);

				pgbson_array_writer ifNullWriter;
				PgbsonWriterStartArray(&addFieldItemWriter, "$ifNull", 7, &ifNullWriter);
				PgbsonArrayWriterWriteUtf8(&ifNullWriter, dollarFieldName->data);
				PgbsonArrayWriterWriteValue(&ifNullWriter, expr);
				PgbsonWriterEndArray(&addFieldItemWriter, &ifNullWriter);

				PgbsonWriterEndDocument(&addFieldsSpecWriter, &addFieldItemWriter);
			}
			else
			{
				/* Field name is not supported */
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_UNKNOWNBSONFIELD),
								errmsg(
									"The BSON field named '$fill.%s' is not recognized as a valid field.",
									key),
								errdetail_log(
									"The BSON field named '$fill.%s' is not recognized as a valid field.",
									key)));
			}
			PgbsonWriterEndDocument(&outputWriter, &outputFieldSpecWriter);
		}
		pfree(dollarFieldName->data);
	}

	/**
	 * construct $sort spec for sortBy if only value fill exists.
	 *
	 * Example:
	 *
	 *  $fill: {
	 *		sortBy: { key1 : 1, key2 : 1 },
	 *	}
	 *
	 *	==>
	 *
	 *  "$sort": {"sortKey": { "key1": 1, "key2": 1 }}
	 */

	if (*onlyHasValueFill && *hasSortBy)
	{
		pgbson_writer sortSpecWriter;
		PgbsonWriterInit(&sortSpecWriter);

		/* iterator each field in sortBy and write to sortKey */
		bson_iter_t sortByIter;
		BsonValueInitIterator(&sortBy, &sortByIter);
		while (bson_iter_next(&sortByIter))
		{
			const char *fieldName = bson_iter_key(&sortByIter);
			const bson_value_t *fieldValue = bson_iter_value(&sortByIter);
			PgbsonWriterAppendValue(&sortSpecWriter, fieldName, strlen(fieldName),
									fieldValue);
		}
		*sortSpec = ConvertPgbsonToBsonValue(PgbsonWriterGetPgbson(&sortSpecWriter));
	}

	PgbsonWriterEndDocument(&swfWriter, &outputWriter);

	*setWindowFieldsSpec = ConvertPgbsonToBsonValue(PgbsonWriterGetPgbson(&swfWriter));
	*addFieldsForValueFill = ConvertPgbsonToBsonValue(PgbsonWriterGetPgbson(
														  &addFieldsSpecWriter));
}


/*
 * Mutates the query for the $project stage
 */
static Query *
HandleProject(const bson_value_t *existingValue, Query *query,
			  AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_PROJECT);
	return HandleSimpleProjectionStage(existingValue, query, context, "$project",
									   BsonDollarProjectFunctionOid(),
									   BsonDollarProjectWithLetFunctionOid,
									   BsonDollarProjectWithLetAndCollationFunctionOid);
}


/*
 * Mutates the query for the $redact stage
 */
static Query *
HandleRedact(const bson_value_t *existingValue, Query *query,
			 AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_REDACT);

	Const *redactSpec;
	Const *redactSpecText;

	if (existingValue->value_type == BSON_TYPE_DOCUMENT)
	{
		redactSpec = MakeBsonConst(PgbsonInitFromDocumentBsonValue(existingValue));
		redactSpecText = MakeTextConst("", 0);
	}
	else if (existingValue->value_type == BSON_TYPE_UTF8)
	{
		redactSpec = MakeBsonConst(PgbsonInitEmpty());
		redactSpecText = MakeTextConst(
			existingValue->value.v_utf8.str, existingValue->value.v_utf8.len);
	}
	else
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION17053),
						errmsg(
							"$redact's parameter must be an expression or string valued as $$KEEP, $$DESCEND, and $$PRUNE, but input as '%s'.",
							BsonValueToJsonForLogging(existingValue)),
						errdetail_log(
							"$redact's parameter must be an expression or string valued as $$KEEP, $$DESCEND, and $$PRUNE.")));
	}

	/* The first projector is the document */
	TargetEntry *firstEntry = linitial(query->targetList);
	Expr *currentProjection = firstEntry->expr;

	/*
	 * There is two kind of $redact parameters:
	 * a. like "$redact: { $cond: { if: { $eq: ["$stuff", "valid"] }, then: "$$KEEP", else: "$$PRUNE" }"
	 * existingValue->value_type is BSON_TYPE_DOCUMENT.
	 * BsonDollarRedactWithLetFunctionOid() takes four parameters, currentProjection, redactSpec, redactSpecText, variableSpec.
	 * redactSpec is the document and redactSpecText is empty
	 * b. like "$redact: "$$PRUNE"
	 * existingValue->value.value_type is BSON_TYPE_UTF8.
	 * In this case, BsonDollarRedactWithLetFunctionOid() takes four parameters, currentProjection, redactSpec, redactSpecText, variableSpec.
	 * redactSpec is set to empty document and redactSpecText is the string.
	 *
	 * If context->collationString is valid, we use BsonDollarRedactWithLetAndCollationFunctionOid() instead.
	 * BsonDollarRedactWithLetAndCollationFunctionOid() takes five parameters, currentProjection, redactSpec, redactSpecText, variableSpec,
	 * collationString.
	 */

	List *args = NIL;
	Oid funcOid = BsonDollarRedactWithLetFunctionOid();

	Expr *variableSpecConst = context->variableSpec == NULL ?
							  (Expr *) makeNullConst(BsonTypeId(), -1, InvalidOid) :
							  context->variableSpec;

	if (IsCollationApplicable(context->collationString))
	{
		Const *collationStringConst = MakeTextConst(context->collationString, strlen(
														context->collationString));
		args = list_make5(currentProjection, redactSpec, redactSpecText,
						  variableSpecConst, collationStringConst);
		funcOid = BsonDollarRedactWithLetAndCollationFunctionOid();
	}
	else
	{
		args = list_make4(currentProjection, redactSpec, redactSpecText,
						  variableSpecConst);
	}

	FuncExpr *resultExpr = makeFuncExpr(
		funcOid, BsonTypeId(), args, InvalidOid,
		InvalidOid, COERCE_EXPLICIT_CALL);

	firstEntry->expr = (Expr *) resultExpr;

	/* The following code addresses nested redact handling.
	 * The behavior of $lookup and $facet with nested redact differs from $unionWith:
	 * $lookup and $facet result in unexpected null values, use COALESCE to return an empty value instead.
	 * For $unionWith, it will still return null as redact does.
	 */
	if (context->nestedPipelineLevel > 0 && context->parentStageName !=
		ParentStageName_UNIONWITH)
	{
		CoalesceExpr *coalesceExpr = makeNode(CoalesceExpr);
		coalesceExpr->args = list_make2(resultExpr, MakeBsonConst(PgbsonInitEmpty()));
		coalesceExpr->coalescetype = BsonTypeId();
		coalesceExpr->coalescecollid = InvalidOid;

		firstEntry->expr = (Expr *) coalesceExpr;
	}

	return query;
}


static Query *
HandleProjectFind(const bson_value_t *existingValue, const bson_value_t *queryValue,
				  Query *query,
				  AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_PROJECT_FIND);
	EnsureTopLevelFieldValueType("projection", existingValue, BSON_TYPE_DOCUMENT);

	if (IsBsonValueEmptyDocument(existingValue))
	{
		return query;
	}

	/* The first projector is the document */
	TargetEntry *firstEntry = linitial(query->targetList);

	Expr *currentProjection = firstEntry->expr;
	pgbson *docBson = PgbsonInitFromBuffer(
		(char *) existingValue->value.v_doc.data,
		existingValue->value.v_doc.data_len);
	Const *projectProcessed = MakeBsonConst(docBson);

	List *args;
	Oid funcOid = BsonDollarProjectFindFunctionOid();

	Const *collationConst = IsCollationApplicable(context->collationString) ?
							MakeTextConst(context->collationString,
										  strlen(context->collationString)) : NULL;

	if (collationConst)
	{
		pgbson *queryDoc = queryValue->value_type == BSON_TYPE_EOD ? PgbsonInitEmpty() :
						   PgbsonInitFromDocumentBsonValue(queryValue);

		args = list_make5(currentProjection,
						  projectProcessed,
						  MakeBsonConst(queryDoc),
						  context->variableSpec ? context->variableSpec :
						  (Expr *) MakeBsonConst(PgbsonInitEmpty()),
						  collationConst);
		funcOid = BsonDollarProjectFindWithLetAndCollationFunctionOid();
	}
	else if (context->variableSpec)
	{
		pgbson *queryDoc = queryValue->value_type == BSON_TYPE_EOD ? PgbsonInitEmpty() :
						   PgbsonInitFromDocumentBsonValue(queryValue);
		args = list_make4(currentProjection, projectProcessed, MakeBsonConst(queryDoc),
						  context->variableSpec);
		funcOid = BsonDollarProjectFindWithLetFunctionOid();
	}
	else if (queryValue->value_type == BSON_TYPE_EOD)
	{
		args = list_make2(currentProjection, projectProcessed);
	}
	else
	{
		pgbson *queryDoc = PgbsonInitFromDocumentBsonValue(queryValue);
		Const *queryDocProcessed = MakeBsonConst(queryDoc);
		args = list_make3(currentProjection, projectProcessed, queryDocProcessed);
	}

	FuncExpr *resultExpr = makeFuncExpr(funcOid, BsonTypeId(), args, InvalidOid,
										InvalidOid, COERCE_EXPLICIT_CALL);

	firstEntry->expr = (Expr *) resultExpr;
	return query;
}


/*
 * Mutates the query for the $replaceRoot stage
 */
static Query *
HandleReplaceRoot(const bson_value_t *existingValue, Query *query,
				  AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_REPLACE_ROOT);

	Oid (*replaceRootWithLetAndCollationFuncOid)(void) =
		BsonDollarReplaceRootWithLetAndCollationFunctionOid;

	return HandleSimpleProjectionStage(existingValue, query, context, "$replaceRoot",
									   BsonDollarReplaceRootFunctionOid(),
									   &BsonDollarReplaceRootWithLetFunctionOid,
									   replaceRootWithLetAndCollationFuncOid);
}


/*
 * Mutates the query for the $skip stage.
 * If there is a limit, then injects a new subquery and sets the skip
 * since Skip is processed before limit in PG.
 */
static Query *
HandleSkip(const bson_value_t *existingValue, Query *query,
		   AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_SKIP);
	if (!BsonValueIsNumber(existingValue))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5107200),
						errmsg(
							"Invalid argument to $skip stage: Expected a non-negative integer argument")));
	}

	bool checkFixedInteger = true;
	if (!IsBsonValueUnquantized64BitInteger(existingValue, checkFixedInteger))
	{
		double doubleValue = BsonValueAsDouble(existingValue);
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5107200),
						errmsg(
							"Invalid parameter provided to $skip stage: value cannot be expressed as a 64-bit integer $skip: %f",
							doubleValue)));
	}

	int64_t skipValue = BsonValueAsInt64(existingValue);
	if (skipValue < 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5107200),
						errmsg(
							"Invalid argument provided to $skip stage: A non-negative numerical value was expected in $skip, but received %ld.",
							skipValue)));
	}

	if (skipValue == 0)
	{
		/* Skip 0 can be ignored */
		return query;
	}

	if (query->limitCount != NULL)
	{
		/* Because skip is evaluated first, an existing limit implies a new stage */
		query = MigrateQueryToSubQuery(query, context);
	}

	if (query->limitOffset != NULL)
	{
		Assert(IsA(query->limitOffset, Const));
		Const *limitOffset = (Const *) query->limitOffset;

		/* We already have a skip: Sum up the skips */
		limitOffset->constvalue = Int64GetDatum(skipValue + DatumGetInt64(
													limitOffset->constvalue));
	}
	else
	{
		query->limitOffset = (Node *) makeConst(INT8OID, -1, InvalidOid, sizeof(int64_t),
												Int64GetDatum(skipValue), false, true);
	}

	/* Postgres applies OFFSET after other layers. Protocol behavior applies it first. to emulate this we need to
	 * Push down a subquery.
	 */
	context->requiresSubQuery = true;

	return query;
}


/*
 * Mutates the query for the $limit stage
 * Simply updates the limit in the current query.
 */
static Query *
HandleLimit(const bson_value_t *existingValue, Query *query,
			AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_LIMIT);
	if (!BsonValueIsNumber(existingValue))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5107201),
						errmsg("the limit must be specified as a number")));
	}

	bool checkFixedInteger = true;
	if (!IsBsonValue64BitInteger(existingValue, checkFixedInteger))
	{
		double doubleValue = BsonValueAsDouble(existingValue);
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5107201),
						errmsg(
							"Invalid $limit stage argument: value cannot be represented as a 64-bit integer: $limit: %f",
							doubleValue)));
	}

	int64_t limitValue = BsonValueAsInt64(existingValue);
	if (limitValue < 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION5107201),
						errmsg(
							"Invalid argument passed to the $skip stage: a non-negative number was expected but received in $limit: %ld",
							limitValue)));
	}

	if (limitValue == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15958),
						errmsg("The specified limit value must always be positive")));
	}

	if (query->limitCount != NULL)
	{
		Assert(IsA(query->limitCount, Const));
		Const *limitConst = (Const *) query->limitCount;

		/* We just take the min of the two */
		limitConst->constvalue = Int64GetDatum(Min(limitValue, DatumGetInt64(
													   limitConst->constvalue)));
	}
	else
	{
		query->limitCount = (Node *) makeConst(INT8OID, -1, InvalidOid, sizeof(int64_t),
											   Int64GetDatum(limitValue), false, true);
	}

	/* PG applies projection before LIMIT - consequently if there was an error in the 11th
	 * document and you do limit 10, PG would error out.
	 * This is a nuance that requires a subquery.
	 */
	context->requiresSubQuery = true;

	return query;
}


/*
 * Mutates the query for the $match stage.
 * Simply calls the ExpandQueryOperator similar to the @@ operator.
 * Also injects a new stage if ther is a LIMIT or a SKIP since the
 * match needs to apply post-limit.
 * Applies the match on the current projector.
 */
Query *
HandleMatchWithIndexFilter(const bson_value_t *existingValue, Query *query,
						   AggregationPipelineBuildContext *context,
						   HTAB *requiredFilterPathNameHashSet)
{
	ReportFeatureUsage(FEATURE_STAGE_MATCH);
	if (existingValue->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15959),
						errmsg(
							"The match filter must always be provided as an expression within a object.")));
	}

	if (query->limitOffset != NULL || query->limitCount != NULL)
	{
		query = MigrateQueryToSubQuery(query, context);
	}

	TargetEntry *entry = linitial(query->targetList);

	if (context->joinStatus == JoinStageStatus_NoJoinsOrUnions &&
		EnableSubqueryPushdownForMatch &&
		EnableDynamicCursors &&
		IsA(entry->expr, FuncExpr))
	{
		/* If we're not doing a filter on a Var field and it's being
		 * processed by a function, then, it's better to push this query
		 * to a subquery such that the projection is applied first, and then
		 * the filter is applied. In the case of joins, we want the match to
		 * be pulled up to be able to participate in the joins
		 * To achieve this and ensure that subquery pullup works, we add an
		 * offset 0 (since offset 0) is skipped by the planner but not by
		 * the subquery optimizer, and push it to a subquery.
		 * TODO: Be more aggressive in the subquery pushdown (not just if
		 * there exists a $lookup somewhere in the pipeline).
		 */
		Assert(query->limitOffset == NULL);
		query->limitOffset = (Node *) makeConst(INT8OID, -1, InvalidOid, sizeof(int64_t),
												Int64GetDatum(0), false, true);
		query = MigrateQueryToSubQuery(query, context);
		entry = linitial(query->targetList);
	}

	BsonQueryOperatorContext filterContext = { 0 };
	filterContext.documentExpr = entry->expr;
	filterContext.inputType = MongoQueryOperatorInputType_Bson;
	filterContext.simplifyOperators = true;
	filterContext.coerceOperatorExprIfApplicable = true;
	filterContext.requiredFilterPathNameHashSet = requiredFilterPathNameHashSet;
	filterContext.variableContext = context->variableSpec;

	if (EnableCollation)
	{
		filterContext.collationString = context->collationString;
	}

	bson_iter_t queryDocIterator;
	BsonValueInitIterator(existingValue, &queryDocIterator);
	List *quals = CreateQualsFromQueryDocIterator(&queryDocIterator, &filterContext);

	UpdateQueryOperatorContextSortList(query, filterContext.sortClauses,
									   filterContext.targetEntries);

	quals = AddShardKeyAndIdFilters(existingValue, query, context, entry, quals);
	if (query->jointree->quals != NULL)
	{
		quals = lappend(quals, query->jointree->quals);
	}

	query->jointree->quals = (Node *) make_ands_explicit(quals);
	return query;
}


Query *
HandleMatch(const bson_value_t *existingValue, Query *query,
			AggregationPipelineBuildContext *context)
{
	return HandleMatchWithIndexFilter(existingValue, query, context, NULL);
}


/*
 * Builds a change stream aggregation query in the form of
 * SELECT document, continuation FROM changestream_aggregation(args);
 */
static Query *
BuildChangeStreamFunctionQuery(Oid queryFunctionOid, List *queryArgs, bool isMultiRow)
{
	Query *query = makeNode(Query);
	query->commandType = CMD_SELECT;
	query->querySource = QSRC_ORIGINAL;
	query->canSetTag = true;

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

	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;
	rte->coltypes = list_make2_oid(BsonTypeId(), BsonTypeId());
	rte->coltypmods = list_make2_int(-1, -1);
	rte->colcollations = list_make2_oid(InvalidOid, InvalidOid);
	rte->ctename = NULL;
	rte->ctelevelsup = 0;


	Param *param = makeNode(Param);
	param->paramid = 1;
	param->paramkind = PARAM_EXTERN;
	param->paramtype = BsonTypeId();
	param->paramtypmod = -1;

	queryArgs = lappend(queryArgs, param);

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

	RangeTblFunction *rangeTableFunction = makeNode(RangeTblFunction);
	rangeTableFunction->funccolcount = 2;
	rangeTableFunction->funccolnames = colNames;
	rangeTableFunction->funccoltypes = list_make2_oid(BsonTypeId(), BsonTypeId());
	rangeTableFunction->funccoltypmods = list_make2_int(-1, -1);
	rangeTableFunction->funccolcollations = list_make2_oid(InvalidOid, 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);
	Var *continuationEntry = makeVar(1, 2, BsonTypeId(), -1, InvalidOid, 0);
	TargetEntry *documentTargetEntry = makeTargetEntry((Expr *) documentEntry, 1,
													   "document",
													   false);
	TargetEntry *continuationTargetEntry = makeTargetEntry((Expr *) continuationEntry, 2,
														   "continuation",
														   false);
	query->targetList = list_make2(documentTargetEntry, continuationTargetEntry);
	return query;
}


/*
 * Checks if the given string is in the given array of strings.
 */
static bool
StringArrayContains(const char *array[], size_t arraySize, const char *value)
{
	for (size_t i = 0; i < arraySize; i++)
	{
		if (strcmp(array[i], value) == 0)
		{
			return true;
		}
	}
	return false;
}


/*
 * Pre-checks the $changeStream pipeline stages to ensure that only supported stages are added to
 * the $changestream pipeline, also ensures that $changeStream is the first stage in the pipeline.
 * This function is called before the pipeline is mutated. It also checks if the feature is enabled.
 */
static void
PreCheckChangeStreamPipelineStages(const bson_value_t *pipelineValue,
								   const AggregationPipelineBuildContext *context)
{
	bson_iter_t pipelineIterator;
	BsonValueInitIterator(pipelineValue, &pipelineIterator);
	int stageNum = 0;
	while (bson_iter_next(&pipelineIterator))
	{
		bson_iter_t documentIterator;

		/* Any errors here will be handled in MutateQueryWithPipeline*/
		if (!BSON_ITER_HOLDS_DOCUMENT(&pipelineIterator) ||
			!bson_iter_recurse(&pipelineIterator, &documentIterator))
		{
			continue;
		}

		/* Any errors here will be handled in MutateQueryWithPipeline*/
		pgbsonelement stageElement;
		if (!TryGetSinglePgbsonElementFromBsonIterator(&documentIterator, &stageElement))
		{
			continue;
		}

		const char *stageName = stageElement.path;

		/* The first change should be $changeStream. */
		if (stageNum == 0 && strcmp(stageName, "$changeStream") == 0)
		{
			continue;
		}
		/* Check the next stages to be one of the allowed stages. */
		else if (!StringArrayContains(CompatibleChangeStreamPipelineStages,
									  COMPATIBLE_CHANGE_STREAM_STAGES_COUNT,
									  stageName))
		{
			/*
			 * 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.
			 */
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_ILLEGALOPERATION),
							errmsg(
								"Stage %s is not permitted in a $changeStream pipeline",
								stageName)));
		}
		stageNum++;
	}
}


/*
 * Modifies the query to handle the $changeStream stage.
 * It forma a query that calls the $changeStream function.
 * SELECT document, continuation
 * FROM changestream_aggregation(args);
 */
Query *
HandleChangeStream(const bson_value_t *existingValue, Query *query,
				   AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_CHANGE_STREAM);

	/* Check if change stream feature is available enabled by GUC. */
	if (!IsChangeStreamFeatureAvailableAndCompatible())
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_COMMANDNOTSUPPORTED),
						errmsg(
							"Stage $changeStream is not supported yet in native pipeline"),
						errdetail_log(
							"Stage $changeStream is not supported yet in native pipeline")));
	}

	EnsureTopLevelFieldValueType("$changeStream", existingValue, BSON_TYPE_DOCUMENT);

	if (context->mongoCollection != NULL &&
		!StringViewEqualsCString(&context->collectionNameView,
								 context->mongoCollection->name.collectionName))
	{
		/* This represents a view */
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_COMMANDNOTSUPPORTEDONVIEW),
						errmsg(
							"$changeStream cannot be used on views.")));
	}

	/*Check the first stage and make sure it is $changestream. */
	if (context->stageNum != 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40602),
						errmsg(
							"$changeStream can only be used as the initial stage in the pipeline.")));
	}

	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 *changeStreamArgs =
		list_make3(databaseConst, collectionConst, MakeBsonConst(bson));

	query = BuildChangeStreamFunctionQuery(ApiChangeStreamAggregationFunctionOid(),
										   changeStreamArgs, true);

	/* $changeStream pipeline requires a tailable cursor. */
	context->requiresTailableCursor = true;
	return query;
}


/*
 * Generates the projection functions expression for grouping stages e.g. $group, $setWindowFields
 * where the maximum number of arguments can be larger than MaxEvenFunctionArguments.
 */
Expr *
GenerateMultiExpressionRepathExpression(List *repathArgs, bool overrideArrayInProjection)
{
	Assert(repathArgs != NIL && list_length(repathArgs) % 2 == 0);

	CHECK_FOR_INTERRUPTS();
	check_stack_depth();

	int numOfArguments = list_length(repathArgs);
	if (numOfArguments <= MaxEvenFunctionArguments)
	{
		return (Expr *) makeFuncExpr(BsonRepathAndBuildFunctionOid(),
									 BsonTypeId(), repathArgs, InvalidOid,
									 InvalidOid, COERCE_EXPLICIT_CALL);
	}

	/*
	 * need to create multiple bson_merge_documents(bson_repath_and_build(...), bson_repath_and_build(...)) expressions
	 * for each MaxEvenFunctionArguments number of arguments.
	 * e.g. If 220 arguments are provided then 110 group by accumulators are present in the query.
	 * which translates to:
	 * bson_dollar_merge_documents(
	 *      bson_dollar_merge_documents(
	 *          bson_repath_and_build(<50 group by exprs>),
	 *          bson_dollar_merge_documents(
	 *              bson_repath_and_build(<50 group by exprs>),
	 *              bson_repath_and_build(<10 group by exprs>)
	 *      ),
	 * )
	 */
	List *args = list_copy_head(repathArgs, MaxEvenFunctionArguments);
	List *remainingArgs = list_copy_tail(repathArgs, MaxEvenFunctionArguments);
	Expr *argsRepathExpression = (Expr *) makeFuncExpr(BsonRepathAndBuildFunctionOid(),
													   BsonTypeId(), args, InvalidOid,
													   InvalidOid, COERCE_EXPLICIT_CALL);
	Expr *remainingArgsExprs = GenerateMultiExpressionRepathExpression(remainingArgs,
																	   overrideArrayInProjection);

	return (Expr *) makeFuncExpr(BsonDollaMergeDocumentsFunctionOid(),
								 BsonTypeId(),
								 list_make3(argsRepathExpression, remainingArgsExprs,
											MakeBoolValueConst(
												overrideArrayInProjection)),
								 InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
}


static List *
AddShardKeyAndIdFilters(const bson_value_t *existingValue, Query *query,
						AggregationPipelineBuildContext *context,
						TargetEntry *entry, List *existingQuals)
{
	if (context->mongoCollection == NULL || !IsA(entry->expr, Var))
	{
		return existingQuals;
	}

	Var *var = (Var *) entry->expr;
	if (var->varlevelsup != 0)
	{
		return existingQuals;
	}

	RangeTblEntry *rtable = rt_fetch(var->varno, query->rtable);

	/* Verify still on base RTE */
	if (rtable->rtekind != RTE_RELATION ||
		rtable->relid != context->mongoCollection->relationId)
	{
		return existingQuals;
	}

	/* If we have a shardkey, add it */
	bool hasShardKeyFilters = false;
	if (context->mongoCollection->shardKey != NULL)
	{
		bool shardKeyFiltersCollationAware = false;
		Expr *shardKeyFilters =
			CreateShardKeyFiltersForQuery(existingValue,
										  context->mongoCollection->shardKey,
										  context->mongoCollection->collectionId,
										  var->varno,
										  &shardKeyFiltersCollationAware);
		if (shardKeyFilters != NULL)
		{
			/* 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 (!shardKeyFiltersCollationAware ||
				!IsCollationApplicable(context->collationString))
			{
				hasShardKeyFilters = true;
				existingQuals = lappend(existingQuals, shardKeyFilters);

				/* Track whether shard key targeting is equality or $in */
				bool isInFilter = IsA(shardKeyFilters, BoolExpr) &&
								  ((BoolExpr *) shardKeyFilters)->boolop == OR_EXPR;
				context->shardKeyFilterAppliedType = isInFilter ?
													 ShardKeyFilter_In :
													 ShardKeyFilter_Equality;
			}
		}
	}
	else
	{
		hasShardKeyFilters = true;
	}

	if (hasShardKeyFilters)
	{
		/* Protocol behavior 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 = false;
		Expr *idFilter = CreateIdFilterForQuery(existingQuals, var->varno,
												&isCollationAware, &isPointRead);

		if (idFilter != NULL &&
			!(isCollationAware && IsCollationApplicable(context->collationString)))
		{
			existingQuals = lappend(existingQuals, idFilter);
			context->isPointReadQuery = isPointRead;

			/* If we skipped writing the shard key on the base table, write it now */
			if (ShouldSkipShardKeyFilterOnBaseTable(context))
			{
				Expr *zeroShardKeyFilter = CreateNonShardedShardKeyValueFilter(
					var->varno,
					context->mongoCollection);
				existingQuals = lappend(existingQuals, zeroShardKeyFilter);
			}
		}
	}

	return existingQuals;
}


/*
 * Applies the $unset operator to the query.
 * Converts to a $project exclude.
 */
static Query *
HandleUnset(const bson_value_t *existingValue, Query *query,
			AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_UNSET);
	if (existingValue->value_type != BSON_TYPE_UTF8 &&
		existingValue->value_type != BSON_TYPE_ARRAY)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION31002),
						errmsg(
							"Expected 'string' or 'array' type for $unset but found '%s' type",
							BsonTypeName(existingValue->value_type))));
	}

	pgbson_writer writer;
	PgbsonWriterInit(&writer);

	if (existingValue->value_type == BSON_TYPE_UTF8)
	{
		if (existingValue->value.v_utf8.len == 0)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40352),
							errmsg("FieldPath cannot be created from an empty string")));
		}

		if (existingValue->value.v_utf8.str[0] == '$')
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION16410),
							errmsg(
								"FieldPath field names cannot begin with the operators symbol '$'.")));
		}

		/* Add it as an exclude path */
		PgbsonWriterAppendInt32(&writer, existingValue->value.v_utf8.str,
								existingValue->value.v_utf8.len, 0);
	}
	else
	{
		bson_iter_t valueIterator;
		BsonValueInitIterator(existingValue, &valueIterator);

		while (bson_iter_next(&valueIterator))
		{
			const bson_value_t *arrayValue = bson_iter_value(&valueIterator);
			if (arrayValue->value_type != BSON_TYPE_UTF8)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION31120),
								errmsg(
									"Expected 'string' or 'array' type for $unset but found '%s' type",
									BsonTypeName(arrayValue->value_type))));
			}

			if (arrayValue->value.v_utf8.len == 0)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40352),
								errmsg(
									"FieldPath cannot be created from an empty string")));
			}

			if (arrayValue->value.v_utf8.str[0] == '$')
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION16410),
								errmsg(
									"FieldPath field names cannot begin with the operators symbol '$'.")));
			}

			/* Add it as an exclude path */
			PgbsonWriterAppendInt32(&writer, arrayValue->value.v_utf8.str,
									arrayValue->value.v_utf8.len, 0);
		}
	}

	pgbson *excludeBson = PgbsonWriterGetPgbson(&writer);

	if (IsPgbsonEmptyDocument(excludeBson))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION31119),
						errmsg(
							"The $unset operator requires input as either a string or an array containing at least one specified field.")));
	}

	Const *addFieldsProcessed = MakeBsonConst(excludeBson);

	TargetEntry *firstEntry = linitial(query->targetList);
	Expr *currentProjection = firstEntry->expr;
	List *args = list_make2(currentProjection, addFieldsProcessed);
	FuncExpr *resultExpr = makeFuncExpr(
		BsonDollarProjectFunctionOid(), BsonTypeId(), args, InvalidOid,
		InvalidOid, COERCE_EXPLICIT_CALL);

	firstEntry->expr = (Expr *) resultExpr;
	return query;
}


/*
 * Error handler that raises the diagnostic reported by TryParseUnwindStage on
 * the throwing $unwind parse path.
 */
static void
ThrowUnwindParseError(int errCode, const char *errMessage, const char *errArg)
{
	ereport(ERROR, (errcode(errCode), errmsg(errMessage, errArg)));
}


/*
 * Parses & validates a $unwind stage spec. On failure invokes onError (when
 * non-NULL) with the mongo-compatible errcode/message and returns false; lets
 * predicate-style callers reject a spec without raising. Passing NULL skips
 * the error callback entirely, avoiding any errArg allocation on that path.
 * Pass ThrowUnwindParseError to raise on invalid input.
 */
bool
TryParseUnwindStage(const bson_value_t *unwindStageValue, UnwindArgs *args,
					UnwindParseErrorHandler onError)
{
	*args = (UnwindArgs) {
		0
	};

	switch (unwindStageValue->value_type)
	{
		case BSON_TYPE_UTF8:
		{
			args->pathValue = *unwindStageValue;
			break;
		}

		case BSON_TYPE_DOCUMENT:
		{
			args->hasOptions = true;
			bson_iter_t optionsDocIter;
			BsonValueInitIterator(unwindStageValue, &optionsDocIter);
			while (bson_iter_next(&optionsDocIter))
			{
				const char *key = bson_iter_key(&optionsDocIter);
				const bson_value_t *value = bson_iter_value(&optionsDocIter);
				if (strcmp(key, "path") == 0)
				{
					args->pathValue = *value;
				}
				else if (strcmp(key, "includeArrayIndex") == 0)
				{
					if (value->value_type != BSON_TYPE_UTF8)
					{
						if (onError != NULL)
						{
							onError(ERRCODE_DOCUMENTDB_LOCATION28810,
									"A non-empty string value was expected for the includeArrayIndex option in the $unwind stage.",
									NULL);
						}
						return false;
					}

					StringView includeArrayIndexView = (StringView) {
						.string = value->value.v_utf8.str,
						.length = value->value.v_utf8.len
					};

					if (includeArrayIndexView.length == 0)
					{
						if (onError != NULL)
						{
							onError(ERRCODE_DOCUMENTDB_LOCATION28810,
									"A non-empty string value was expected for the includeArrayIndex option in the $unwind stage.",
									NULL);
						}
						return false;
					}

					if (StringViewStartsWith(&includeArrayIndexView, '$'))
					{
						if (onError != NULL)
						{
							onError(ERRCODE_DOCUMENTDB_LOCATION28822,
									"The includeArrayIndex option used in the $unwind stage must not have a '$' operator at the beginning: %s",
									CreateStringFromStringView(
										&includeArrayIndexView));
						}
						return false;
					}

					args->includeArrayIndex = includeArrayIndexView;
				}
				else if (strcmp(key, "preserveNullAndEmptyArrays") == 0)
				{
					if (value->value_type != BSON_TYPE_BOOL)
					{
						if (onError != NULL)
						{
							onError(ERRCODE_DOCUMENTDB_LOCATION28809,
									"A boolean value was expected for the preserveNullAndEmptyArrays option used in the $unwind stage.",
									NULL);
						}
						return false;
					}

					args->preserveNullAndEmptyArrays = value->value.v_bool;
				}
				else
				{
					if (onError != NULL)
					{
						onError(ERRCODE_DOCUMENTDB_LOCATION28811,
								"Invalid option specified for $unwind stage", NULL);
					}
					return false;
				}
			}

			break;
		}

		default:
		{
			if (onError != NULL)
			{
				onError(ERRCODE_DOCUMENTDB_LOCATION15981,
						"A string or an object was expected as the specification for the $unwind stage, but instead received %s.",
						pstrdup(BsonTypeName(unwindStageValue->value_type)));
			}
			return false;
		}
	}

	if (args->pathValue.value_type == BSON_TYPE_EOD)
	{
		if (onError != NULL)
		{
			onError(ERRCODE_DOCUMENTDB_LOCATION28812,
					"No path provided for $unwind stage", NULL);
		}
		return false;
	}
	if (args->pathValue.value_type != BSON_TYPE_UTF8)
	{
		if (onError != NULL)
		{
			onError(ERRCODE_DOCUMENTDB_LOCATION28808,
					"A string value was expected as the path in the $unwind stage, but received %s.",
					pstrdup(BsonTypeName(args->pathValue.value_type)));
		}
		return false;
	}

	StringView pathView = {
		.string = args->pathValue.value.v_utf8.str,
		.length = args->pathValue.value.v_utf8.len
	};
	if (pathView.length == 0)
	{
		if (onError != NULL)
		{
			onError(ERRCODE_DOCUMENTDB_LOCATION28812,
					"No path provided for $unwind stage", NULL);
		}
		return false;
	}

	if (!StringViewStartsWith(&pathView, '$') || pathView.length == 1)
	{
		if (onError != NULL)
		{
			onError(ERRCODE_DOCUMENTDB_LOCATION28818,
					"The path option provided to the $unwind stage must start with the '$' symbol: %s",
					CreateStringFromStringView(&pathView));
		}
		return false;
	}

	if (pathView.string[1] == '$')
	{
		if (onError != NULL)
		{
			onError(ERRCODE_DOCUMENTDB_LOCATION16410,
					"FieldPath field names cannot begin with the symbol '$'.", NULL);
		}
		return false;
	}

	return true;
}


/*
 * Applies the Unwind operator to the query.
 * Requests a new subquery stage (since we process unwind as an SRF)
 * after the unwind to ensure subsequent stages can process it on a
 * per tuple basis.
 */
static Query *
HandleUnwind(const bson_value_t *existingValue, Query *query,
			 AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_UNWIND);

	UnwindArgs unwindArgs;
	TryParseUnwindStage(existingValue, &unwindArgs, ThrowUnwindParseError);

	FuncExpr *resultExpr;

	/* The first projector is the document */
	TargetEntry *firstEntry = linitial(query->targetList);
	Expr *currentProjection = firstEntry->expr;
	if (unwindArgs.hasOptions)
	{
		Const *unwindValue = MakeBsonConst(PgbsonInitFromDocumentBsonValue(
											   existingValue));
		List *args = list_make2(currentProjection, unwindValue);
		resultExpr = makeFuncExpr(
			BsonDollarUnwindWithOptionsFunctionOid(), BsonTypeId(), args, InvalidOid,
			InvalidOid, COERCE_EXPLICIT_CALL);
	}
	else
	{
		Const *unwindValue = MakeTextConst(existingValue->value.v_utf8.str,
										   existingValue->value.v_utf8.len);
		List *args = list_make2(currentProjection, unwindValue);
		resultExpr = makeFuncExpr(
			BsonDollarUnwindFunctionOid(), BsonTypeId(), args, InvalidOid,
			InvalidOid, COERCE_EXPLICIT_CALL);
	}

	resultExpr->funcretset = true;
	firstEntry->expr = (Expr *) resultExpr;
	query->hasTargetSRFs = true;

	/* Since this is an SRF, we need a child CTE if anyone comes after an unwind */
	context->requiresSubQuery = true;
	return query;
}


/*
 * Helper function to handle making an aggregate function projector with a single
 * argument. e.g. BSONMAX(document), BSONSUM(document)
 * borrows code from the pg parser (ParseFuncOrColumn in parse_func.c)
 */
static Aggref *
CreateSingleArgAggregate(Oid aggregateFunctionId, Expr *argument, ParseState *parseState)
{
	List *aggregateArgs = list_make1(argument);
	Aggref *aggref = makeNode(Aggref);
	aggref->aggfnoid = aggregateFunctionId;
	aggref->aggtype = BsonTypeId();
	aggref->aggtranstype = InvalidOid; /* set by planner later */
	aggref->aggdirectargs = NIL; /* count has no direct args */
	aggref->aggkind = AGGKIND_NORMAL; /* reset by planner */
	/* aggref->agglevelsup = 0 / * > 0 if agg belongs to outer query * / */
	aggref->aggsplit = AGGSPLIT_SIMPLE;
	aggref->aggno = -1;     /* planner will set aggno and aggtransno */
	aggref->aggtransno = -1;
	aggref->location = -1;
	aggref->aggargtypes = list_make1_oid(BsonTypeId());


	bool aggDistinct = false;
	transformAggregateCall(parseState, aggref, aggregateArgs, NIL, aggDistinct);
	return aggref;
}


static List *
AddFullScanQualsToJoinTreeForOrderByPushdown(List *currentQuals, Expr *documentExpr,
											 const pgbsonelement *sortSpecElement,
											 AggregationPipelineBuildContext *context,
											 bool *addedShardKeyFilter)
{
	Const *sortConst = AddCollationToSortSpec(sortSpecElement,
											  context->collationString);
	List *rangeArgs = list_make2(documentExpr, sortConst);
	Expr *fullScanExpr = (Expr *) makeFuncExpr(
		BsonFullScanFunctionOid(), BOOLOID, rangeArgs,
		InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
	currentQuals = lappend(currentQuals, fullScanExpr);

	if (sortSpecElement->pathLength == 3 && strncmp(sortSpecElement->path, "_id", 3) ==
		0 &&
		ShouldSkipShardKeyFilterOnBaseTable(context) && !*addedShardKeyFilter &&
		IsA(documentExpr, Var))
	{
		Var *documentVar = (Var *) documentExpr;

		/* If we are sorting on the _id for sort or group on an unsharded table,
		 * and we haven't added the zero shard key filter, add it now.
		 */
		Expr *zeroShardKeyFilter = CreateNonShardedShardKeyValueFilter(
			documentVar->varno,
			context->mongoCollection);
		currentQuals = lappend(currentQuals, zeroShardKeyFilter);
		*addedShardKeyFilter = true;
	}

	return currentQuals;
}


static void
AddFullScanQualsForOrderByPushdown(Query *query, Expr *documentExpr,
								   const pgbsonelement *sortSpecElement,
								   AggregationPipelineBuildContext *context)
{
	List *currentQuals = make_ands_implicit(
		(Expr *) query->jointree->quals);

	bool addedShardKeyFilter = false;
	currentQuals = AddFullScanQualsToJoinTreeForOrderByPushdown(currentQuals,
																documentExpr,
																sortSpecElement, context,
																&addedShardKeyFilter);
	query->jointree->quals = (Node *) make_ands_explicit(
		currentQuals);
}


static Query *
HandleDistinct(const StringView *distinctKey, Query *query,
			   AggregationPipelineBuildContext *context)
{
	FuncExpr *resultExpr;

	/* If there are existing sort clauses in the query, can happen with $near / $nearSphere query operators
	 * push the query down as a subquery.
	 */
	if (query->sortClause != NULL)
	{
		query = MigrateQueryToSubQuery(query, context);
	}

	/* The first projector is the document */
	TargetEntry *firstEntry = linitial(query->targetList);
	Expr *currentProjection = firstEntry->expr;
	Const *unwindValue = MakeTextConst(distinctKey->string,
									   distinctKey->length);
	List *args = list_make2(currentProjection, unwindValue);

	/* Create a distinct unwind - to expand arrays and such */
	resultExpr = makeFuncExpr(
		BsonDistinctUnwindFunctionOid(), BsonTypeId(), args, InvalidOid,
		InvalidOid, COERCE_EXPLICIT_CALL);
	resultExpr->funcretset = true;
	firstEntry->expr = (Expr *) resultExpr;
	query->hasTargetSRFs = true;

	/* Add the distinct */
	SortGroupClause *distinctSortGroup = makeNode(SortGroupClause);
	distinctSortGroup->eqop = BsonEqualOperatorId();
	distinctSortGroup->sortop = BsonLessThanOperatorId();
	distinctSortGroup->tleSortGroupRef = assignSortGroupRef(firstEntry,
															query->targetList);
	query->distinctClause = list_make1(distinctSortGroup);

	/* Add the bson_distinct_agg function */
	ParseState *parseState = make_parsestate(NULL);
	parseState->p_expr_kind = EXPR_KIND_SELECT_TARGET;
	parseState->p_next_resno = firstEntry->resno + 1;

	/* Add the full scan expr to be able to push the sort to an index */
	if (CanPushSortFilterToIndex(query, context) && EnableDistinctIndexPushdown)
	{
		pgbsonelement sortSpecElement = { 0 };
		sortSpecElement.path = distinctKey->string;
		sortSpecElement.pathLength = distinctKey->length;
		sortSpecElement.bsonValue.value_type = BSON_TYPE_INT32;
		sortSpecElement.bsonValue.value.v_int32 = 1;

		AddFullScanQualsForOrderByPushdown(query, currentProjection, &sortSpecElement,
										   context);
	}

	/*
	 * Append a distinct-exists filter (path >= MinKey once lowered) so that an
	 * ordered index on the distinct path can restrict the scan to documents
	 * that actually hold the path. The filter is trimmable when it is pushed to
	 * the index, and is otherwise evaluated at runtime. Gated on the cluster
	 * having the function installed (missingOk lookup returns InvalidOid on
	 * older clusters).
	 */
	if (EnableDistinctExistsFilterPushdown &&
		IsClusterVersionAtleast(DocDB_V0, 114, 1) &&
		OidIsValid(BsonDollarDistinctExistsFunctionOid()))
	{
		pgbson_writer existsWriter;
		PgbsonWriterInit(&existsWriter);
		PgbsonWriterAppendBool(&existsWriter, distinctKey->string,
							   distinctKey->length, true);
		Const *existsSpecConst =
			makeConst(BsonTypeId(), -1, InvalidOid, -1,
					  PointerGetDatum(PgbsonWriterGetPgbson(&existsWriter)),
					  false, false);
		List *existsArgs = list_make2(currentProjection, existsSpecConst);
		Expr *distinctExistsExpr = (Expr *) makeFuncExpr(
			BsonDollarDistinctExistsFunctionOid(), BOOLOID, existsArgs,
			InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
		List *existsQuals = make_ands_implicit(
			(Expr *) query->jointree->quals);
		existsQuals = lappend(existsQuals, distinctExistsExpr);
		query->jointree->quals = (Node *) make_ands_explicit(existsQuals);
	}

	query = MigrateQueryToSubQuery(query, context);
	firstEntry = linitial(query->targetList);
	Aggref *aggref = CreateSingleArgAggregate(BsonDistinctAggregateFunctionOid(),
											  firstEntry->expr, parseState);

	firstEntry->expr = (Expr *) aggref;
	query->hasAggs = true;
	return query;
}


static Query *
HandleGeoNear(const bson_value_t *existingValue, Query *query,
			  AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_GEONEAR);

	if (IsCollationApplicable(context->collationString))
	{
		ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						errmsg(
							"collation is not supported in the $geoNear stage yet.")));
	}

	if (context->stageNum != 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40603),
						errmsg(
							"$geoNear stage must be present before any other stage in the pipeline")));
	}


	RangeTblEntry *rte = linitial(query->rtable);
	if (rte->rtekind != RTE_RELATION)
	{
		ereport(ERROR, (
					errcode(ERRCODE_DOCUMENTDB_BADVALUE),
					errmsg("$geoNear can only be used with specific collections."),
					errdetail_log(
						"$geoNear can only be used with specific collections. RTE KIND: %d",
						rte->rtekind)));
	}

	if (existingValue->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (
					errcode(ERRCODE_DOCUMENTDB_LOCATION10065),
					errmsg(
						"Invalid parameter: an object was expected for $geoNear.")));
	}

	pgbson *geoNearQueryDoc = EvaluateGeoNearConstExpression(existingValue,
															 context->variableSpec);

	/*
	 * Create a $geoNear query of this form:
	 *
	 * For 2dsphere index:
	 *		SELECT bson_dollar_project_geonear(document, <geoNearSpec>) AS document
	 *		FROM <collection> WHERE bson_validate_geography(document, <geoNearSpec.key>) IS NOT NULL AND
	 *      [optional] document <|-|> <geoNearSpec> <= maxDistance AND document <|-|> <geoNearSpec> >= minDistance
	 *		ORDER BY bson_validate_geography(document, <geoNearSpec.key>) <|-|> <geoNearSpec>;
	 *
	 * For 2d index:
	 *      SELECT bson_dollar_project_geonear(document, <geoNearSpec>) AS document
	 *      FROM <collection> WHERE bson_validate_geometry(document, <geoNearSpec.key>) IS NOT NULL
	 *      [optional] document <|-|> <geoNearSpec> <= maxDistance AND document <|-|> <geoNearSpec> >= minDistance
	 *      ORDER BY bson_validate_geometry(document, <geoNearSpec.key>) <|-|> <geoNearSpec>;
	 *
	 * The <|-|> operator is a custom operator that compares the distance between two geometries/geographies
	 * based on the geoNear requirements.
	 *
	 */
	Const *queryConst = makeConst(BsonTypeId(), -1, InvalidOid, -1, PointerGetDatum(
									  geoNearQueryDoc),
								  false, false);

	TargetEntry *firstEntry = linitial(query->targetList);
	Var *docExpr = (Var *) firstEntry->expr;

	GeonearRequest *request = ParseGeonearRequest(geoNearQueryDoc);

	/* Add any query filters available */
	if (request->query.value_type != BSON_TYPE_EOD)
	{
		query = HandleMatch(&(request->query), query, context);
		ValidateQueryOperatorsForGeoNear((Node *) query->jointree->quals, NULL);

		if (TargetListContainsGeonearOp(query->targetList))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
							errmsg("Excessive number of geoNear query expressions")));
		}
	}

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

	/* Update the resno and sortgroup ref based on the query */
	UpdateQueryOperatorContextSortList(query, list_make1(sortGroupClause),
									   list_make1(sortTargetEntry));

	if (query->jointree->quals != NULL)
	{
		quals = lappend(quals, query->jointree->quals);
	}

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

	/* Add the geoNear projection function */
	FuncExpr *projectionExpr = makeFuncExpr(
		BsonDollarProjectGeonearFunctionOid(), BsonTypeId(), list_make2(docExpr,
																		queryConst),
		InvalidOid,
		InvalidOid, COERCE_EXPLICIT_CALL);

	firstEntry->expr = (Expr *) projectionExpr;

	if (context->nestedPipelineLevel > 0)
	{
		/* For nested pipelines this will require a subquery */
		context->requiresSubQueryAfterProject = true;
	}

	return query;
}


/*
 * Creates a multi-argument aggregate based on the provided function id.
 * e.g. BSON_ARRAY_AGG(bson, text)
 */
Aggref *
CreateMultiArgAggregate(Oid aggregateFunctionId, List *args, List *argTypes,
						ParseState *parseState)
{
	Aggref *aggref = makeNode(Aggref);
	aggref->aggfnoid = aggregateFunctionId;
	aggref->aggtype = BsonTypeId();
	aggref->aggtranstype = InvalidOid; /* set by planner later */
	aggref->aggdirectargs = NIL; /* count has no direct args */
	aggref->aggkind = AGGKIND_NORMAL; /* reset by planner */
	/* aggref->agglevelsup = 0 / * > 0 if agg belongs to outer query * / */
	aggref->aggsplit = AGGSPLIT_SIMPLE;
	aggref->aggno = -1;     /* planner will set aggno and aggtransno */
	aggref->aggtransno = -1;
	aggref->location = -1;
	aggref->aggargtypes = argTypes;

	bool aggDistinct = false;
	parseState->p_hasAggs = true;
	transformAggregateCall(parseState, aggref, args, NIL, aggDistinct);
	return aggref;
}


/*
 * Handles the $count stage. Injects an aggregate of
 * bsoncommandcount(*) or bsoncount(*) in the case it needs to repath the count field.
 * First moves existing query to a subquery.
 * Then injects the aggregate projector.
 * We request a new subquery for subsequent stages, but it
 * may not be needed.
 */
static Query *
HandleCountCore(const bson_value_t *existingValue, Query *query,
				AggregationPipelineBuildContext *context, bool isCountCommand)
{
	ReportFeatureUsage(FEATURE_STAGE_COUNT);
	StringView countField = { 0 };
	if (existingValue->value_type == BSON_TYPE_UTF8)
	{
		countField.string = existingValue->value.v_utf8.str;
		countField.length = existingValue->value.v_utf8.len;
	}

	if (countField.length == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40156),
						errmsg(
							"count cannot be empty")));
	}

	if (StringViewStartsWith(&countField, '$'))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40158),
						errmsg(
							"The count field is not allowed to be a $-prefixed path")));
	}

	if (StringViewContains(&countField, '.'))
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40160),
						errmsg(
							"The count field is not allowed to contain '.'.")));
	}

	/* if it is command count query we can just use BSONCOMMANDCOUNT and avoid the bson repath and build. */
	bool useCommandCount = isCountCommand;

	/* Count requires the existing query to move to subquery */
	query = MigrateQueryToSubQuery(query, context);

	/* The first projector is the document */
	TargetEntry *firstEntry = linitial(query->targetList);


	ParseState *parseState = make_parsestate(NULL);
	parseState->p_expr_kind = EXPR_KIND_SELECT_TARGET;
	parseState->p_next_resno = firstEntry->resno + 1;

	Aggref *aggref = NULL;
	Oid aggFuncId = useCommandCount ? BsonCommandCountAggregateFunctionOid()
					: BsonCountAggregateFunctionOid();

	Expr *constValue = (Expr *) makeConst(INT4OID, -1, InvalidOid, 4, Int32GetDatum(
											  1), false, true);
	aggref = CreateSingleArgAggregate(aggFuncId,
									  constValue,
									  parseState);
	firstEntry->expr = (Expr *) aggref;

	/* We wrap the count in a bson_repath_and_build */
	if (!useCommandCount)
	{
		Const *countFieldText = MakeTextConst(countField.string, countField.length);

		List *args = list_make2(countFieldText, aggref);
		FuncExpr *expression = makeFuncExpr(BsonRepathAndBuildFunctionOid(), BsonTypeId(),
											args, InvalidOid, InvalidOid,
											COERCE_EXPLICIT_CALL);
		firstEntry->expr = (Expr *) expression;
	}

	pfree(parseState);
	query->hasAggs = true;

	/* Having count means the next stage must be a new outer query */
	context->requiresSubQuery = true;
	return query;
}


static Query *
HandleCount(const bson_value_t *existingValue, Query *query,
			AggregationPipelineBuildContext *context)
{
	bool isCountCommand = false;
	return HandleCountCore(existingValue, query, context, isCountCommand);
}


/*
 * Handles the $replaceWith stage. Converts to a $replaceRoot stage
 * and calls $replaceRoot
 */
static Query *
HandleReplaceWith(const bson_value_t *existingValue, Query *query,
				  AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_REPLACE_WITH);

	/* Convert to replaceRoot */
	pgbson_writer writer;
	PgbsonWriterInit(&writer);
	PgbsonWriterAppendValue(&writer, "newRoot", 7, existingValue);
	pgbson *bson = PgbsonWriterGetPgbson(&writer);
	bson_value_t currentValue = ConvertPgbsonToBsonValue(bson);
	return HandleReplaceRoot(&currentValue, query, context);
}


/*
 * Checks if a sort can be pushed to an index explicitly. Typically
 * index selection happens based on filters. But in the case where there
 * are no filters, sorts can never be pushed to the index:
 * e.g.
 * find('collection: foo, sort: { a.b: 1 })'
 * In this case, given our sort is bson_orderby() it'll never go through picking
 * the index. However, we do want the ability to push orderby to available indexes
 * if possible. So we add a fullscan filter *iff* the sort is against a base table
 * and it has no filters. If there are any filters, we let the filters determine
 * index pushdown.
 */
static bool
CanPushSortFilterToIndex(Query *query, AggregationPipelineBuildContext *context)
{
	if (list_length(query->jointree->fromlist) != 1)
	{
		return false;
	}

	RangeTblRef *rtref = linitial(query->jointree->fromlist);
	RangeTblEntry *entry = rt_fetch(rtref->rtindex, query->rtable);

	/* Only push sort to index via meta_qual if it's a mongo collection
	 * This means catalog tables used in listCollections and such do not
	 * get the sort.
	 */
	return entry->rtekind == RTE_RELATION && context->mongoCollection != NULL;
}


static Const *
AddCollationToSortSpec(const pgbsonelement *sortElement, const char *collationString)
{
	if (!IsCollationValid(collationString))
	{
		return MakeBsonConst(PgbsonElementToPgbson((pgbsonelement *) sortElement));
	}

	pgbson_writer writer;
	PgbsonWriterInit(&writer);
	PgbsonWriterAppendValue(&writer, sortElement->path, sortElement->pathLength,
							&sortElement->bsonValue);
	PgbsonWriterAppendUtf8(&writer, "collation", 9, collationString);

	return MakeBsonConst(PgbsonWriterGetPgbson(&writer));
}


/*
 * Handles the $sort stage.
 * Creates a subquery if there's a skip/limit (Since those need to be
 * applied first).
 * Parses the sort spec and takes the current projector and applies the sort
 * function on it. Injects the tle_sort_group similiar to the parser phase.
 */
static Query *
HandleSort(const bson_value_t *existingValue, Query *query,
		   AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_SORT);
	if (existingValue->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
						errmsg("Expected document for sort specification")));
	}

	/* If there's an existing skip/limit we need to push those down */
	if (query->limitOffset != NULL || query->limitCount != NULL ||
		query->sortClause != NULL)
	{
		query = MigrateQueryToSubQuery(query, context);
	}

	/* Store the sort in the context (used for downstream groups) */
	context->sortSpec = *existingValue;

	bson_iter_t sortSpec;
	BsonValueInitIterator(existingValue, &sortSpec);

	/* Take the current output (That's to be sorted)*/
	TargetEntry *entry = linitial(query->targetList);

	/* Grab a copy of the targetList */
	List *targetEntryList = list_copy(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 = list_length(query->targetList) + 1;
	List *sortlist = NIL;
	bool isNaturalSort = false;
	bool isNaturalReverseSort = false;
	int naturalCount = 0;
	int nonNaturalCount = 0;

	while (bson_iter_next(&sortSpec))
	{
		pgbsonelement element;
		BsonIterToPgbsonElement(&sortSpec, &element);

		if (strcmp(element.path, "$natural") == 0)
		{
			if (!BsonValueIsNumber(&element.bsonValue))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg("Sort direction value %s is not valid",
									   BsonValueToJsonForLogging(
										   &element.bsonValue))));
			}

			int64_t naturalValue = BsonValueAsInt64(&element.bsonValue);

			isNaturalSort = false;
			isNaturalReverseSort = false;

			if (naturalValue == 1)
			{
				isNaturalSort = true;
			}
			else if (naturalValue == -1)
			{
				isNaturalReverseSort = true;
			}
			else
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(
									"$natural sort cannot be set to a value other than -1 or 1.")));
			}
			naturalCount++;
		}
		else
		{
			nonNaturalCount++;
			Expr *sortInput = entry->expr;
			pgbsonelement subOrderingElement;
			bool isSortByMeta = false;
			if (element.bsonValue.value_type == BSON_TYPE_DOCUMENT &&
				TryGetBsonValueToPgbsonElement(&element.bsonValue, &subOrderingElement) &&
				subOrderingElement.pathLength == 5 &&
				strncmp(subOrderingElement.path, "$meta", 5) == 0)
			{
				RangeTblEntry *rte = linitial(query->rtable);
				isSortByMeta = true;
				if (rte->rtekind == RTE_RELATION ||
					rte->rtekind == RTE_FUNCTION)
				{
					/* This is a base table */
					sortInput = (Expr *) MakeSimpleDocumentVar();
				}
				else
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
									errmsg("Sort direction value %s is not valid",
										   BsonValueToJsonForLogging(
											   &element.bsonValue))));
				}
			}

			Expr *expr = NULL;
			pgbson *sortDoc = PgbsonElementToPgbson(&element);
			Const *sortBson = MakeBsonConst(sortDoc);
			bool isAscending = ValidateOrderbyExpressionAndGetIsAscending(sortDoc);

			bool hasSortById = strcmp(element.path, "_id") == 0;
			if (hasSortById)
			{
				ReportFeatureUsage(FEATURE_STAGE_SORT_BY_ID);
			}

			bool isOrderByIndexTerm = EnableOrderByIndexTerm && !isSortByMeta;
			SortBy *sortBy = makeNode(SortBy);
			SortByNulls sortByNulls = isAscending ? SORTBY_NULLS_FIRST :
									  SORTBY_NULLS_LAST;
			SortByDir sortByDirection = isAscending ? SORTBY_ASC : SORTBY_DESC;
			sortBy->location = -1;

			Oid funcOid = BsonOrderByFunctionOid();
			Oid funcOidWithCollation = IsCollationApplicable(context->collationString) ?
									   BsonOrderByWithCollationFunctionOid() : InvalidOid;
			Oid funcReturnType = BsonTypeId();
			Const *sortOrderConst = sortBson;
			if (isOrderByIndexTerm)
			{
				funcOid = isAscending ? BsonOrderByIndexFunctionOid() :
						  BsonOrderByIndexReverseFunctionOid();
				funcReturnType = BsonIndexTermTypeId();
				funcOidWithCollation = isAscending ?
									   BsonOrderByIndexWithCollationFunctionOid() :
									   BsonOrderByIndexWithCollationReverseFunctionOid();

				pgbsonelement updatedSortElement = element;
				updatedSortElement.bsonValue.value_type = BSON_TYPE_INT32;
				updatedSortElement.bsonValue.value.v_int32 = 1;
				pgbson *updatedSortDoc = PgbsonElementToPgbson(&updatedSortElement);
				sortOrderConst = MakeBsonConst(updatedSortDoc);
			}

			List *args = NIL;

			/* apply collation to the sort comparison */
			if (IsCollationApplicable(context->collationString))
			{
				funcOid = funcOidWithCollation;
				Const *collationConst = MakeTextConst(context->collationString,
													  strlen(
														  context->collationString));

				args = list_make3(sortInput, sortOrderConst, collationConst);

				/*
				 * For ascending order: ORDER BY <value> USING ApiInternalSchemaNameV2.<<<
				 * For descending order: ORDER BY <value> USING ApiInternalSchemaNameV2.>>>
				 */
				if (!isOrderByIndexTerm)
				{
					sortByDirection = SORTBY_USING;
					sortBy->useOp = isAscending ?
									list_make2(makeString(ApiInternalSchemaNameV2),
											   makeString("<<<")) :
									list_make2(makeString(ApiInternalSchemaNameV2),
											   makeString(">>>"));
				}
			}
			else
			{
				args = list_make2(sortInput, sortOrderConst);
			}

			expr = (Expr *) makeFuncExpr(funcOid,
										 funcReturnType,
										 args,
										 InvalidOid, InvalidOid,
										 COERCE_EXPLICIT_CALL);

			if (!isSortByMeta)
			{
				/*
				 * If there's an orderby pushdown to the index, add a full scan clause iff
				 * the query has no filters yet.
				 */
				if (CanPushSortFilterToIndex(query, context))
				{
					AddFullScanQualsForOrderByPushdown(query, sortInput, &element,
													   context);
				}

				/* If sort by is descending use the new operators: this allows for
				 * customization of reverse scan.
				 */
				if (!isAscending && !isOrderByIndexTerm)
				{
					sortByDirection = SORTBY_USING;
					sortBy->useOp = list_make2(makeString(ApiInternalSchemaNameV2),
											   makeString(">>>"));
				}
			}

			sortBy->sortby_dir = sortByDirection;
			sortBy->sortby_nulls = sortByNulls;
			sortBy->node = (Node *) expr;

			bool resjunk = true;
			TargetEntry *sortEntry = makeTargetEntry((Expr *) expr,
													 (AttrNumber) parseState->p_next_resno
													 ++,
													 "?sort?",
													 resjunk);
			targetEntryList = lappend(targetEntryList, sortEntry);
			sortlist = addTargetToSortList(parseState, sortEntry,
										   sortlist, targetEntryList, sortBy);
		}
	}

	/* if there is other operator exist with $natural, should throw exception, example like db.coll.find().sort({$size:1, $natural: -1}) */
	if (naturalCount > 0 && nonNaturalCount > 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
						errmsg(
							"$natural sort cannot be set to a value other than -1 or 1.")));
	}

	if (isNaturalReverseSort || isNaturalSort)
	{
		/* server would throw exception Exception while reading from stream if collection is null,
		 * directly return query when collection is null
		 */
		if (context->mongoCollection == NULL)
		{
			pfree(parseState);
			return query;
		}

		/*
		 * Only allow $natural when the query is a simple base-relation scan.
		 * If this is a view / subquery / derived query, ignore $natural.
		 */
		if (list_length(query->rtable) != 1)
		{
			/* ignore $natural */
			context->sortSpec.value_type = BSON_TYPE_EOD;
			pfree(parseState);
			return query;
		}

		RangeTblEntry *rte = linitial(query->rtable);
		if (!(rte->rtekind == RTE_RELATION && rte->relkind == RELKIND_RELATION))
		{
			/* ignore $natural */
			context->sortSpec.value_type = BSON_TYPE_EOD;
			pfree(parseState);
			return query;
		}

		/*
		 * Safe: base table case -> use CTID
		 */
		Var *ctid_var = makeVar(1, SelfItemPointerAttributeNumber,
								TIDOID, -1, 0, 0);
		TargetEntry *tle_ctid = makeTargetEntry((Expr *) ctid_var,
												(AttrNumber) parseState->p_next_resno++,
												"?ctid?",
												true);

		SortBy *sortBy = makeNode(SortBy);
		sortBy->location = -1;
		sortBy->sortby_dir = isNaturalSort ? SORTBY_ASC : SORTBY_DESC;
		sortBy->sortby_nulls = isNaturalSort ? SORTBY_NULLS_FIRST : SORTBY_NULLS_LAST;
		sortBy->node = (Node *) ctid_var;

		targetEntryList = lappend(targetEntryList, tle_ctid);
		sortlist = addTargetToSortList(parseState, tle_ctid,
									   sortlist, targetEntryList, sortBy);

		query->targetList = targetEntryList;
		query->sortClause = sortlist;
		pfree(parseState);
		return query;
	}

	pfree(parseState);
	if (sortlist == NIL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15976),
						errmsg(
							"The $sort stage requires specifying at least one sorting key to proceed")));
	}

	query->targetList = targetEntryList;
	query->sortClause = sortlist;

	return query;
}


/*
 * Handles the $sortByCount stage.
 * Creates a group followed by a sort and calls each handler
 */
static Query *
HandleSortByCount(const bson_value_t *existingValue, Query *query,
				  AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_SORT_BY_COUNT);

	if (IsCollationApplicable(context->collationString))
	{
		ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						errmsg(
							"collation is not supported in the $sortByCount stage yet.")));
	}

	/* Do validations */
	bool isInvalidSpec = false;
	if (existingValue->value_type == BSON_TYPE_UTF8)
	{
		isInvalidSpec = (existingValue->value.v_utf8.len == 0 ||
						 existingValue->value.v_utf8.str[0] != '$');
	}
	else if (existingValue->value_type == BSON_TYPE_DOCUMENT)
	{
		pgbsonelement expressionElement;
		isInvalidSpec =
			!TryGetBsonValueToPgbsonElement(existingValue, &expressionElement) ||
			expressionElement.pathLength == 0 ||
			expressionElement.path[0] != '$';
	}
	else
	{
		isInvalidSpec = true;
	}

	if (isInvalidSpec)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40147),
						errmsg(
							"The sortByCount field must be specified either as a $-prefixed path or as a valid expression contained within an object.")));
	}

	/* Convert to
	 * { $group: { _id: <expression>, count: { $sum: 1 } } },
	 * { $sort: { count: -1 } }
	 */

	pgbson_writer groupWriter;
	PgbsonWriterInit(&groupWriter);
	PgbsonWriterAppendValue(&groupWriter, "_id", 3, existingValue);

	pgbson_writer countWriter;
	PgbsonWriterStartDocument(&groupWriter, "count", 5, &countWriter);
	PgbsonWriterAppendInt32(&countWriter, "$sum", 4, 1);
	PgbsonWriterEndDocument(&groupWriter, &countWriter);

	pgbson *group = PgbsonWriterGetPgbson(&groupWriter);
	bson_value_t groupValue = ConvertPgbsonToBsonValue(group);

	query = HandleGroup(&groupValue, query, context);
	if (context->requiresSubQuery)
	{
		query = MigrateQueryToSubQuery(query, context);
	}

	/* Add sort */
	pgbson_writer sortWriter;
	PgbsonWriterInit(&sortWriter);
	PgbsonWriterAppendInt32(&sortWriter, "count", 5, -1);
	pgbson *sort = PgbsonWriterGetPgbson(&sortWriter);
	bson_value_t sortValue = ConvertPgbsonToBsonValue(sort);

	query = HandleSort(&sortValue, query, context);
	return query;
}


/*
 * Checks whether a $sort specification can be pushed to an accumulator operator.
 * Cases with $natural or $meta cannot be pushed.
 */
static bool
IsSortSpecCompatibleForPushToAccumulatorOperator(const bson_value_t *sortValue)
{
	if (sortValue->value_type != BSON_TYPE_DOCUMENT)
	{
		return false;
	}

	bson_iter_t sortIter;
	BsonValueInitIterator(sortValue, &sortIter);

	while (bson_iter_next(&sortIter))
	{
		pgbsonelement element;
		BsonIterToPgbsonElement(&sortIter, &element);

		/* $natural sort uses ctid ordering which aggorder cannot express */
		if (strcmp(element.path, "$natural") == 0)
		{
			return false;
		}

		/* Only numeric sort directions (1/-1) can be pushed to accumulator;
		 * non-numeric types like $meta are not supported. */
		if (!BsonValueIsNumber(&element.bsonValue))
		{
			return false;
		}
	}

	return true;
}


/* Checks whether a $sort preceding $group can be dropped or pushed into accumulator aggorder. */
static SortGroupAnalysisResult
AnalyzeSortGroupAccumulators(const bson_value_t *groupValue,
							 const bson_value_t *sortValue)
{
	SortGroupAnalysisResult result = {
		.canDropSort = false,
		.groupKeysFormSortPrefix = false,
		.suffixSortSpec = { .value_type = BSON_TYPE_EOD }
	};

	bson_iter_t groupIter;
	BsonValueInitIterator(groupValue, &groupIter);

	bool hasFirstAccumulator = false;

	while (bson_iter_next(&groupIter))
	{
		StringView keyView = bson_iter_key_string_view(&groupIter);
		if (StringViewEquals(&keyView, &IdFieldStringView))
		{
			continue;
		}

		bson_iter_t accumulatorIterator;
		if (!BSON_ITER_HOLDS_DOCUMENT(&groupIter) ||
			!bson_iter_recurse(&groupIter, &accumulatorIterator))
		{
			/* HandleGroup will report this error */
			return result;
		}

		pgbsonelement accumulatorElement;
		if (!TryGetSinglePgbsonElementFromBsonIterator(&accumulatorIterator,
													   &accumulatorElement))
		{
			/* HandleGroup will report this error */
			return result;
		}

		StringView accumulatorName = {
			.length = accumulatorElement.pathLength, .string = accumulatorElement.path
		};

		if (StringViewEqualsCString(&accumulatorName, "$first"))
		{
			hasFirstAccumulator = true;
			continue;
		}

		/* Order-insensitive accumulators - sort can be safely dropped or pushed */
		if (StringViewEqualsCString(&accumulatorName, "$avg") ||
			StringViewEqualsCString(&accumulatorName, "$sum") ||
			StringViewEqualsCString(&accumulatorName, "$max") ||
			StringViewEqualsCString(&accumulatorName, "$min") ||
			StringViewEqualsCString(&accumulatorName, "$count"))
		{
			continue;
		}

		/* Accumulator like $last, $push, $percentile etc. that needs a full $sort stage */
		return result;
	}

	/* After the loop every remaining accumulator is either $first or
	* order-insensitive.  The sort is unnecessary only when none of
	* the accumulators were order-sensitive (i.e. no $first seen). */
	result.canDropSort = !hasFirstAccumulator;

	/* When $first is present, check compatibility once for the full sort
	 * spec, then try the prefix-based suffix decomposition. */
	if (hasFirstAccumulator)
	{
		bool canPushSortToAccumulator =
			IsSortSpecCompatibleForPushToAccumulatorOperator(sortValue);
		if (canPushSortToAccumulator)
		{
			result.groupKeysFormSortPrefix =
				TryBuildSuffixSortSpec(groupValue, sortValue, &result.suffixSortSpec);
		}
	}

	return result;
}


/*
 * Extracts the unprefixed group-by field paths from a $group _id value
 * (scalar "$f" or document { f: "$f", ... }).  Returns false (and leaves
 * *numFieldPaths == 0) when _id isn't a string or document, an entry isn't
 * a "$"-prefixed field reference, an entry is a "$$" variable ref, or the
 * field count would exceed maxFieldPaths.
 */
static bool
TryExtractGroupByFieldPaths(const bson_value_t *idValue,
							StringView *fieldPaths,
							int maxFieldPaths,
							int *numFieldPaths)
{
	*numFieldPaths = 0;

	if (maxFieldPaths < 1)
	{
		return false;
	}

	if (idValue->value_type == BSON_TYPE_UTF8)
	{
		/* Scalar _id (e.g. "$f3"): require "$" + >=1 field char (also makes
		 * path[1] safe), and reject "$$" variable refs (e.g. "$$ROOT"). */
		const char *path = idValue->value.v_utf8.str;
		uint32_t pathLen = idValue->value.v_utf8.len;
		if (pathLen < 2 || path[0] != '$' || path[1] == '$')
		{
			return false;
		}

		fieldPaths[0].string = path + 1;
		fieldPaths[0].length = pathLen - 1;
		*numFieldPaths = 1;
		return true;
	}
	else if (idValue->value_type == BSON_TYPE_DOCUMENT)
	{
		/* Compound _id: e.g., { f3: "$f3", f4: "$f4" } */
		bson_iter_t idIter;
		BsonValueInitIterator(idValue, &idIter);
		int count = 0;
		while (bson_iter_next(&idIter))
		{
			if (count >= maxFieldPaths)
			{
				return false;
			}

			const bson_value_t *val = bson_iter_value(&idIter);
			if (val->value_type != BSON_TYPE_UTF8)
			{
				return false;
			}

			/* Same per-entry guard as the scalar branch: require "$" + >=1
			 * field char and reject "$$" variable refs (e.g. "$$ROOT"). */
			const char *path = val->value.v_utf8.str;
			uint32_t pathLen = val->value.v_utf8.len;
			if (pathLen < 2 || path[0] != '$' || path[1] == '$')
			{
				return false;
			}

			fieldPaths[count].string = path + 1;
			fieldPaths[count].length = pathLen - 1;
			count++;
		}

		if (count == 0)
		{
			return false;
		}

		*numFieldPaths = count;
		return true;
	}

	return false;
}


/*
 * Checks if group-by keys are a non-dotted prefix of the sort keys and builds
 * a suffix sort spec containing only the remaining sort keys.
 * Returns true if the group keys form a valid prefix of (or exactly match)
 * the sort keys; false if the keys don't match or a bail-out occurs (e.g.
 * dotted paths).  When true and there are remaining sort keys, suffixSortSpec
 * is populated; when true and there are no remaining keys, suffixSortSpec
 * stays EOD (exact-match case).
 */
static bool
TryBuildSuffixSortSpec(const bson_value_t *groupValue,
					   const bson_value_t *sortValue,
					   bson_value_t *suffixSortSpec)
{
	if (groupValue->value_type != BSON_TYPE_DOCUMENT ||
		sortValue->value_type != BSON_TYPE_DOCUMENT)
	{
		return false;
	}

	/* Step 1: Locate _id within the group spec. */
	bson_iter_t groupIter;
	BsonValueInitIterator(groupValue, &groupIter);

	bson_value_t idValue = { 0 };
	while (bson_iter_next(&groupIter))
	{
		StringView keyView = bson_iter_key_string_view(&groupIter);
		if (StringViewEquals(&keyView, &IdFieldStringView))
		{
			idValue = *bson_iter_value(&groupIter);
			break;
		}
	}

	if (idValue.value_type == BSON_TYPE_EOD)
	{
		return false;
	}

	/* Step 2: Extract the group-by field paths from _id. */
	StringView groupKeys[MAX_SUFFIX_SORT_GROUP_KEYS];
	int numGroupKeys = 0;
	if (!TryExtractGroupByFieldPaths(&idValue, groupKeys,
									 MAX_SUFFIX_SORT_GROUP_KEYS, &numGroupKeys))
	{
		return false;
	}

	/* Reject dotted paths: pushing a sort with dotted keys into the
	 * accumulator is not supported by the prefix-decomposition path. */
	for (int i = 0; i < numGroupKeys; i++)
	{
		if (StringViewContains(&groupKeys[i], '.'))
		{
			return false;
		}
	}

	/* Step 3: Check that group keys form an exact prefix of sort keys. */
	bson_iter_t sortIter;
	BsonValueInitIterator(sortValue, &sortIter);

	for (int i = 0; i < numGroupKeys; i++)
	{
		if (!bson_iter_next(&sortIter))
		{
			/* Sort spec has fewer keys than group keys */
			return false;
		}

		uint32_t sortKeyLen = bson_iter_key_len(&sortIter);
		const char *sortKey = bson_iter_key(&sortIter);
		if (sortKeyLen != groupKeys[i].length ||
			memcmp(groupKeys[i].string, sortKey, sortKeyLen) != 0)
		{
			return false;
		}
	}

	/* Step 4: Build suffix sort spec from remaining sort keys. */
	if (!bson_iter_next(&sortIter))
	{
		/* No suffix keys — sort is fully covered by group keys.
		 * Signal this via the suffixSortSpec remaining EOD; the caller
		 * distinguishes this from bail-out via groupKeysFormSortPrefix. */
		return true;
	}

	pgbson_writer suffixWriter;
	PgbsonWriterInit(&suffixWriter);

	/* Write the current (first suffix) key and then the rest */
	do {
		pgbsonelement element;
		BsonIterToPgbsonElement(&sortIter, &element);
		PgbsonWriterAppendValue(&suffixWriter, element.path, element.pathLength,
								&element.bsonValue);
	} while (bson_iter_next(&sortIter));

	pgbson *suffixBson = PgbsonWriterGetPgbson(&suffixWriter);
	*suffixSortSpec = ConvertPgbsonToBsonValue(suffixBson);

	return true;
}


static void
ValidateSortSpec(const bson_value_t *sortValue)
{
	if (sortValue->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40236),
						errmsg(
							"The sort specification must be a document.")));
	}

	bson_iter_t sortIter;
	BsonValueInitIterator(sortValue, &sortIter);

	int naturalCount = 0;
	int nonNaturalCount = 0;

	while (bson_iter_next(&sortIter))
	{
		pgbsonelement element;
		BsonIterToPgbsonElement(&sortIter, &element);

		if (strcmp(element.path, "$natural") == 0)
		{
			if (!BsonValueIsNumber(&element.bsonValue))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg("Sort direction value %s is not valid",
									   BsonValueToJsonForLogging(
										   &element.bsonValue))));
			}

			int64_t naturalValue = BsonValueAsInt64(&element.bsonValue);
			if (naturalValue != 1 && naturalValue != -1)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(
									"$natural sort cannot be set to a value other than -1 or 1.")));
			}
			naturalCount++;
		}
		else
		{
			nonNaturalCount++;
			pgbson *sortDoc = PgbsonElementToPgbson(&element);
			ValidateOrderbyExpressionAndGetIsAscending(sortDoc);
		}
	}

	if (naturalCount > 0 && nonNaturalCount > 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
						errmsg(
							"$natural sort cannot be set to a value other than -1 or 1.")));
	}

	if (naturalCount == 0 && nonNaturalCount == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15976),
						errmsg(
							"The $sort stage requires specifying at least one sorting key to proceed")));
	}
}


static void
SetAccumulatorSortOrder(TargetEntry *accumulatorTle, Expr *documentExpr,
						const bson_value_t *sortSpec,
						const char *collationString)
{
	Assert(IsA(accumulatorTle->expr, Aggref));
	Aggref *aggref = (Aggref *) accumulatorTle->expr;

	bson_iter_t sortIter;
	BsonValueInitIterator(sortSpec, &sortIter);

	ParseState *sortParseState = make_parsestate(NULL);
	sortParseState->p_expr_kind = EXPR_KIND_ORDER_BY;

	if (aggref->args == NIL || list_length(aggref->args) == 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INTERNALERROR),
						errmsg(
							"Unexpected error in SetAccumulatorSortOrder: AggRef has no arguments")));
	}

	/* Continue resno numbering from where the existing args left off. */
	TargetEntry *lastArg = llast(aggref->args);

	sortParseState->p_next_resno = lastArg->resno + 1;

	bool hasCollation = IsCollationApplicable(collationString);

	List *aggorderList = NIL;

	while (bson_iter_next(&sortIter))
	{
		pgbsonelement element;
		BsonIterToPgbsonElement(&sortIter, &element);

		pgbson *sortDoc = PgbsonElementToPgbson(&element);
		bool isAscending = ValidateOrderbyExpressionAndGetIsAscending(sortDoc);

		bool isOrderByIndexTerm = EnableOrderByIndexTerm;

		Oid funcOid = BsonOrderByFunctionOid();
		Oid funcReturnType = BsonTypeId();
		Const *sortOrderConst = MakeBsonConst(sortDoc);

		if (isOrderByIndexTerm)
		{
			funcOid = isAscending ? BsonOrderByIndexFunctionOid() :
					  BsonOrderByIndexReverseFunctionOid();
			funcReturnType = BsonIndexTermTypeId();

			pgbsonelement updatedSortElement = element;
			updatedSortElement.bsonValue.value_type = BSON_TYPE_INT32;
			updatedSortElement.bsonValue.value.v_int32 = 1;
			sortOrderConst = MakeBsonConst(PgbsonElementToPgbson(&updatedSortElement));
		}

		List *sortFuncArgs;

		if (hasCollation)
		{
			if (isOrderByIndexTerm)
			{
				funcOid = isAscending ?
						  BsonOrderByIndexWithCollationFunctionOid() :
						  BsonOrderByIndexWithCollationReverseFunctionOid();
			}
			else
			{
				funcOid = BsonOrderByWithCollationFunctionOid();
			}
			Const *collationConst = MakeTextConst(collationString,
												  strlen(collationString));
			sortFuncArgs = list_make3(copyObject(documentExpr), sortOrderConst,
									  collationConst);
		}
		else
		{
			sortFuncArgs = list_make2(copyObject(documentExpr), sortOrderConst);
		}

		Expr *sortExpr = (Expr *) makeFuncExpr(
			funcOid, funcReturnType, sortFuncArgs,
			InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);

		SortBy *sortBy = makeNode(SortBy);
		sortBy->location = -1;
		sortBy->sortby_nulls = isAscending ? SORTBY_NULLS_FIRST : SORTBY_NULLS_LAST;
		sortBy->node = (Node *) sortExpr;

		if (hasCollation && !isOrderByIndexTerm)
		{
			sortBy->sortby_dir = SORTBY_USING;
			sortBy->useOp = isAscending ?
							list_make2(makeString(ApiInternalSchemaNameV2),
									   makeString("<<<")) :
							list_make2(makeString(ApiInternalSchemaNameV2),
									   makeString(">>>"));
		}
		else if (isAscending)
		{
			sortBy->sortby_dir = SORTBY_ASC;
		}
		else if (isOrderByIndexTerm)
		{
			/* bsonindexterm already encodes direction via the function variant
			 * (bson_orderby_index_reverse), so standard DESC suffices. The
			 * custom >>> operator is only defined for the bson type. */
			sortBy->sortby_dir = SORTBY_DESC;
		}
		else
		{
			/* Use custom >>> operator for descending, matching HandleSort */
			sortBy->sortby_dir = SORTBY_USING;
			sortBy->useOp = list_make2(makeString(ApiInternalSchemaNameV2),
									   makeString(">>>"));
		}

		bool resjunk = true;
		TargetEntry *sortTle = makeTargetEntry(
			sortExpr,
			(AttrNumber) sortParseState->p_next_resno++,
			NULL, resjunk);

		aggref->args = lappend(aggref->args, sortTle);

		aggorderList = addTargetToSortList(sortParseState, sortTle,
										   aggorderList, aggref->args, sortBy);
	}

	aggref->aggorder = aggorderList;
}


static Query *
HandleSortGroup(const bson_value_t *existingValue, Query *query,
				AggregationPipelineBuildContext *context)
{
	bson_iter_t sortGroupSpec;
	bson_value_t sortValue = { 0 };
	bson_value_t groupValue = { 0 };
	BsonValueInitIterator(existingValue, &sortGroupSpec);
	while (bson_iter_next(&sortGroupSpec))
	{
		const char *fieldName = bson_iter_key(&sortGroupSpec);
		if (strcmp(fieldName, "sort") == 0)
		{
			sortValue = *bson_iter_value(&sortGroupSpec);
		}
		else if (strcmp(fieldName, "group") == 0)
		{
			groupValue = *bson_iter_value(&sortGroupSpec);
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INTERNALERROR),
							errmsg(
								"Invalid specification for $sortGroup. Expected fields are 'sort' and 'group'.")));
		}
	}

	if (sortValue.value_type == BSON_TYPE_EOD || groupValue.value_type == BSON_TYPE_EOD)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INTERNALERROR),
						errmsg(
							"Invalid specification for $sortGroup. Both 'sort' and 'group' fields are required.")));
	}

	SortGroupAnalysisResult analysisResult = AnalyzeSortGroupAccumulators(&groupValue,
																		  &sortValue);

	/* Drop the explicit Sort node when :
	 * 1) Group keys form a non-dotted prefix of the sort keys; any remaining suffix keys are pushed into the
	 * accumulator's ORDER BY.  Gated by EnableSortPushToAccumulatorWithPrefix.
	 * 2) No order-sensitive accumulators are present.
	 * */
	bool hasSuffixSort = analysisResult.suffixSortSpec.value_type != BSON_TYPE_EOD;
	if (analysisResult.groupKeysFormSortPrefix)
	{
		ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_SORT_PREFIX_CANDIDATE);
	}
	bool prefixOptimization = analysisResult.groupKeysFormSortPrefix &&
							  EnableSortPushToAccumulatorWithPrefix;
	bool canPushSort = prefixOptimization && hasSuffixSort;
	bool canSkipSortNode = prefixOptimization || analysisResult.canDropSort;

	if (canSkipSortNode)
	{
		/* Two skip-the-Sort-node paths converge here: prefix optimization
		 * (sort keys are covered by group keys, optionally with a suffix
		 * pushed into the accumulator) and canDropSort (every accumulator
		 * is order-insensitive).  Neither calls HandleSort, so we must
		 * still validate the spec here to catch bad inputs (empty spec,
		 * mixed $natural and non-$natural keys, invalid directions). */
		ValidateSortSpec(&sortValue);
	}
	else
	{
		query = HandleSort(&sortValue, query, context);
		if (context->requiresSubQuery || context->requiresSubQueryAfterProject)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INTERNALERROR),
							errmsg(
								"Unexpected error - $sort stage demands a subquery when it should not.")));
		}
	}

	const bson_value_t *accumulatorSortSpec = canPushSort ?
											  &analysisResult.suffixSortSpec : NULL;

	return HandleGroupCore(&groupValue, query, context,
						   accumulatorSortSpec);
}


/*
 * Helper method that adds a group expression projection to the query's targetList.
 * Creates a VAR that can be used in the projector of the higher level sub-query.
 * Used with GROUP to ensure that we can split the group into
 * generate projectors/generate bson_repath_and_build in the second stage.
 */
inline static Var *
AddGroupExpression(Expr *expression, ParseState *parseState, char *identifiers,
				   Query *query, Oid outputOid, TargetEntry **createdEntry)
{
	int identifierInt = parseState->p_next_resno++;
	identifiers[0] = 'c';
	pg_ltoa(identifierInt, &identifiers[1]);
	bool resjunk = false;
	TargetEntry *entry = makeTargetEntry(expression, identifierInt, pstrdup(identifiers),
										 resjunk);
	query->targetList = lappend(query->targetList, entry);

	if (createdEntry)
	{
		*createdEntry = entry;
	}

	query->hasAggs = true;
	parseState->p_hasAggs = true;
	Index childIndex = 1;
	return makeVar(childIndex, entry->resno, outputOid, -1, InvalidOid, 0);
}


/*
 * Given an accumulator expression, checks whether it can be statically
 * evaluated to a const. If it can, then returns a const "Expr".
 * If not, returns the original docExpr.
 */
static Expr *
GetDocumentExprForGroupAccumulatorValue(const bson_value_t *accumulatorValue,
										Expr *docExpr)
{
	ParseAggregationExpressionContext parseContext = { 0 };
	AggregationExpressionData expressionData;
	memset(&expressionData, 0, sizeof(AggregationExpressionData));

	ParseAggregationExpressionData(&expressionData, accumulatorValue, &parseContext);
	if (expressionData.kind == AggregationExpressionKind_Constant)
	{
		/* Expression evaluates to a const, we don't need to pass in the input documentExpr */
		return (Expr *) MakeBsonConst(PgbsonInitEmpty());
	}

	return docExpr;
}


/*
 * True when the document is read through the distributed table rather than
 * directly off a local, unsharded shard base table: either the collection is
 * sharded, or the shard base table wasn't usable for this query. In that case
 * the document column is typed as core-schema bson and must be converted to the
 * API bson type before being consumed by aggregates that expect it.
 */
inline static bool
IsDistributedShardContext(AggregationPipelineBuildContext *context)
{
	return context->mongoCollection != NULL &&
		   (context->mongoCollection->shardKey != NULL ||
			!context->allowShardBaseTable);
}


/*
 * Simple helper method that has logic to insert a Group accumulator to a query.
 * This adds the group aggregate to the TargetEntry (for projection)
 * and also adds the necessary data to the bson_repath_and_build arguments.
 */
inline static List *
AddSimpleGroupAccumulator(Query *query, const bson_value_t *accumulatorValue,
						  List *repathArgs, Const *accumulatorText,
						  ParseState *parseState, char *identifiers,
						  Expr *documentExpr, Oid aggregateFunctionOid,
						  Expr *variableSpec,
						  TargetEntry **createdAccumulatorEntry)
{
	Expr *constValue = (Expr *) MakeBsonConst(BsonValueToDocumentPgbson(
												  accumulatorValue));

	documentExpr = GetDocumentExprForGroupAccumulatorValue(accumulatorValue,
														   documentExpr);
	Const *trueConst = makeConst(BOOLOID, -1, InvalidOid, 1, BoolGetDatum(true), false,
								 true);
	List *groupArgs;
	Oid functionId;
	if (variableSpec != NULL)
	{
		groupArgs = list_make4(documentExpr, constValue, trueConst, variableSpec);
		functionId = BsonExpressionGetWithLetFunctionOid();
	}
	else
	{
		groupArgs = list_make3(documentExpr, constValue, trueConst);
		functionId = BsonExpressionGetFunctionOid();
	}


	FuncExpr *accumFunc = makeFuncExpr(
		functionId, BsonTypeId(), groupArgs, InvalidOid,
		InvalidOid, COERCE_EXPLICIT_CALL);

	if (BsonTypeId() != DocumentDBCoreBsonTypeId())
	{
		accumFunc = makeFuncExpr(
			DocumentDBCoreBsonToBsonFunctionOId(), BsonTypeId(), list_make1(accumFunc),
			InvalidOid,
			InvalidOid, COERCE_EXPLICIT_CALL);
	}

	Aggref *aggref = CreateSingleArgAggregate(aggregateFunctionOid,
											  (Expr *) accumFunc, parseState);
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumulatorText,
														parseState, identifiers,
														query, TEXTOID, NULL));
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) aggref,
														parseState, identifiers,
														query, BsonTypeId(),
														createdAccumulatorEntry));
	return repathArgs;
}


inline static List *
AddSimpleGroupAccumulatorWithExpr(Query *query, const bson_value_t *accumulatorValue,
								  List *repathArgs, Const *accumulatorText,
								  ParseState *parseState, char *identifiers,
								  Expr *documentExpr, Oid aggregateFunctionOid,
								  AggregationPipelineBuildContext *context,
								  TargetEntry **createdAccumulatorEntry)
{
	Expr *exprConst = (Expr *) MakeBsonConst(BsonValueToDocumentPgbson(accumulatorValue));

	documentExpr = GetDocumentExprForGroupAccumulatorValue(accumulatorValue,
														   documentExpr);

	if (IsDistributedShardContext(context) &&
		BsonTypeId() != DocumentDBCoreBsonTypeId())
	{
		documentExpr = (Expr *) makeFuncExpr(
			DocumentDBCoreBsonToBsonFunctionOId(), BsonTypeId(), list_make1(documentExpr),
			InvalidOid,
			InvalidOid, COERCE_EXPLICIT_CALL);
	}

	Const *collationConst = IsCollationApplicable(context->collationString) ?
							MakeTextConst(context->collationString,
										  strlen(context->collationString)) :
							makeNullConst(TEXTOID, -1, InvalidOid);

	List *aggregateArgs = list_make4(documentExpr, exprConst, context->variableSpec,
									 collationConst);
	List *argTypesList = list_make4_oid(BsonTypeId(), BsonTypeId(), BsonTypeId(),
										TEXTOID);

	Aggref *aggref = CreateMultiArgAggregate(aggregateFunctionOid,
											 aggregateArgs,
											 argTypesList,
											 parseState);

	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumulatorText,
														parseState, identifiers,
														query, TEXTOID, NULL));
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) aggref,
														parseState, identifiers,
														query, BsonTypeId(),
														createdAccumulatorEntry));
	return repathArgs;
}


inline static List *
AddSumGroupAccumulator(Query *query, const bson_value_t *accumulatorValue,
					   List *repathArgs, Const *accumulatorText,
					   ParseState *parseState, char *identifiers,
					   Expr *documentExpr, bool isGroupIdConst,
					   AggregationPipelineBuildContext *context)
{
	bool canUseBsonCountAggregate = isGroupIdConst;

	bool useNewCountAggregate = false;
	if (canUseBsonCountAggregate &&
		BsonValueIsNumber(accumulatorValue))
	{
		/* use for counting with $sum: 1 only */
		int64_t countValue = BsonValueAsInt64(accumulatorValue);
		useNewCountAggregate = countValue == 1;
	}

	if (!useNewCountAggregate)
	{
		if (CanUseWithExprAggregates())
		{
			return AddSimpleGroupAccumulatorWithExpr(query, accumulatorValue,
													 repathArgs, accumulatorText,
													 parseState, identifiers,
													 documentExpr,
													 BsonSumWithExprAggregateFunctionOid(),
													 context,
													 NULL);
		}
		else
		{
			return AddSimpleGroupAccumulator(query, accumulatorValue, repathArgs,
											 accumulatorText, parseState,
											 identifiers, documentExpr,
											 BsonSumAggregateFunctionOid(),
											 context->variableSpec,
											 NULL);
		}
	}

	Expr *constValue = (Expr *) makeConst(INT4OID, -1, InvalidOid, 4, Int32GetDatum(1),
										  false, true);
	Aggref *aggref = CreateSingleArgAggregate(BsonCountAggregateFunctionOid(),
											  constValue, parseState);
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumulatorText,
														parseState, identifiers,
														query, TEXTOID, NULL));
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) aggref,
														parseState, identifiers,
														query, BsonTypeId(),
														NULL));

	return repathArgs;
}


/*
 * Simple helper method that has logic to insert a BSON_ARRAY_AGG accumulator to a query.
 * This adds the group aggregate to the TargetEntry (for projection)
 * and also adds the necessary data to the bson_repath_and_build arguments.
 */
inline static List *
AddArrayAggGroupAccumulator(Query *query, const bson_value_t *accumulatorValue,
							List *repathArgs, Const *accumulatorText,
							ParseState *parseState, char *identifiers,
							Expr *documentExpr, Oid aggregateFunctionOid,
							char *fieldPath, bool handleSingleValue,
							Expr *variableSpec)
{
	Expr *constValue = (Expr *) MakeBsonConst(BsonValueToDocumentPgbson(
												  accumulatorValue));

	/* isNullOnEmpty is set to false for $push */
	Const *isNullOnEmptyConst = (Const *) MakeBoolValueConst(false);
	List *funcArgs;
	Oid functionOid;


	if (variableSpec != NULL)
	{
		funcArgs = list_make4(documentExpr, constValue, isNullOnEmptyConst, variableSpec);
		functionOid = BsonExpressionGetWithLetFunctionOid();
	}
	else
	{
		funcArgs = list_make3(documentExpr, constValue, isNullOnEmptyConst);
		functionOid = BsonExpressionGetFunctionOid();
	}

	FuncExpr *accumFunc = makeFuncExpr(functionOid, BsonTypeId(), funcArgs, InvalidOid,
									   InvalidOid, COERCE_EXPLICIT_CALL);

	List *aggregateArgs = list_make3(
		(Expr *) accumFunc,
		MakeTextConst(fieldPath, strlen(fieldPath)),
		MakeBoolValueConst(handleSingleValue));
	List *argTypesList = list_make3_oid(BsonTypeId(), TEXTOID, BOOLOID);

	Aggref *aggref = CreateMultiArgAggregate(aggregateFunctionOid,
											 aggregateArgs,
											 argTypesList,
											 parseState);

	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumulatorText,
														parseState, identifiers,
														query, TEXTOID, NULL));
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) aggref,
														parseState, identifiers,
														query, BsonTypeId(),
														NULL));
	return repathArgs;
}


/*
 * Simple helper method that has logic to insert a BSON_MERGE_OBJECTS accumulator to a query.
 * This adds the group aggregate to the TargetEntry (for projection)
 * and also adds the necessary data to the bson_repath_and_build arguments.
 */
inline static List *
AddMergeObjectsGroupAccumulator(Query *query, const bson_value_t *accumulatorValue,
								List *repathArgs, Const *accumulatorText,
								ParseState *parseState, char *identifiers,
								Expr *documentExpr, const bson_value_t *sortSpec,
								Expr *variableSpec)
{
	/* First apply the sorted aggs */
	int nelems = BsonDocumentValueCountKeys(sortSpec);
	Datum *sortDatumArray = palloc(sizeof(Datum) * nelems);

	bson_iter_t sortIter;
	BsonValueInitIterator(sortSpec, &sortIter);
	int i = 0;
	while (bson_iter_next(&sortIter))
	{
		pgbsonelement sortElement = { 0 };
		sortElement.path = bson_iter_key(&sortIter);
		sortElement.pathLength = strlen(sortElement.path);
		sortElement.bsonValue = *bson_iter_value(&sortIter);
		sortDatumArray[i] = PointerGetDatum(PgbsonElementToPgbson(&sortElement));
		i++;
	}

	/* Here we use INT_MAX to sort the whole array regardless of its length. */
	Const *nConst = makeConst(INT8OID, -1, InvalidOid, sizeof(int64_t),
							  Int64GetDatum(INT_MAX), false, true);
	ArrayType *arrayValue = construct_array(sortDatumArray, nelems, BsonTypeId(), -1,
											false, TYPALIGN_INT);

	Const *sortArrayConst = makeConst(GetBsonArrayTypeOid(), -1, InvalidOid, -1,
									  PointerGetDatum(arrayValue), false, false);


	if (variableSpec != NULL)
	{
		/* If variableSpec only contains the time system variables, do not fail. */
		Node *specNode = (Node *) variableSpec;
		Const *specConst = (Const *) specNode;
		pgbson *specBson = DatumGetPgBson(specConst->constvalue);

		bson_iter_t iter;
		if (PgbsonInitIteratorAtPath(specBson, "let", &iter))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_COMMANDNOTSUPPORTED),
							errmsg("let with $mergeObjects is not supported yet")));
		}
	}

	/*
	 * Here we add a parameter with the input expression. The reason is we need
	 * to evaluate it against the document after the sort takes place.
	 */
	Expr *inputExpression = (Expr *) MakeBsonConst(BsonValueToDocumentPgbson(
													   accumulatorValue));
	Aggref *aggref = CreateMultiArgAggregate(BsonMergeObjectsFunctionOid(),
											 list_make4(documentExpr, nConst,
														sortArrayConst,
														inputExpression),
											 list_make4_oid(BsonTypeId(),
															nConst->consttype,
															sortArrayConst->consttype,
															BsonTypeId()),
											 parseState);

	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumulatorText,
														parseState, identifiers,
														query, TEXTOID, NULL));
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) aggref,
														parseState, identifiers,
														query, BsonTypeId(),
														NULL));
	return repathArgs;
}


/*
 * Simple helper method that has logic to insert a Group accumulator to a query.
 * This adds the group aggregate to the TargetEntry (for projection)
 * and also adds the necessary data to the bson_expression_map arguments.
 */
inline static List *
AddSimpleNGroupAccumulator(Query *query, const bson_value_t *input,
						   bson_value_t *elementsToFetch,
						   List *repathArgs, Const *accumulatorText,
						   ParseState *parseState, char *identifiers,
						   Expr *documentExpr, Oid aggregateFunctionOid,
						   StringView *accumulatorName, Expr *variableSpec)
{
	ValidateElementForNGroupAccumulators(elementsToFetch, accumulatorName->string);

	/* First add the agg function */
	Const *nConst = makeConst(INT8OID, -1, InvalidOid, sizeof(int64_t),
							  Int64GetDatum(elementsToFetch->value.v_int64), false, true);
	Aggref *aggref = CreateMultiArgAggregate(aggregateFunctionOid,
											 list_make2((Expr *) documentExpr, nConst),
											 list_make2_oid(BsonTypeId(),
															nConst->consttype),
											 parseState);

	/* First apply the map function to the document */
	Const *fieldConst = makeConst(TEXTOID, -1, InvalidOid, -1, CStringGetTextDatum(""),
								  false,
								  false);
	Expr *constValue = (Expr *) MakeBsonConst(BsonValueToDocumentPgbson(input));

	Const *trueConst = makeConst(BOOLOID, -1, InvalidOid, 1, BoolGetDatum(true), false,
								 true);
	List *groupArgs;
	Oid functionOid;

	if (variableSpec != NULL)
	{
		functionOid = BsonExpressionMapWithLetFunctionOid();
		groupArgs = list_make5(aggref, fieldConst, constValue, trueConst,
							   variableSpec);
	}
	else
	{
		functionOid = BsonExpressionMapFunctionOid();
		groupArgs = list_make4(aggref, fieldConst, constValue, trueConst);
	}

	FuncExpr *accumFunc = makeFuncExpr(
		functionOid, BsonTypeId(), groupArgs, InvalidOid,
		InvalidOid, COERCE_EXPLICIT_CALL);

	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumulatorText,
														parseState, identifiers,
														query, TEXTOID,
														NULL));
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumFunc,
														parseState, identifiers,
														query, BsonTypeId(), NULL));
	return repathArgs;
}


/*
 * Add a sorted group accumulator e.g. BSONFIRST/BSONLAST
 */
inline static List *
AddSortedGroupAccumulator(Query *query, const bson_value_t *accumulatorValue,
						  List *repathArgs, Const *accumulatorText,
						  ParseState *parseState, char *identifiers,
						  Expr *documentExpr, Oid aggregateFunctionOid,
						  const bson_value_t *sortSpec,
						  Expr *variableSpec)
{
	/* First apply the sorted agg (FIRST/LAST) */
	int nelems = BsonDocumentValueCountKeys(sortSpec);
	Datum *sortDatumArray = palloc(sizeof(Datum) * nelems);

	bson_iter_t sortIter;
	BsonValueInitIterator(sortSpec, &sortIter);
	int i = 0;
	while (bson_iter_next(&sortIter))
	{
		pgbsonelement sortElement = { 0 };
		sortElement.path = bson_iter_key(&sortIter);
		sortElement.pathLength = strlen(sortElement.path);
		sortElement.bsonValue = *bson_iter_value(&sortIter);
		sortDatumArray[i] = PointerGetDatum(PgbsonElementToPgbson(&sortElement));
		i++;
	}

	ArrayType *arrayValue = construct_array(sortDatumArray, nelems, BsonTypeId(), -1,
											false, TYPALIGN_INT);

	Const *sortArrayConst = makeConst(GetBsonArrayTypeOid(), -1, InvalidOid, -1,
									  PointerGetDatum(arrayValue), false, false);

	/* Cast documentExpr from CoreSchemaName.bson to bson to ensure type */
	/* correctness for accumulators that require bson. */
	if (BsonTypeId() != DocumentDBCoreBsonTypeId())
	{
		documentExpr = (Expr *) makeRelabelType(documentExpr, BsonTypeId(), -1,
												InvalidOid,
												COERCE_IMPLICIT_CAST);
	}

	Aggref *aggref = CreateMultiArgAggregate(aggregateFunctionOid,
											 list_make2(documentExpr, sortArrayConst),
											 list_make2_oid(BsonTypeId(),
															sortArrayConst->consttype),
											 parseState);

	/* Now apply the bson_expression_get */
	Expr *constValue = (Expr *) MakeBsonConst(BsonValueToDocumentPgbson(
												  accumulatorValue));

	Const *trueConst = makeConst(BOOLOID, -1, InvalidOid, 1, BoolGetDatum(true), false,
								 true);
	List *groupArgs;
	Oid expressionGetFunction;

	if (variableSpec != NULL)
	{
		groupArgs = list_make4(aggref, constValue, trueConst, variableSpec);
		expressionGetFunction = BsonExpressionGetWithLetFunctionOid();
	}
	else
	{
		groupArgs = list_make3(aggref, constValue, trueConst);
		expressionGetFunction = BsonExpressionGetFunctionOid();
	}

	FuncExpr *accumFunc = makeFuncExpr(
		expressionGetFunction, BsonTypeId(), groupArgs, InvalidOid,
		InvalidOid, COERCE_EXPLICIT_CALL);

	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumulatorText,
														parseState, identifiers,
														query, TEXTOID, NULL));
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumFunc,
														parseState, identifiers,
														query, BsonTypeId(),
														NULL));
	return repathArgs;
}


/*
 * Add a sorted group accumulator e.g. BSONFIRSTN/BSONLASTN
 */
inline static List *
AddSortedNGroupAccumulator(Query *query, const bson_value_t *input,
						   bson_value_t *elementsToFetch,
						   List *repathArgs, Const *accumulatorText,
						   ParseState *parseState, char *identifiers,
						   Expr *documentExpr, Oid aggregateFunctionOid,
						   const bson_value_t *sortSpec, StringView *accumulatorName,
						   Expr *variableSpec)
{
	ValidateElementForNGroupAccumulators(elementsToFetch, accumulatorName->string);


	/* First apply the sorted agg (FIRSTN/LASTN) */
	int nelems = BsonDocumentValueCountKeys(sortSpec);
	Datum *sortDatumArray = palloc(sizeof(Datum) * nelems);

	bson_iter_t sortIter;
	BsonValueInitIterator(sortSpec, &sortIter);
	int i = 0;
	while (bson_iter_next(&sortIter))
	{
		pgbsonelement sortElement = { 0 };
		sortElement.path = bson_iter_key(&sortIter);
		sortElement.pathLength = strlen(sortElement.path);
		sortElement.bsonValue = *bson_iter_value(&sortIter);
		sortDatumArray[i] = PointerGetDatum(PgbsonElementToPgbson(&sortElement));
		i++;
	}

	Const *nConst = makeConst(INT8OID, -1, InvalidOid, sizeof(int64_t),
							  Int64GetDatum(elementsToFetch->value.v_int64), false, true);
	ArrayType *arrayValue = construct_array(sortDatumArray, nelems, BsonTypeId(), -1,
											false, TYPALIGN_INT);
	Const *sortArrayConst = makeConst(GetBsonArrayTypeOid(), -1, InvalidOid, -1,
									  PointerGetDatum(arrayValue), false, false);
	Aggref *aggref = CreateMultiArgAggregate(aggregateFunctionOid,
											 list_make3(documentExpr, nConst,
														sortArrayConst),
											 list_make3_oid(BsonTypeId(),
															nConst->consttype,
															sortArrayConst->consttype),
											 parseState);

	/* Now apply the bson_expression_map */
	Const *fieldConst = makeConst(TEXTOID, -1, InvalidOid, -1, CStringGetTextDatum(""),
								  false,
								  false);
	Expr *constValue = (Expr *) MakeBsonConst(BsonValueToDocumentPgbson(input));

	Const *trueConst = makeConst(BOOLOID, -1, InvalidOid, 1, BoolGetDatum(true), false,
								 true);
	List *groupArgs;
	Oid functionOid;

	if (variableSpec != NULL)
	{
		functionOid = BsonExpressionMapWithLetFunctionOid();
		groupArgs = list_make5(aggref, fieldConst, constValue, trueConst, variableSpec);
	}
	else
	{
		functionOid = BsonExpressionMapFunctionOid();
		groupArgs = list_make4(aggref, fieldConst, constValue, trueConst);
	}

	FuncExpr *accumFunc = makeFuncExpr(
		functionOid, BsonTypeId(), groupArgs, InvalidOid,
		InvalidOid, COERCE_EXPLICIT_CALL);

	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumulatorText,
														parseState, identifiers,
														query, TEXTOID, NULL));
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumFunc,
														parseState, identifiers,
														query, BsonTypeId(),
														NULL));
	return repathArgs;
}


/*
 * Function used to support maxN/minN accumulator.
 */
inline static List *
AddMaxMinNGroupAccumulator(Query *query, const bson_value_t *accumulatorValue,
						   List *repathArgs, Const *accumulatorText,
						   ParseState *parseState, char *identifiers,
						   Expr *documentExpr, Oid aggregateFunctionOid,
						   StringView *accumulatorName,
						   Expr *variableSpec)
{
	bson_value_t input = { 0 };
	bson_value_t elementsToFetch = { 0 };

	/*check the syntax of maxN/minN */
	ParseInputForNGroupAccumulators(accumulatorValue, &input, &elementsToFetch,
									accumulatorName->string);

	return AddSimpleGroupAccumulator(query, accumulatorValue, repathArgs, accumulatorText,
									 parseState, identifiers, documentExpr,
									 aggregateFunctionOid, variableSpec, NULL);
}


/*
 * Function used to support percentile/median accumulator.
 */
inline static List *
AddPercentileMedianGroupAccumulator(Query *query, const bson_value_t *accumulatorValue,
									List *repathArgs, Const *accumulatorText,
									ParseState *parseState, char *identifiers,
									Expr *documentExpr, StringView *accumulatorName,
									Expr *variableSpec, bool isMedianOp)
{
	bson_value_t input = { 0 };
	bson_value_t p = { 0 };
	bson_value_t method = { 0 };

	ParseInputDocumentForMedianAndPercentile(accumulatorValue, &input, &p, &method,
											 isMedianOp);

	/* construct expression to get input and p */
	List *inputFuncArgs;
	List *pFuncArgs;
	Oid bsonExpressionGetFunction;

	Expr *inputConstValue = (Expr *) MakeBsonConst(BsonValueToDocumentPgbson(&input));
	Const *accuracyConstValue = makeConst(INT4OID, -1, InvalidOid, sizeof(int32_t),
										  Int32GetDatum(TdigestCompressionAccuracy),
										  false, true);
	Expr *pConstValue = (Expr *) MakeBsonConst(BsonValueToDocumentPgbson(&p));
	Const *trueConst = makeConst(BOOLOID, -1, InvalidOid, 1, BoolGetDatum(true), false,
								 true);
	if (variableSpec != NULL)
	{
		bsonExpressionGetFunction = BsonExpressionGetWithLetFunctionOid();
		inputFuncArgs = list_make4(documentExpr, inputConstValue, trueConst,
								   variableSpec);
		pFuncArgs = list_make4(documentExpr, pConstValue, trueConst, variableSpec);
	}
	else
	{
		bsonExpressionGetFunction = BsonExpressionGetFunctionOid();
		inputFuncArgs = list_make3(documentExpr, inputConstValue, trueConst);
		pFuncArgs = list_make3(documentExpr, pConstValue, trueConst);
	}

	FuncExpr *inputAccumFunc = makeFuncExpr(bsonExpressionGetFunction, BsonTypeId(),
											inputFuncArgs, InvalidOid,
											InvalidOid, COERCE_EXPLICIT_CALL);
	FuncExpr *pAccumFunc = makeFuncExpr(bsonExpressionGetFunction, BsonTypeId(),
										pFuncArgs, InvalidOid,
										InvalidOid, COERCE_EXPLICIT_CALL);

	if (BsonTypeId() != DocumentDBCoreBsonTypeId())
	{
		inputAccumFunc = makeFuncExpr(
			DocumentDBCoreBsonToBsonFunctionOId(), BsonTypeId(), list_make1(
				inputAccumFunc),
			InvalidOid,
			InvalidOid, COERCE_EXPLICIT_CALL);
		pAccumFunc = makeFuncExpr(
			DocumentDBCoreBsonToBsonFunctionOId(), BsonTypeId(), list_make1(pAccumFunc),
			InvalidOid,
			InvalidOid, COERCE_EXPLICIT_CALL);
	}

	Oid aggregateFunctionOid = isMedianOp ? BsonMedianAggregateFunctionOid() :
							   BsonPercentileAggregateFunctionOid();
	Aggref *aggref = CreateMultiArgAggregate(aggregateFunctionOid, list_make3(
												 (Expr *) inputAccumFunc,
												 accuracyConstValue, (Expr *) pAccumFunc),
											 list_make3_oid(
												 BsonTypeId(),
												 accuracyConstValue->consttype,
												 BsonTypeId()), parseState);

	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) accumulatorText,
														parseState, identifiers, query,
														TEXTOID, NULL));

	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) aggref, parseState,
														identifiers, query, BsonTypeId(),
														NULL));

	return repathArgs;
}


/*
 * Handles the $group stage.
 * Creates a subquery.
 * Then creates a grouping specified by the _id expression.
 * Then creates accumulators in the projectors.
 *
 * The raw accumulators/group are in the first query
 * The aggregates are then pushed to a subquery and then an outer query
 * with the bson_repath_and_build is added as part of the group.
 * This is done because without this, sharded multi-node group by fails
 * due to a quirk in the distribution layer's worker query generation.
 *
 * TODO: Support n(elementsToFetch) as an expression for firstN, lastN, topN, bottomN
 */
static Query *
HandleGroupCore(const bson_value_t *existingValue, Query *query,
				AggregationPipelineBuildContext *context,
				const bson_value_t *accumulatorSortSpec)
{
	/*
	 * Collation is only supported with the new WithExpr accumulators, not for
	 * the grouping key itself. We check both helpers because their underlying
	 * SQL aggregate functions were introduced in different schema versions:
	 * min/max WithExpr in v110 and first/last WithExpr in v111. Either being
	 * available is sufficient to allow collation in $group with accumulators.
	 */
	if (IsCollationApplicable(context->collationString) &&
		!(EnableCollationWithNewGroupAccumulators &&
		  (CanUseWithExprMinMaxAggregates() || CanUseWithExprAggregates())))
	{
		ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						errmsg("collation is not supported in $group stage yet.")));
	}

	ReportFeatureUsage(FEATURE_STAGE_GROUP);

	/* Part 1, let's do the group */
	if (existingValue->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15947),
						errmsg(
							"The fields of a group must be explicitly defined within an object")));
	}

	/* Push prior stuff to a subquery first since we're gonna aggregate our way */
	if (list_length(query->targetList) > 1 || query->hasAggs ||
		list_length(query->groupClause) > 0 || list_length(query->sortClause) > 0)
	{
		query = MigrateQueryToSubQuery(query, context);
	}

	/* Take the current output (That's to be grouped)*/
	TargetEntry *origEntry = linitial(query->targetList);

	/* Clear the group's output */
	query->targetList = NIL;

	bson_iter_t groupIter;
	BsonValueInitIterator(existingValue, &groupIter);

	/* First get the _id */
	bson_value_t idValue = { 0 };
	bool idAlreadyFound = false;
	while (bson_iter_next(&groupIter))
	{
		StringView keyView = bson_iter_key_string_view(&groupIter);
		if (StringViewEquals(&keyView, &IdFieldStringView))
		{
			if (idAlreadyFound)
			{
				ReportFeatureUsage(FEATURE_STAGE_GROUP_DUPLICATE_ID);

				if (FailOnGroupIdDuplicate)
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15948),
									errmsg("a group's _id may only be specified once")));
				}

				break;
			}

			idValue = *bson_iter_value(&groupIter);
			idAlreadyFound = true;
		}
	}

	if (idValue.value_type == BSON_TYPE_EOD)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15955),
						errmsg("_id is missing from group specification")));
	}

	/* Group by is valid for pushdown iff it's a string expression of a path that's not a variable
	 * or a document whose fields are all $path expressions.
	 */
	bool isGroupByValidForIndexPushdown = false;
	StringView groupByFieldPaths[INDEX_MAX_KEYS] = { 0 };
	int numGroupByFields = 0;
	if (CanPushSortFilterToIndex(query, context))
	{
		/* TODO: Consider lifting the INDEX_MAX_KEYS cap -
		 * in the case where there are more than INDEX_MAX_KEYS fields, we
		 * can still do partial index pushdown on the first INDEX_MAX_KEYS
		 * fields. TryExtractGroupByFieldPaths currently bails entirely. */
		isGroupByValidForIndexPushdown =
			TryExtractGroupByFieldPaths(&idValue, groupByFieldPaths,
										INDEX_MAX_KEYS,
										&numGroupByFields);
	}

	ParseState *parseState = make_parsestate(NULL);
	parseState->p_next_resno = 1;
	parseState->p_expr_kind = EXPR_KIND_GROUP_BY;

	List *groupArgs;
	Oid bsonExpressionGetFunction;

	Expr *transformedGroupExpr = GetDocumentExprForGroupAccumulatorValue(&idValue,
																		 origEntry->expr);
	bool isGroupConstant = IsA(transformedGroupExpr, Const);

	/* Now do the projector / accumulators
	 * We do this in 2 stages to handle citus query generation.
	 * In the first phase we just project out the group and accumulators
	 * Identifiers are basically 'c' + some number or '?junk?' + some number.
	 * We set aside characters to "tostring" the number such that
	 * (c + <number> + \0). Note this is a temporary buffer - we
	 * pstrdup it after creating it.
	 */
	char identifiers[UINT32_MAX_STR_LEN + 128];

	/*
	 * For multi-field document _id with index pushdown, decompose into
	 * individual per-field GROUP BY expressions.
	 */
	bool canUseDecomposedGroupBy = isGroupByValidForIndexPushdown &&
								   idValue.value_type == BSON_TYPE_DOCUMENT &&
								   numGroupByFields > 1 &&
								   !isGroupConstant;
	if (canUseDecomposedGroupBy)
	{
		ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_DECOMPOSED_GROUP_BY);
	}

	bool useDecomposedGroupBy = canUseDecomposedGroupBy &&
								EnableGroupByCompoundIdIndexPushdown;

	Const *idFieldText = MakeTextConst("_id", 3);

	List *repathArgs = NIL;
	List *groupEntries = NIL;
	repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) idFieldText, parseState,
														identifiers, query, TEXTOID,
														NULL));

	/* Assign the _id clause as what we're grouping on. */
	if (useDecomposedGroupBy)
	{
		query->groupClause = NIL;
		List *buildDocumentArgs = NIL;

		bson_iter_t fieldIter;
		BsonValueInitIterator(&idValue, &fieldIter);
		int groupFieldNum = 0;
		while (bson_iter_next(&fieldIter))
		{
			const char *fieldName = bson_iter_key(&fieldIter);
			uint32_t fieldNameLen = bson_iter_key_len(&fieldIter);
			const bson_value_t *fieldVal = bson_iter_value(&fieldIter);
			pgbson *fieldGroupValue = BsonValueToDocumentPgbson(fieldVal);
			Expr *fieldDocExpr = GetDocumentExprForGroupAccumulatorValue(
				fieldVal, origEntry->expr);

			FuncExpr *fieldGroupFunc;
			if (context->variableSpec != NULL)
			{
				List *fieldArgs = list_make4(fieldDocExpr,
											 MakeBsonConst(fieldGroupValue),
											 MakeBoolValueConst(true),
											 context->variableSpec);
				fieldGroupFunc = makeFuncExpr(
					BsonExpressionGetWithLetFunctionOid(), BsonTypeId(),
					fieldArgs, InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
				if (IsDistributedShardContext(context) &&
					BsonTypeId() != DocumentDBCoreBsonTypeId())
				{
					fieldGroupFunc = makeFuncExpr(
						DocumentDBCoreBsonToBsonFunctionOId(), BsonTypeId(), list_make1(
							fieldGroupFunc),
						InvalidOid,
						InvalidOid, COERCE_EXPLICIT_CALL);
				}
			}
			else
			{
				List *fieldArgs = list_make3(fieldDocExpr,
											 MakeBsonConst(fieldGroupValue),
											 MakeBoolValueConst(true));
				fieldGroupFunc = makeFuncExpr(
					BsonExpressionGetFunctionOid(), BsonTypeId(),
					fieldArgs, InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
			}

			pg_sprintf(identifiers, "?junk?%d", groupFieldNum++);
			TargetEntry *fieldEntry = makeTargetEntry((Expr *) fieldGroupFunc, 0, pstrdup(
														  identifiers), true);
			groupEntries = lappend(groupEntries, fieldEntry);

			Const *fieldNameConst = MakeTextConst(fieldName, fieldNameLen);

			buildDocumentArgs = lappend(buildDocumentArgs, fieldNameConst);
			buildDocumentArgs = lappend(buildDocumentArgs, fieldGroupFunc);
		}

		FuncExpr *buildDocumentFunc = makeFuncExpr(
			BsonRepathAndBuildFunctionOid(), BsonTypeId(), buildDocumentArgs, InvalidOid,
			InvalidOid, COERCE_EXPLICIT_CALL);

		TargetEntry *groupEntry;
		repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) buildDocumentFunc,
															parseState,
															identifiers, query,
															BsonTypeId(),
															&groupEntry));
	}
	else
	{
		pgbson *groupValue = BsonValueToDocumentPgbson(&idValue);
		FuncExpr *groupFunc;
		if (context->variableSpec != NULL)
		{
			bsonExpressionGetFunction = BsonExpressionGetWithLetFunctionOid();
			groupArgs = list_make4(transformedGroupExpr, MakeBsonConst(groupValue),
								   MakeBoolValueConst(true), context->variableSpec);
			groupFunc = makeFuncExpr(
				bsonExpressionGetFunction, BsonTypeId(), groupArgs, InvalidOid,
				InvalidOid, COERCE_EXPLICIT_CALL);
			if (isGroupByValidForIndexPushdown && IsDistributedShardContext(context) &&
				BsonTypeId() != DocumentDBCoreBsonTypeId())
			{
				groupFunc = makeFuncExpr(
					DocumentDBCoreBsonToBsonFunctionOId(), BsonTypeId(), list_make1(
						groupFunc),
					InvalidOid,
					InvalidOid, COERCE_EXPLICIT_CALL);
			}
		}
		else
		{
			bsonExpressionGetFunction = BsonExpressionGetFunctionOid();
			groupArgs = list_make3(transformedGroupExpr, MakeBsonConst(groupValue),
								   MakeBoolValueConst(true));
			groupFunc = makeFuncExpr(
				bsonExpressionGetFunction, BsonTypeId(), groupArgs, InvalidOid,
				InvalidOid, COERCE_EXPLICIT_CALL);
		}

		TargetEntry *groupEntry;
		repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) groupFunc,
															parseState,
															identifiers, query,
															BsonTypeId(),
															&groupEntry));

		groupEntries = list_make1(groupEntry);
	}

	/* Now add accumulators */
	parseState->p_expr_kind = EXPR_KIND_SELECT_TARGET;
	BsonValueInitIterator(existingValue, &groupIter);
	while (bson_iter_next(&groupIter))
	{
		StringView keyView = bson_iter_key_string_view(&groupIter);
		if (StringViewEquals(&keyView, &IdFieldStringView))
		{
			continue;
		}

		if (StringViewContains(&keyView, '.'))
		{
			/* Paths here cannot be dotted paths */
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40235),
							errmsg(
								"The specified field name %.*s is not allowed to include the '.' character.",
								keyView.length, keyView.string)));
		}

		Const *accumulatorText = MakeTextConst(keyView.string, keyView.length);

		bson_iter_t accumulatorIterator;
		if (!BSON_ITER_HOLDS_DOCUMENT(&groupIter) ||
			!bson_iter_recurse(&groupIter, &accumulatorIterator))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40234),
							errmsg(
								"The field '%.*s' is required to be an accumulator-type object",
								keyView.length, keyView.string)));
		}

		pgbsonelement accumulatorElement;
		if (!TryGetSinglePgbsonElementFromBsonIterator(&accumulatorIterator,
													   &accumulatorElement))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40238),
							errmsg(
								"The field '%.*s' is required to define exactly one accumulator",
								keyView.length, keyView.string)));
		}

		StringView accumulatorName = {
			.length = accumulatorElement.pathLength, .string = accumulatorElement.path
		};
		if (StringViewEqualsCString(&accumulatorName, "$avg"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_AVG);

			if (CanUseWithExprAggregates())
			{
				repathArgs = AddSimpleGroupAccumulatorWithExpr(
					query, &accumulatorElement.bsonValue,
					repathArgs,
					accumulatorText, parseState,
					identifiers, origEntry->expr,
					BsonAvgWithExprAggregateFunctionOid(),
					context,
					NULL);
			}
			else
			{
				repathArgs = AddSimpleGroupAccumulator(query,
													   &accumulatorElement.bsonValue,
													   repathArgs,
													   accumulatorText, parseState,
													   identifiers,
													   origEntry->expr,
													   BsonAvgAggregateFunctionOid(),
													   context->variableSpec,
													   NULL);
			}
		}
		else if (StringViewEqualsCString(&accumulatorName, "$sum"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_SUM);
			repathArgs = AddSumGroupAccumulator(query, &accumulatorElement.bsonValue,
												repathArgs,
												accumulatorText, parseState,
												identifiers,
												origEntry->expr,
												isGroupConstant,
												context);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$max"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_MAX);

			if (CanUseWithExprMinMaxAggregates())
			{
				repathArgs = AddSimpleGroupAccumulatorWithExpr(
					query, &accumulatorElement.bsonValue,
					repathArgs,
					accumulatorText, parseState,
					identifiers, origEntry->expr,
					BsonMaxWithExprAggregateFunctionOid(),
					context,
					NULL);
			}
			else
			{
				repathArgs = AddSimpleGroupAccumulator(query,
													   &accumulatorElement.bsonValue,
													   repathArgs,
													   accumulatorText, parseState,
													   identifiers,
													   origEntry->expr,
													   BsonMaxAggregateFunctionOid(),
													   context->variableSpec,
													   NULL);
			}
		}
		else if (StringViewEqualsCString(&accumulatorName, "$min"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_MIN);

			if (CanUseWithExprMinMaxAggregates())
			{
				repathArgs = AddSimpleGroupAccumulatorWithExpr(
					query, &accumulatorElement.bsonValue,
					repathArgs,
					accumulatorText, parseState,
					identifiers, origEntry->expr,
					BsonMinWithExprAggregateFunctionOid(),
					context,
					NULL);
			}
			else
			{
				repathArgs = AddSimpleGroupAccumulator(query,
													   &accumulatorElement.bsonValue,
													   repathArgs,
													   accumulatorText, parseState,
													   identifiers,
													   origEntry->expr,
													   BsonMinAggregateFunctionOid(),
													   context->variableSpec,
													   NULL);
			}
		}
		else if (StringViewEqualsCString(&accumulatorName, "$count"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_COUNT);

			if (!IsBsonValueEmptyDocument(&accumulatorElement.bsonValue))
			{
				ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_COUNT_WITH_ARG);

				if (FailOnNonEmptyGroupCountArg)
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_TYPEMISMATCH),
									errmsg(
										"$count:{} accumulator requires 0 arguments")));
				}
			}

			/* Use the new BSONCOUNT aggregate. */
			Expr *constValue = (Expr *) makeConst(INT4OID, -1, InvalidOid, 4,
												  Int32GetDatum(1), false, true);
			Aggref *aggref = CreateSingleArgAggregate(
				BsonCountAggregateFunctionOid(),
				constValue, parseState);

			repathArgs = lappend(repathArgs, AddGroupExpression(
									 (Expr *) accumulatorText,
									 parseState,
									 identifiers,
									 query, TEXTOID,
									 NULL));
			repathArgs = lappend(repathArgs, AddGroupExpression((Expr *) aggref,
																parseState,
																identifiers,
																query, BsonTypeId(),
																NULL));
		}
		else if (StringViewEqualsCString(&accumulatorName, "$first"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_FIRST);
			if (context->sortSpec.value_type == BSON_TYPE_EOD ||
				accumulatorSortSpec != NULL)
			{
				TargetEntry *accumulatorTle = NULL;
				if (CanUseWithExprAggregates())
				{
					repathArgs = AddSimpleGroupAccumulatorWithExpr(
						query, &accumulatorElement.bsonValue,
						repathArgs,
						accumulatorText, parseState,
						identifiers, origEntry->expr,
						BsonFirstWithExprAggregateFunctionOid(),
						context,
						&accumulatorTle);
				}
				else
				{
					repathArgs = AddSimpleGroupAccumulator(query,
														   &accumulatorElement.bsonValue,
														   repathArgs,
														   accumulatorText, parseState,
														   identifiers,
														   origEntry->expr,
														   BsonFirstOnSortedAggregateFunctionOid(),
														   context->variableSpec,
														   &accumulatorTle);
				}

				if (accumulatorSortSpec != NULL)
				{
					SetAccumulatorSortOrder(accumulatorTle, origEntry->expr,
											accumulatorSortSpec,
											context->collationString);
				}
			}
			else
			{
				repathArgs = AddSortedGroupAccumulator(query,
													   &accumulatorElement.bsonValue,
													   repathArgs,
													   accumulatorText, parseState,
													   identifiers,
													   origEntry->expr,
													   BsonFirstAggregateFunctionOid(),
													   &context->sortSpec,
													   context->variableSpec);
			}
		}
		else if (StringViewEqualsCString(&accumulatorName, "$last"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_LAST);
			if (context->sortSpec.value_type == BSON_TYPE_EOD)
			{
				if (CanUseWithExprAggregates())
				{
					repathArgs = AddSimpleGroupAccumulatorWithExpr(
						query, &accumulatorElement.bsonValue,
						repathArgs,
						accumulatorText, parseState,
						identifiers, origEntry->expr,
						BsonLastWithExprAggregateFunctionOid(),
						context,
						NULL);
				}
				else
				{
					repathArgs = AddSimpleGroupAccumulator(query,
														   &accumulatorElement.bsonValue,
														   repathArgs,
														   accumulatorText, parseState,
														   identifiers,
														   origEntry->expr,
														   BsonLastOnSortedAggregateFunctionOid(),
														   context->variableSpec,
														   NULL);
				}
			}
			else
			{
				repathArgs = AddSortedGroupAccumulator(query,
													   &accumulatorElement.bsonValue,
													   repathArgs,
													   accumulatorText, parseState,
													   identifiers,
													   origEntry->expr,
													   BsonLastAggregateFunctionOid(),
													   &context->sortSpec,
													   context->variableSpec);
			}
		}
		else if (StringViewEqualsCString(&accumulatorName, "$firstN"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_FIRST_N);
			bson_value_t input = { 0 };
			bson_value_t elementsToFetch = { 0 };
			ParseInputForNGroupAccumulators(&accumulatorElement.bsonValue, &input,
											&elementsToFetch, accumulatorName.string);
			if (context->sortSpec.value_type == BSON_TYPE_EOD)
			{
				repathArgs = AddSimpleNGroupAccumulator(query,
														&input,
														&elementsToFetch,
														repathArgs,
														accumulatorText, parseState,
														identifiers,
														origEntry->expr,
														BsonFirstNOnSortedAggregateFunctionOid(),
														&accumulatorName,
														context->variableSpec);
			}
			else
			{
				repathArgs = AddSortedNGroupAccumulator(query,
														&input,
														&elementsToFetch,
														repathArgs,
														accumulatorText, parseState,
														identifiers,
														origEntry->expr,
														BsonFirstNAggregateFunctionOid(),
														&context->sortSpec,
														&accumulatorName,
														context->variableSpec);
			}
		}
		else if (StringViewEqualsCString(&accumulatorName, "$lastN"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_LAST_N);
			bson_value_t input = { 0 };
			bson_value_t elementsToFetch = { 0 };
			ParseInputForNGroupAccumulators(&accumulatorElement.bsonValue, &input,
											&elementsToFetch, accumulatorName.string);
			if (context->sortSpec.value_type == BSON_TYPE_EOD)
			{
				repathArgs = AddSimpleNGroupAccumulator(query,
														&input,
														&elementsToFetch,
														repathArgs,
														accumulatorText, parseState,
														identifiers,
														origEntry->expr,
														BsonLastNOnSortedAggregateFunctionOid(),
														&accumulatorName,
														context->variableSpec);
			}
			else
			{
				repathArgs = AddSortedNGroupAccumulator(query,
														&input,
														&elementsToFetch,
														repathArgs,
														accumulatorText, parseState,
														identifiers,
														origEntry->expr,
														BsonLastNAggregateFunctionOid(),
														&context->sortSpec,
														&accumulatorName,
														context->variableSpec);
			}
		}
		else if (StringViewEqualsCString(&accumulatorName, "$maxN"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_MAX_N);
			repathArgs = AddMaxMinNGroupAccumulator(query,
													&accumulatorElement.bsonValue,
													repathArgs,
													accumulatorText, parseState,
													identifiers,
													origEntry->expr,
													BsonMaxNAggregateFunctionOid(),
													&accumulatorName,
													context->variableSpec);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$minN"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_MIN_N);
			repathArgs = AddMaxMinNGroupAccumulator(query,
													&accumulatorElement.bsonValue,
													repathArgs,
													accumulatorText, parseState,
													identifiers,
													origEntry->expr,
													BsonMinNAggregateFunctionOid(),
													&accumulatorName,
													context->variableSpec);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$addToSet"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_ADD_TO_SET);
			repathArgs = AddSimpleGroupAccumulator(query,
												   &accumulatorElement.bsonValue,
												   repathArgs,
												   accumulatorText, parseState,
												   identifiers,
												   origEntry->expr,
												   BsonAddToSetAggregateFunctionOid(),
												   context->variableSpec,
												   NULL);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$mergeObjects"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_MERGE_OBJECTS);
			if (context->sortSpec.value_type == BSON_TYPE_EOD)
			{
				repathArgs = AddSimpleGroupAccumulator(query,
													   &accumulatorElement.bsonValue,
													   repathArgs,
													   accumulatorText, parseState,
													   identifiers,
													   origEntry->expr,
													   BsonMergeObjectsOnSortedFunctionOid(),
													   context->variableSpec,
													   NULL);
			}
			else
			{
				repathArgs = AddMergeObjectsGroupAccumulator(query,
															 &accumulatorElement.
															 bsonValue,
															 repathArgs,
															 accumulatorText,
															 parseState,
															 identifiers,
															 origEntry->expr,
															 &context->sortSpec,
															 context->variableSpec);
			}
		}
		else if (StringViewEqualsCString(&accumulatorName, "$push"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_PUSH);
			char *fieldPath = "";
			bool handleSingleValue = true;
			repathArgs = AddArrayAggGroupAccumulator(query,
													 &accumulatorElement.bsonValue,
													 repathArgs,
													 accumulatorText, parseState,
													 identifiers,
													 origEntry->expr,
													 BsonArrayAggregateAllArgsFunctionOid(),
													 fieldPath,
													 handleSingleValue,
													 context->variableSpec);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$stdDevSamp"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_STDDEV_SAMP);
			if (accumulatorElement.bsonValue.value_type == BSON_TYPE_ARRAY)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40237), errmsg(
									"The %s accumulator functions as a single-operand operator",
									accumulatorName.string)),
						errdetail_log(
							"The %s accumulator functions as a single-operand operator",
							accumulatorName.string));
			}

			repathArgs = AddSimpleGroupAccumulator(query,
												   &accumulatorElement.bsonValue,
												   repathArgs,
												   accumulatorText, parseState,
												   identifiers,
												   origEntry->expr,
												   BsonStdDevSampAggregateFunctionOid(),
												   context->variableSpec,
												   NULL);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$stdDevPop"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_STDDEV_POP);
			if (accumulatorElement.bsonValue.value_type == BSON_TYPE_ARRAY)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40237), errmsg(
									"The %s accumulator functions as a single-operand operator",
									accumulatorName.string)),
						errdetail_log(
							"The %s accumulator functions as a single-operand operator",
							accumulatorName.string));
			}

			repathArgs = AddSimpleGroupAccumulator(query,
												   &accumulatorElement.bsonValue,
												   repathArgs,
												   accumulatorText, parseState,
												   identifiers,
												   origEntry->expr,
												   BsonStdDevPopAggregateFunctionOid(),
												   context->variableSpec,
												   NULL);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$top"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_TOP);
			bson_value_t output = { 0 };
			bson_value_t elementsToFetch = { 0 };
			bson_value_t sortSpec = { 0 };
			ParseInputDocumentForTopAndBottom(&accumulatorElement.bsonValue, &output,
											  &elementsToFetch,
											  &sortSpec,
											  accumulatorName.string);
			repathArgs = AddSortedGroupAccumulator(query,
												   &output,
												   repathArgs,
												   accumulatorText, parseState,
												   identifiers,
												   origEntry->expr,
												   BsonFirstAggregateFunctionOid(),
												   &sortSpec,
												   context->variableSpec);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$bottom"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_BOTTOM);
			bson_value_t output = { 0 };
			bson_value_t elementsToFetch = { 0 };
			bson_value_t sortSpec = { 0 };
			ParseInputDocumentForTopAndBottom(&accumulatorElement.bsonValue, &output,
											  &elementsToFetch,
											  &sortSpec,
											  accumulatorName.string);
			repathArgs = AddSortedGroupAccumulator(query,
												   &output,
												   repathArgs,
												   accumulatorText, parseState,
												   identifiers,
												   origEntry->expr,
												   BsonLastAggregateFunctionOid(),
												   &sortSpec,
												   context->variableSpec);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$topN"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_TOP_N);

			/* Parse accumulatorValue to pull output, n and sortBy*/
			bson_value_t input = { 0 };
			bson_value_t elementsToFetch = { 0 };
			bson_value_t sortSpec = { 0 };

			ParseInputDocumentForTopAndBottom(&accumulatorElement.bsonValue, &input,
											  &elementsToFetch, &sortSpec,
											  accumulatorName.string);
			repathArgs = AddSortedNGroupAccumulator(query,
													&input,
													&elementsToFetch,
													repathArgs,
													accumulatorText, parseState,
													identifiers,
													origEntry->expr,
													BsonFirstNAggregateFunctionOid(),
													&sortSpec,
													&accumulatorName,
													context->variableSpec);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$bottomN"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_BOTTOM_N);

			/* Parse accumulatorValue to pull output, n and sortBy*/
			bson_value_t input = { 0 };
			bson_value_t elementsToFetch = { 0 };
			bson_value_t sortSpec = { 0 };

			ParseInputDocumentForTopAndBottom(&accumulatorElement.bsonValue, &input,
											  &elementsToFetch, &sortSpec,
											  accumulatorName.string);
			repathArgs = AddSortedNGroupAccumulator(query,
													&input,
													&elementsToFetch,
													repathArgs,
													accumulatorText, parseState,
													identifiers,
													origEntry->expr,
													BsonLastNAggregateFunctionOid(),
													&sortSpec,
													&accumulatorName,
													context->variableSpec);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$median"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_MEDIAN);
			repathArgs = AddPercentileMedianGroupAccumulator(query,
															 &accumulatorElement.bsonValue,
															 repathArgs,
															 accumulatorText, parseState,
															 identifiers,
															 origEntry->expr,
															 &accumulatorName,
															 context->variableSpec, true);
		}
		else if (StringViewEqualsCString(&accumulatorName, "$percentile"))
		{
			ReportFeatureUsage(FEATURE_AGGREGATE_GROUP_PERCENTILE);
			repathArgs = AddPercentileMedianGroupAccumulator(query,
															 &accumulatorElement.bsonValue,
															 repathArgs,
															 accumulatorText, parseState,
															 identifiers,
															 origEntry->expr,
															 &accumulatorName,
															 context->variableSpec,
															 false);
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15952),
							errmsg("Unrecognized group operator %s",
								   accumulatorElement.path),
							errdetail_log("Unrecognized group operator %s",
										  accumulatorElement.path)));
		}
	}

	/*
	 * If there's an orderby pushdown to the index, add a full scan clause iff
	 * the query has no filters yet.
	 */
	if (isGroupByValidForIndexPushdown && (useDecomposedGroupBy || numGroupByFields == 1))
	{
		List *currentQuals = make_ands_implicit(
			(Expr *) query->jointree->quals);

		bool addedShardKeyFilter = false;
		for (int i = 0; i < numGroupByFields; i++)
		{
			pgbsonelement sortElement = { 0 };
			sortElement.path = groupByFieldPaths[i].string;
			sortElement.pathLength = groupByFieldPaths[i].length;
			sortElement.bsonValue.value_type = BSON_TYPE_INT32;
			sortElement.bsonValue.value.v_int32 = 1;

			currentQuals = AddFullScanQualsToJoinTreeForOrderByPushdown(
				currentQuals, origEntry->expr, &sortElement,
				context, &addedShardKeyFilter);
		}

		/* When the group-by keys form a prefix of the sort spec, the suffix
		 * keys (those not covered by group-by) are pushed into the
		 * accumulator's ORDER BY instead of a full $sort stage.  Add fullScan
		 * quals for these suffix keys so the planner can consider index
		 * pushdown on them.
		 * We don't do this for Pg15 as it does not push ordered aggregates to the index.
		 * Had we gone forward in pg15, we would have fewer pathkeys than sort/orderby columns
		 * which causes a crash in ProcessOrderByStatements(index_support.c).
		 */
		if (accumulatorSortSpec != NULL && PG_VERSION_NUM >= 160000)
		{
			bson_iter_t suffixSortIter;
			BsonValueInitIterator(accumulatorSortSpec, &suffixSortIter);

			while (bson_iter_next(&suffixSortIter))
			{
				pgbsonelement suffixSortElement;
				BsonIterToPgbsonElement(&suffixSortIter, &suffixSortElement);

				currentQuals = AddFullScanQualsToJoinTreeForOrderByPushdown(
					currentQuals, origEntry->expr, &suffixSortElement,
					context, &addedShardKeyFilter);
			}
		}

		query->jointree->quals = (Node *) make_ands_explicit(
			currentQuals);
	}

	/*
	 * TODO: Remove this guard once distributed decomposition handles inlined
	 * target lists correctly.
	 *
	 * Skip for sharded + non-constant _id: distributed query decomposition
	 * cannot handle the inlined bson_repath_and_build target list and breaks
	 * the GROUP BY contract. Constant _id is safe since the group key has no
	 * document column references.
	 */
	bool isSharded = (context->mongoCollection != NULL &&
					  context->mongoCollection->shardKey != NULL);

	bool canEliminateSubquery = ForceGroupSubqueryElimination ||
								(!isSharded || isGroupConstant);

	if (canEliminateSubquery)
	{
		/* Collect the actual expressions from the target entries */
		List *inlineRepathArgs = NIL;
		ListCell *cell;
		foreach(cell, query->targetList)
		{
			TargetEntry *entry = lfirst(cell);
			if (entry->resjunk)
			{
				continue;
			}

			inlineRepathArgs = lappend(inlineRepathArgs, entry->expr);
		}

		/* $group doesn't allow dotted path, no array override needed */
		bool overrideArrayInProjection = false;
		Expr *repathExpression = GenerateMultiExpressionRepathExpression(
			inlineRepathArgs, overrideArrayInProjection);

		/*
		 * Add each GROUP BY entries as resjunk and add a corresponding
		 * SortGroupClause for them.
		 */
		TargetEntry *repathEntry = makeTargetEntry(repathExpression, 1,
												   origEntry->resname, false);
		query->targetList = list_make1(repathEntry);
		ListCell *groupEntryCell;
		int nextResNo = 2;
		foreach(groupEntryCell, groupEntries)
		{
			TargetEntry *groupEntry = lfirst(groupEntryCell);
			if (!groupEntry->resjunk)
			{
				groupEntry->resjunk = true;
			}

			groupEntry->resno = nextResNo++;
			query->targetList = lappend(query->targetList, groupEntry);

			/* Assign the _id clause as what we're grouping on */
			SortGroupClause *grpcl = makeNode(SortGroupClause);
			grpcl->tleSortGroupRef = assignSortGroupRef(groupEntry, query->targetList);
			grpcl->eqop = BsonEqualOperatorId();
			grpcl->sortop = BsonLessThanOperatorId();
			grpcl->nulls_first = false; /* OK with or without sortop */
			grpcl->hashable = true;
			query->groupClause = lappend(query->groupClause, grpcl);
		}
	}
	else
	{
		ListCell *groupEntryCell;
		foreach(groupEntryCell, groupEntries)
		{
			TargetEntry *groupEntry = lfirst(groupEntryCell);

			if (groupEntry->resjunk)
			{
				groupEntry->resno = parseState->p_next_resno++;
				query->targetList = lappend(query->targetList, groupEntry);
			}

			/* Assign the _id clause as what we're grouping on */
			SortGroupClause *grpcl = makeNode(SortGroupClause);
			grpcl->tleSortGroupRef = assignSortGroupRef(groupEntry, query->targetList);
			grpcl->eqop = BsonEqualOperatorId();
			grpcl->sortop = BsonLessThanOperatorId();
			grpcl->nulls_first = false; /* OK with or without sortop */
			grpcl->hashable = true;
			query->groupClause = lappend(query->groupClause, grpcl);
		}

		/* Legacy path: push to a subquery, then apply repath on the outer query */
		context->expandTargetList = true;
		query = MigrateQueryToSubQuery(query, context);

		/* Take the output and replace it with the repath_and_build */
		TargetEntry *entry = linitial(query->targetList);

		/* $group doesn't allow dotted path so no need to override */
		bool overrideArrayInProjection = false;
		Expr *repathExpression = GenerateMultiExpressionRepathExpression(
			repathArgs, overrideArrayInProjection);

		entry->expr = repathExpression;
		entry->resname = origEntry->resname;
	}

	/* Mark new stages to push a new subquery */
	context->requiresSubQuery = true;
	return query;
}


/*
 * Main entry point for handling $group stage. This function will rewrite the
 * given query to perform the aggregation specified by the $group stage.
 */
Query *
HandleGroup(const bson_value_t *existingValue, Query *query,
			AggregationPipelineBuildContext *context)
{
	return HandleGroupCore(existingValue, query, context,
						   NULL);
}


/*
 * Pushes the current query into a subquery.
 * Then creates a brand new query that projects the 'document' value
 * from the underlying subquery.
 */
Query *
MigrateQueryToSubQuery(Query *parse, AggregationPipelineBuildContext *context)
{
	context->numNestedLevels++;

	TargetEntry *tle = (TargetEntry *) linitial(parse->targetList);
	RangeTblEntry *rte = MakeSubQueryRte(parse, context->stageNum,
										 context->nestedPipelineLevel,
										 "agg", context->expandTargetList);
	context->expandTargetList = false;

	Index childIndex = 1;
	Var *newQueryOutput = makeVar(childIndex, tle->resno, BsonTypeId(), -1, InvalidOid,
								  0);
	TargetEntry *upperEntry = makeTargetEntry((Expr *) newQueryOutput, 1, tle->resname,
											  tle->resjunk);


	Query *newquery = makeNode(Query);
	newquery->commandType = CMD_SELECT;
	newquery->querySource = parse->querySource;
	newquery->canSetTag = true;
	newquery->targetList = list_make1(upperEntry);
	newquery->rtable = list_make1(rte);

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

	context->requiresSubQuery = false;
	return newquery;
}


/*
 * Creates an RTE for a Subquery provided given the prefixed stage name
 */
RangeTblEntry *
MakeSubQueryRte(Query *subQuery, int stageNum, int pipelineDepth,
				const char *prefix, bool includeAllColumns)
{
	RangeTblEntry *rte = makeNode(RangeTblEntry);
	rte->rtekind = RTE_SUBQUERY;
	rte->subquery = subQuery;
	rte->lateral = false;
	rte->inh = false;
	rte->inFromCl = true;

	StringInfo s = makeStringInfo();
	if (pipelineDepth > 0)
	{
		appendStringInfo(s, "%s_stage_sub_%d_%d", prefix, pipelineDepth, stageNum);
	}
	else
	{
		appendStringInfo(s, "%s_stage_%d", prefix, stageNum);
	}

	rte->alias = makeAlias(s->data, NIL);
	if (includeAllColumns)
	{
		ListCell *cell;
		List *colnames = NIL;
		foreach(cell, subQuery->targetList)
		{
			TargetEntry *tle = lfirst(cell);

			/* append only non-junk columns */
			if (!tle->resjunk)
			{
				colnames = lappend(colnames, makeString(tle->resname ? tle->resname :
														""));
			}
		}

		rte->eref = makeAlias(s->data, colnames);
	}
	else
	{
		TargetEntry *tle = linitial(subQuery->targetList);
		List *colnames = list_make1(makeString(tle->resname ? tle->resname : ""));
		rte->eref = makeAlias(s->data, colnames);
	}

	return rte;
}


/*
 * Checks if partitionByFields expression of $densify stage is on the shard key
 * of the collection
 *
 * `partitionByFields`: { "": ["a", "b", "c"]}
 * `shardkey`: {"a": "hashed", "b": "hashed", "c": "hashed"}
 *
 * These 2 are same
 */
bool
IsPartitionByFieldsOnShardKey(const pgbson *partitionByFields, const
							  MongoCollection *collection)
{
	if (collection == NULL || collection->shardKey == NULL || partitionByFields == NULL)
	{
		return false;
	}

	pgbson_writer shardKeyWriter;
	pgbson_array_writer arrayWriter;
	PgbsonWriterInit(&shardKeyWriter);
	PgbsonWriterStartArray(&shardKeyWriter, "", 0, &arrayWriter);

	bson_iter_t shardKeyIter;
	PgbsonInitIterator(collection->shardKey, &shardKeyIter);
	while (bson_iter_next(&shardKeyIter))
	{
		PgbsonArrayWriterWriteUtf8(&arrayWriter, bson_iter_key(&shardKeyIter));
	}
	PgbsonWriterEndArray(&shardKeyWriter, &arrayWriter);

	if (PgbsonEquals(PgbsonWriterGetPgbson(&shardKeyWriter), partitionByFields))
	{
		return true;
	}

	return false;
}


/*
 * A simple function that never requires persistent cursors
 */
static bool
RequiresPersistentCursorFalse(const bson_value_t *pipelineValue, bool *isSingleRowResult)
{
	/* This path doesn't consider singleRow - defers to other stages but doesn't exclude it */
	return false;
}


static bool
RequiresPersistentCursorFalseNoSingleRow(const bson_value_t *pipelineValue,
										 bool *isSingleRowResult)
{
	/* This path doesn't consider single row output but excludes singleBatch */
	*isSingleRowResult = false;
	return false;
}


/*
 * A simple function that always requires persistent cursors
 */
static bool
RequiresPersistentCursorTrue(const bson_value_t *pipelineValue, bool *isSingleRowResult)
{
	/* A persisted cursor for now assumes multi-row */
	*isSingleRowResult = false;
	return true;
}


static bool
RequiresPersistentCursorTrueSingleRow(const bson_value_t *pipelineValue,
									  bool *isSingleRowResult)
{
	/* A persisted cursor that produces a single row response */
	*isSingleRowResult = true;
	return true;
}


/*
 * Checks that the limit stage requires persistence (true if it's not limit 1).
 */
static bool
RequiresPersistentCursorLimit(const bson_value_t *pipelineValue, bool *isSingleRowResult)
{
	if (pipelineValue->value_type != BSON_TYPE_EOD &&
		BsonValueIsNumber(pipelineValue))
	{
		int32_t limit = BsonValueAsInt32(pipelineValue);
		if (limit == 1)
		{
			/* For special case limit 1 - this can be a singleBatch cursor
			 * instead of streaming.
			 */
			*isSingleRowResult = true;
			return false;
		}

		/* Defer to prior */
		return limit != 1 && limit != 0;
	}

	return pipelineValue->value_type != BSON_TYPE_EOD;
}


/*
 * Checks that the skip stage requires persistence (true if it's not skip 0).
 */
static bool
RequiresPersistentCursorSkip(const bson_value_t *pipelineValue, bool *isSingleRowResult)
{
	/* Skip doesn't make any judgements on singleRowResults */
	if (pipelineValue->value_type != BSON_TYPE_EOD &&
		BsonValueIsNumber(pipelineValue))
	{
		int32_t skip = BsonValueAsInt32(pipelineValue);
		return skip != 0;
	}

	return pipelineValue->value_type != BSON_TYPE_EOD;
}


/*
 * Given a database, and a view definition, recursively extracts the
 * views pointed to by the view definition until a base collection (or non-existent)
 * collection is found. Also appends the pipeline stages associated with it to the
 * List of pipeline stages.
 */
static MongoCollection *
ExtractViewDefinitionAndPipeline(Datum databaseDatum, pgbson *viewDefinition,
								 List **pipelineStages,
								 AggregationPipelineBuildContext *context)
{
	int viewDepth = 0;
	bool canViewUseShardTable = context->allowShardBaseTable;
	while (true)
	{
		CHECK_FOR_INTERRUPTS();
		check_stack_depth();

		if (viewDepth > MAX_VIEW_DEPTH)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_VIEWDEPTHLIMITEXCEEDED),
							errmsg("View depth exceeded limit %d", MAX_VIEW_DEPTH)));
		}

		viewDepth++;
		ViewDefinition definition = { 0 };
		DecomposeViewDefinition(viewDefinition, &definition);

		if (definition.pipeline.value_type != BSON_TYPE_EOD)
		{
			bson_value_t *valueCopy = palloc(sizeof(bson_value_t));
			*valueCopy = definition.pipeline;
			List *stages = ExtractAggregationStages(valueCopy, context);
			if (!context->allowShardBaseTable)
			{
				/* If any nested view uses stage that can't use shard base table then
				 * we will not use shard base tables.
				 */
				canViewUseShardTable = context->allowShardBaseTable;
			}
			*pipelineStages = lappend(*pipelineStages, stages);
		}

		MongoCollection *collection = GetMongoCollectionOrViewByNameDatum(
			databaseDatum, CStringGetTextDatum(definition.viewSource), NoLock);
		if (collection == NULL)
		{
			/* It's a non-existent table - we can stop the search */
			return collection;
		}
		else if (collection->viewDefinition == NULL)
		{
			/* It's a base table - lock the table and stop the search */
			GetRelationIdForCollectionId(collection->collectionId, AccessShareLock);
			context->allowShardBaseTable = canViewUseShardTable;
			return collection;
		}
		else
		{
			/* It's a view redo the check */
			viewDefinition = collection->viewDefinition;
		}
	}
}


/*
 * Given the pipeline definition extract the stages as a list of `AggregationStage`.
 * Performs basic validation in the structure of the pipeline.
 * If `context->optimizePipelines` is set to true, the function will optimize the pipelines.
 */
List *
ExtractAggregationStages(const bson_value_t *pipelineValue,
						 AggregationPipelineBuildContext *context)
{
	if (pipelineValue->value_type != BSON_TYPE_ARRAY ||
		IsBsonValueEmptyArray(pipelineValue))
	{
		return NIL;
	}

	bson_iter_t pipelineIterator;
	BsonValueInitIterator(pipelineValue, &pipelineIterator);

	const char *lastEncounteredOutputStage = NULL;

	List *aggregationStages = NIL;
	while (bson_iter_next(&pipelineIterator))
	{
		bson_iter_t documentIterator;
		if (!BSON_ITER_HOLDS_DOCUMENT(&pipelineIterator) ||
			!bson_iter_recurse(&pipelineIterator, &documentIterator))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_TYPEMISMATCH),
							errmsg(
								"Every item within the 'pipeline' array is required to be an object.")));
		}

		pgbsonelement stageElement;
		if (!TryGetSinglePgbsonElementFromBsonIterator(&documentIterator, &stageElement))
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40323),
							errmsg(
								"A pipeline stage specification object is required to have one and only one field.")));
		}

		/* If lastEncounteredOutputStage isn't NULL, it means we've seen an output stage like $out or $merge before this.
		 * Since the output stage was expected to be last, encountering it earlier leads to failure. */
		if (lastEncounteredOutputStage != NULL)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION40601),
							errmsg(
								"%s must appear exclusively as the last stage in the pipeline",
								lastEncounteredOutputStage),
							errdetail_log(
								"%s must appear exclusively as the last stage in the pipeline",
								lastEncounteredOutputStage)));
		}

		/* Get the definition of the stage */
		const AggregationStageDefinition *definition = bsearch(
			stageElement.path, StageDefinitions,
			AggregationStageCount,
			sizeof(AggregationStageDefinition),
			CompareStageByStageName);
		if (definition == NULL)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_UNRECOGNIZEDCOMMAND),
							errmsg("Pipeline stage name not recognized: %s",
								   stageElement.path),
							errdetail_log("Pipeline stage name not recognized: %s",
										  stageElement.path)));
		}
		if (definition->pipelineCheckFunc != NULL)
		{
			definition->pipelineCheckFunc(pipelineValue, context);
		}

		if (definition->mutateFunc == NULL)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_COMMANDNOTSUPPORTED),
							errmsg(
								"Stage %s is not supported yet in native pipeline",
								definition->stage),
							errdetail_log(
								"Stage %s is not supported yet in native pipeline",
								definition->stage)));
		}

		if (definition->isOutputStage)
		{
			lastEncounteredOutputStage = definition->stage;
		}

		if (definition->isMultiJoinUnionStage)
		{
			context->joinStatus = JoinStageStatus_HasJoinsOrUnions;
		}

		if (definition->stageEnum == Stage_ChangeStream)
		{
			context->requiresTailableCursor = true;
		}

		AggregationStage *stage = palloc0(sizeof(AggregationStage));
		stage->stageDefinition = definition;
		stage->stageValue = stageElement.bsonValue;

		aggregationStages = lappend(aggregationStages, stage);
	}

	/*
	 * Validate number of stages in the pipeline, we purposefully do it here because
	 * it saves a few CPU cycles to not check the length everytime for each stage
	 *
	 * But, because of this large pipelines will not fail as soon as they exceed the limit but eventually
	 * fail. This is a tradeoff for perf b/w most of the valid cases v/s the rare invalid ones.
	 */
	CheckMaxAllowedAggregationStages(list_length(aggregationStages));

	if (context->optimizePipelineStages)
	{
		aggregationStages = TryOptimizeAggregationPipelines(aggregationStages, context);
	}

	if (context->joinStatus == JoinStageStatus_Unknown)
	{
		context->joinStatus = JoinStageStatus_NoJoinsOrUnions;
	}

	return aggregationStages;
}


/*
 * Updates the base table
 */
Query *
GenerateBaseTableQuery(text *databaseDatum, const StringView *collectionNameView,
					   pg_uuid_t *collectionUuid, const bson_value_t *indexHint,
					   AggregationPipelineBuildContext *context)
{
	Query *query = makeNode(Query);
	query->commandType = CMD_SELECT;
	query->querySource = QSRC_ORIGINAL;
	query->canSetTag = true;
	context->collectionNameView = *collectionNameView;
	context->namespaceName = CreateNamespaceName(databaseDatum,
												 collectionNameView);
	Datum collectionNameDatum = PointerGetDatum(
		cstring_to_text_with_len(collectionNameView->string, collectionNameView->length));

	MongoCollection *collection;
	if (context->mongoCollection == NULL)
	{
		collection = GetMongoCollectionOrViewByNameDatum(PointerGetDatum(databaseDatum),
														 collectionNameDatum,
														 AccessShareLock);
	}
	else
	{
		collection = context->mongoCollection;
	}

	/* CollectionUUID mismatch when collection doesn't exist */
	if (collectionUuid != NULL)
	{
		if (collection == NULL)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_COLLECTIONUUIDMISMATCH),
							errmsg(
								"Namespace %s contains a mismatch in the collectionUUID identifier: Collection does not exist",
								context->namespaceName)));
		}

		if (memcmp(collectionUuid->data, collection->collectionUUID.data, 16) != 0)
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_COLLECTIONUUIDMISMATCH),
							errmsg(
								"Namespace %s contains a mismatch in the collectionUUID identifier",
								context->namespaceName)));
		}
	}

	List *pipelineStages = NIL;
	if (collection != NULL && collection->viewDefinition != NULL)
	{
		collection = ExtractViewDefinitionAndPipeline(PointerGetDatum(databaseDatum),
													  collection->viewDefinition,
													  &pipelineStages,
													  context);
	}

	context->mongoCollection = collection;

	/* First step - create the RTE.
	 * This is either a RTE for a table, or the empty table RTE.
	 * (Note that sample can modify this later in the sample stage).
	 */
	RangeTblEntry *rte = makeNode(RangeTblEntry);

	/* Match spec for ApiSchema.collection() function */
	List *colNames = list_make3(makeString("shard_key_value"), makeString("object_id"),
								makeString("document"));

	const char *collectionAlias = "collection";
	if (context->numNestedLevels > 0 || context->nestedPipelineLevel > 0)
	{
		StringInfo s = makeStringInfo();
		appendStringInfo(s, "collection_%d_%d", context->numNestedLevels,
						 context->nestedPipelineLevel);
		collectionAlias = s->data;
	}

	if (collection == NULL)
	{
		/* Here: Special case, if the database is config, try to see if we can create a base
		 * table out of the system metadata.
		 */
		StringView databaseView = CreateStringViewFromText(databaseDatum);
		if (StringViewEqualsCString(&databaseView, "config"))
		{
			Query *returnedQuery = GenerateConfigDatabaseQuery(context);
			if (returnedQuery != NULL)
			{
				return returnedQuery;
			}
		}

		rte->rtekind = RTE_FUNCTION;
		rte->relid = InvalidOid;
		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(BsonEmptyDataTableFunctionId(), RECORDOID, NIL,
										   InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);


		rangeFunc->funcretset = true;
		RangeTblFunction *rangeTableFunction = makeNode(RangeTblFunction);
		rangeTableFunction->funccolcount = 3;
		rangeTableFunction->funcparams = NULL;
		rangeTableFunction->funcexpr = (Node *) rangeFunc;

		rte->alias = makeAlias(collectionAlias, NIL);
		rte->eref = makeAlias(collectionAlias, colNames);

		/* Add the RTFunc to the RTE */
		rte->functions = list_make1(rangeTableFunction);
	}
	else
	{
		rte->rtekind = RTE_RELATION;
		rte->relid = collection->relationId;

		if (collection->mongoDataCreationTimeVarAttrNumber != -1)
		{
			colNames = lappend(colNames, makeString("creation_time"));
		}

		if (context->allowShardBaseTable)
		{
			Oid shardOid = TryGetCollectionShardTable(collection, AccessShareLock);
			if (shardOid != InvalidOid)
			{
				/* Mark on our copy of the collection that we're using the shard */
				collection->relationId = shardOid;
				rte->relid = shardOid;
			}
			else if (DefaultInlineWriteOperations)
			{
				context->allowShardBaseTable = true;
			}
			else
			{
				/* Signal that shard table pushdown didn't succeed */
				context->allowShardBaseTable = false;
			}
		}

		rte->alias = makeAlias(collectionAlias, NIL);
		rte->eref = makeAlias(collectionAlias, colNames);
		rte->lateral = false;
		rte->inFromCl = true;
		rte->relkind = RELKIND_RELATION;
		rte->functions = NIL;
		rte->inh = true;
#if PG_VERSION_NUM >= 160000
		RTEPermissionInfo *permInfo = addRTEPermissionInfo(&query->rteperminfos, rte);
		permInfo->requiredPerms = ACL_SELECT;
#else
		rte->requiredPerms = ACL_SELECT;
#endif
		rte->rellockmode = AccessShareLock;
	}

	query->rtable = list_make1(rte);

	/* Now register the RTE in the "FROM" clause with no filters */
	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 = CreateDocumentVar();
	TargetEntry *baseTargetEntry = makeTargetEntry((Expr *) documentEntry, 1, "document",
												   false);
	query->targetList = list_make1(baseTargetEntry);

	/* Now do filters - if there's no shard key we inject a default shard key of 'collectionId' */
	if (collection != NULL && collection->shardKey == NULL &&
		!ShouldSkipShardKeyFilterOnBaseTable(context))
	{
		/* construct a shard_key_value = <collection_id> filter */
		Expr *zeroShardKeyFilter = CreateNonShardedShardKeyValueFilter(
			documentEntry->varno,
			collection);

		/* add the filter to WHERE */
		query->jointree->quals = (Node *) zeroShardKeyFilter;
	}

	/* Before applying the view stages, apply the hint on the base table */
	if (collection != NULL && indexHint != NULL &&
		indexHint->value_type != BSON_TYPE_EOD)
	{
		const char *indexName = NULL;
		bool isSparse = false;
		pgbson *indexKeyDocument = NULL;
		if (indexHint->value_type == BSON_TYPE_UTF8)
		{
			indexName = indexHint->value.v_utf8.str;
			IndexDetails *details = IndexNameGetReadyIndexDetails(
				collection->collectionId, indexName);
			if (details == NULL)
			{
				/*
				 * 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.
				 */
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg(
									"index specified by index hint is not found: hint provided does not correspond to an existing index")));
			}

			indexKeyDocument = details->indexSpec.indexKeyDocument;
			isSparse = details->indexSpec.indexSparse;
		}
		else if (indexHint->value_type == BSON_TYPE_DOCUMENT)
		{
			indexKeyDocument = PgbsonInitFromDocumentBsonValue(indexHint);
			if (IsNaturalSortHint(indexHint))
			{
				/* Index hint is $natural, in this case */
				indexName = "_id_";
			}
			else
			{
				List *indexDocs = IndexKeyGetReadyMatchingIndexes(
					collection->collectionId,
					indexKeyDocument);

				if (list_length(indexDocs) == 0)
				{
					/*
					 * 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.
					 */
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
									errmsg(
										"index specified by index hint is not found: hint provided does not correspond to an existing index")));
				}
				else if (list_length(indexDocs) > 1)
				{
					ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
									errmsg(
										"index specified by index hint is ambiguous. please specify hint by name")));
				}

				IndexDetails *detail = linitial(indexDocs);
				indexName = pstrdup(detail->indexSpec.indexName);
				isSparse = detail->indexSpec.indexSparse;
				list_free_deep(indexDocs);
			}
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INVALIDOPTIONS),
							errmsg(
								"Index hint should be provided as either a string or a document")));
		}

		FuncExpr *indexHintExpr = makeFuncExpr(
			BsonIndexHintFunctionOid(), BOOLOID,
			list_make4(documentEntry, MakeTextConst(indexName, strlen(indexName)),
					   MakeBsonConst(indexKeyDocument), makeBoolConst(isSparse, false)),
			InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
		if (query->jointree->quals == NULL)
		{
			query->jointree->quals = (Node *) indexHintExpr;
		}
		else
		{
			List *currentQuals = make_ands_implicit((Expr *) query->jointree->quals);
			currentQuals = lappend(currentQuals, indexHintExpr);
			query->jointree->quals = (Node *) make_ands_explicit(currentQuals);
		}
	}

	/* Now if there's pipeline stages, apply the stages in reverse (innermost first) */
	for (int i = list_length(pipelineStages) - 1; i >= 0; i--)
	{
		List *stages = list_nth(pipelineStages, i);
		query = MutateQueryWithPipeline(query, stages, context);
	}

	return query;
}


/*
 * Given a query, adds the cursor based functions to the query if it's a streaming query
 * and updates the queryData with the appropriate information.
 */
static void
AddCursorFunctionsToQuery(Query *query, Query *baseQuery,
						  QueryData *queryData,
						  AggregationPipelineBuildContext *context,
						  bool addCursorAsConst)
{
	if (queryData->cursorKind != QueryCursorType_Streamable)
	{
		return;
	}

	/* Add the projector and percolate it down */
	if (baseQuery != query)
	{
		/*
		 * Technically we can support this for narrow scenarios like:
		 * {"$match"}{"$limit": 1 } {"$addFields"}
		 * We can just mark this as non-streamable.
		 * Note that this typically wont add any actual persistent state
		 * since it'll likely drain in 1 shot due to the $limit: 1
		 */
		elog(LOG_SERVER_ONLY, "Query has more than 1 level with streaming mode.");
		queryData->cursorKind = QueryCursorType_Persistent;
		return;
	}

	/* Add a parameter for the cursor state */
	Node *cursorNode;
	if (queryData->cursorStateConst != NULL)
	{
		cursorNode = (Node *) MakeBsonConst(queryData->cursorStateConst);
	}
	else if (addCursorAsConst)
	{
		cursorNode = (Node *) MakeBsonConst(PgbsonInitEmpty());
	}
	else
	{
		context->currentParamCount++;
		queryData->cursorStateParamNumber = context->currentParamCount;
		Param *param = makeNode(Param);
		param->paramid = queryData->cursorStateParamNumber;
		param->paramkind = PARAM_EXTERN;
		param->paramtype = BsonTypeId();
		param->paramtypmod = -1;
		cursorNode = (Node *) param;
	}

	/* Create the WHERE cursor_state(document, continuationParam) and add it to the WHERE */
	List *cursorArgs = list_make2(CreateDocumentVar(), cursorNode);
	FuncExpr *cursorQual = makeFuncExpr(ApiCursorStateFunctionId(), BOOLOID,
										cursorArgs, InvalidOid, InvalidOid,
										COERCE_EXPLICIT_CALL);
	if (baseQuery->jointree->quals != NULL)
	{
		List *ands = make_ands_implicit((Expr *) baseQuery->jointree->quals);
		ands = lappend(ands, cursorQual);
		baseQuery->jointree->quals = (Node *) make_ands_explicit(ands);
	}
	else
	{
		baseQuery->jointree->quals = (Node *) cursorQual;
	}

	/* Add the cursor projector */
	TargetEntry *entry = llast(query->targetList);

	List *projectorArgs = list_make1(CreateDocumentVar());
	FuncExpr *projectionExpr = makeFuncExpr(ApiCurrentCursorStateFunctionId(),
											BsonTypeId(), projectorArgs, InvalidOid,
											InvalidOid, COERCE_EXPLICIT_CALL);

	bool resjunk = false;
	TargetEntry *newEntry = makeTargetEntry((Expr *) projectionExpr, entry->resno + 1,
											"continuation", resjunk);

	query->targetList = lappend(query->targetList, newEntry);
}


/*
 * This function adds the continuation condition to the query for change stream,
 * to make sure the continuation token is always returned to the caller event if
 * the change document doesn't match the $match condition in the aggregation pipeline.
 * An example query after the modification here will be:
 * SELECT document, continuation
 *  FROM change_stream_aggregation(<args>)
 *  WHERE
 *   document IS NULL // This condition is added to make sure the continuation token is always returned
 *                    // even if the change document doesn't match the $match condition in the aggregation pipeline.
 *      OR
 *   <other qualifiers>
 */
static void
AddQualifierForTailableQuery(Query *query, Query *baseQuery,
							 QueryData *queryData,
							 AggregationPipelineBuildContext *context)
{
	Assert(queryData->cursorKind == QueryCursorType_Tailable);

	/* Make sure that continuation is still projected after all mutations. */
	TargetEntry *entry = llast(query->targetList);
	if (entry->resname == NULL || strcmp(entry->resname, "continuation") != 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_INVALIDOPTIONS),
						errmsg(
							"The last target entry in the query must be the document")));
	}

	/*
	 * If there are some WHERE clause qualifiers, in a change stream query, then prepend
	 * the condition document IS NULL OR <other qualifiers> to the WHERE clause.
	 * This is needed becuase, when document IS NULL, change stream aggregation still
	 * needs to provide the continuation doc for the tailable cursor to continue the
	 * change stream query next time. So any other qualifiers should be ORed with this
	 * NULL check so that they don't filter out the document IS NULL condition.
	 *
	 *   Before transformation:
	 *      SELECT doc, continuation FROM change_stream_aggregation(<params>)
	 *           WHERE bson_dollor_match(doc) //doc.full_document.operationtype == "insert"
	 *
	 *   After transformation:
	 *      SELECT doc, continuation FROM change_stream_aggregation(<params>)
	 *           (document IS NULL) OR
	 *           (WHERE bson_dollor_match(doc) //doc.full_document.operationtype == "insert")
	 */
	if (query->jointree->quals != NULL)
	{
		Var *documentVar = CreateChangeStreamDocumentVar();
		NullTest *nullTest = makeNode(NullTest);
		nullTest->argisrow = false;
		nullTest->nulltesttype = IS_NULL;
		nullTest->arg = (Expr *) documentVar;

		/* Create the new OR clause including cursorQual */
		List *qualifierList = list_make1(query->jointree->quals);

		/* Prepend the document == NULL condition first before any other qualifiers. */
		qualifierList = lcons(nullTest, qualifierList);

		/* Add the new OR clause to the query */
		query->jointree->quals = (Node *) make_orclause(qualifierList);
	}
}


/*
 * Parses the numeric $sample size into an int64. Out-of-range magnitudes
 * saturate to the int64 bounds; NaN coerces to zero.
 */
static int64_t
ParseSampleSize(const bson_value_t *sizeValue)
{
	/* Quiet conversion saturates an out-of-range magnitude to +/-infinity instead of throwing. */
	double sizeDouble = BsonValueAsDoubleQuiet(sizeValue);

	if (isnan(sizeDouble))
	{
		/* NaN coerces to zero, which the caller rejects as non positive. */
		return 0;
	}

	if (sizeDouble >= (double) PG_INT64_MAX)
	{
		/* Oversized: clamp to maximum int64. */
		return PG_INT64_MAX;
	}

	if (sizeDouble <= (double) PG_INT64_MIN)
	{
		/* Undersized: clamp to minimum int64. */
		return PG_INT64_MIN;
	}

	/* In-range: Decimal128 rounds half to even; other numerics truncate toward zero. */
	return sizeValue->value_type == BSON_TYPE_DECIMAL128 ?
		   GetBsonDecimal128AsInt64(sizeValue, ConversionRoundingMode_NearestEven) :
		   BsonValueAsInt64(sizeValue);
}


/*
 * Processes the Sample stage for the aggregation pipeline.
 * If the sample is against the base RTE - injects the Sample TSM.
 * If it's on a downstream stage, injects an ORDER BY Random().
 */
static Query *
HandleSample(const bson_value_t *existingValue, Query *query,
			 AggregationPipelineBuildContext *context)
{
	ReportFeatureUsage(FEATURE_STAGE_SAMPLE);
	if (existingValue->value_type != BSON_TYPE_DOCUMENT)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION28745),
						errmsg(
							"The $sample stage specification is required to be provided as an object.")));
	}

	bson_iter_t sampleIter;
	BsonValueInitIterator(existingValue, &sampleIter);
	bson_value_t sizeValue = { 0 };

	while (bson_iter_next(&sampleIter))
	{
		if (strcmp(bson_iter_key(&sampleIter), "size") == 0)
		{
			sizeValue = *bson_iter_value(&sampleIter);

			/* Validate the size type inline so a non-numeric size takes
			 * precedence over any later unrecognized option. */
			if (!BsonValueIsNumber(&sizeValue))
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION28746),
								errmsg(
									"size argument to $sample must be a number")));
			}
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION28748),
							errmsg("Option not recognized for $sample")));
		}
	}

	if (sizeValue.value_type == BSON_TYPE_EOD)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION28749),
						errmsg("The $sample stage must explicitly define a size value")));
	}

	/*
	 * Coerce the size to an int64, then require a strictly positive value; a
	 * zero, NaN, or fractional value that rounds to zero is rejected.
	 */
	int64_t sampleSize = ParseSampleSize(&sizeValue);
	if (sampleSize <= 0)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION28747),
						errmsg(
							"size argument to $sample must be a positive integer")));
	}

	/* If the sample is against the base RTE - convert to a sample CTE */
	RangeTblEntry *rte = linitial(query->rtable);

	/* If there is a filter that's not the default filter then we can't push down sample */
	/* TODO: Pushdown sample to base RTE for $lookup. */
	if (rte->rtekind == RTE_RELATION &&
		IsDefaultJoinTree(query->jointree->quals))
	{
		/* Then just convert this to a Sample RTE */
		if (rte->tablesample != NULL)
		{
			Node *sampleArg = linitial(rte->tablesample->args);
			if (!IsA(sampleArg, Const))
			{
				ereport(PANIC, (errmsg("Expected tample sample to be a const")));
			}

			Const *constVal = (Const *) sampleArg;
			int64_t finalSize = DatumGetInt64(constVal->constvalue);
			finalSize = Min(finalSize, sampleSize);
			constVal->constvalue = Int64GetDatum(finalSize);
		}
		else
		{
			TableSampleClause *tablesample_sys_rows = makeNode(TableSampleClause);
			tablesample_sys_rows->tsmhandler = ExtensionTableSampleSystemRowsFunctionId();

			Node *rowCountArg = (Node *) makeConst(INT8OID, -1, InvalidOid,
												   sizeof(int64_t),
												   Int64GetDatum(sampleSize), false,
												   true);

			tablesample_sys_rows->args = list_make1(rowCountArg);

			rte->tablesample = tablesample_sys_rows;
		}
	}
	else if (EnableDollarSampleReservoirScan && rte->rtekind == RTE_RELATION &&
			 context->mongoCollection != NULL &&
			 context->mongoCollection->shardKey == NULL &&
			 sampleSize <= GetMaxReservoirSampleSize())
	{
		/*
		 * When reservoir sampling is enabled, the target is a base relation,
		 * and the collection is NOT sharded, emit a bson_dollar_range qual
		 * with a "sample" parameter. The set_rel_pathlist_hook detects this
		 * and wraps the scan paths with a ReservoirSample CustomPath that
		 * uses PostgreSQL's built-in reservoir sampling algorithm.
		 * Sharded collections are excluded for now.
		 *
		 * The "sampleSize <= GetMaxReservoirSampleSize()" guard above keeps
		 * oversized requests off this path: the reservoir buffers K HeapTuple
		 * copies in a palloc'd array, so K is capped by the palloc size limit.
		 * Larger sizes fall through to the ORDER BY random() path below.
		 *
		 * TODO: Handle reservoir sampling for upper paths as well (e.g.,
		 * $unwind followed by $sample should still use reservoir instead of
		 * the ORDER BY random() fallback).
		 */

		pgbson_writer writer;
		PgbsonWriterInit(&writer);
		pgbson_writer rangeWriter;
		PgbsonWriterStartDocument(&writer, "", 0, &rangeWriter);
		PgbsonWriterAppendInt64(&rangeWriter, "sample", 6, sampleSize);
		PgbsonWriterEndDocument(&writer, &rangeWriter);

		Const *bsonConst = makeConst(BsonTypeId(), -1, InvalidOid, -1,
									 PointerGetDatum(PgbsonWriterGetPgbson(&writer)),
									 false, false);
		List *args = list_make2(MakeSimpleDocumentVar(), (Node *) bsonConst);
		FuncExpr *sampleQual = makeFuncExpr(BsonRangeMatchFunctionId(),
											BOOLOID, args, InvalidOid, InvalidOid,
											COERCE_EXPLICIT_CALL);

		if (query->jointree->quals != NULL)
		{
			List *quals = lappend(
				make_ands_implicit((Expr *) query->jointree->quals),
				sampleQual);
			query->jointree->quals = (Node *) make_ands_explicit(quals);
		}
		else
		{
			query->jointree->quals = (Node *) sampleQual;
		}

		/* Next stage must be wrapped in a subquery */
		context->requiresSubQuery = true;
		return query;
	}

	/* Add an order by Random(), Limit N */
	ParseState *parseState = make_parsestate(NULL);
	parseState->p_expr_kind = EXPR_KIND_ORDER_BY;

	/* set after what is already taken */
	parseState->p_next_resno = list_length(query->targetList) + 1;

	Expr *expr = (Expr *) makeFuncExpr(PgRandomFunctionOid(), FLOAT8OID, NIL,
									   InvalidOid, InvalidOid, COERCE_EXPLICIT_CALL);
	SortBy *sortBy = makeNode(SortBy);
	sortBy->location = -1;
	sortBy->sortby_dir = SORTBY_DEFAULT; /* reset later */
	sortBy->node = (Node *) expr;

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

	pfree(parseState);
	if (sortlist == NIL)
	{
		ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_LOCATION15976),
						errmsg(
							"The $sort stage requires specifying at least one sorting key to proceed")));
	}

	query->sortClause = sortlist;

	query->limitCount = (Node *) makeConst(INT8OID, -1, InvalidOid, sizeof(int64_t),
										   Int64GetDatum(sampleSize), false, true);

	/* Push next stage to a new subquery (since we did a sort) */
	context->requiresSubQuery = true;

	return query;
}


/*
 * Helper method used by MutateStageWithPipeline to extract the appropriate
 * Stage information. Compares the aggregation stage by the ordinal comparison
 * of its name.
 */
static int
CompareStageByStageName(const void *a, const void *b)
{
	const char *key = (const char *) a;
	AggregationStageDefinition *stage = (AggregationStageDefinition *) b;

	return strcmp(key, stage->stage);
}


/*
 * Returns true if the node is a boolean TRUE constant.
 */
static inline bool
IsBooleanTrueConst(Node *node)
{
	return IsA(node, Const) &&
		   ((Const *) node)->consttype == BOOLOID &&
		   !((Const *) node)->constisnull &&
		   DatumGetBool(((Const *) node)->constvalue);
}


/*
 * Checks whether the given node represents the default state
 * (i.e. no user-specified filter). This is true when:
 *  - node is NULL (no filter applied)
 *  - node is BoolConst(TRUE) (empty match on sharded collections)
 *  - node is a single OpExpr of shard_key_value = <bigint>
 *    (the default shard key equality filter)
 */
static bool
IsDefaultJoinTree(Node *node)
{
	if (node == NULL)
	{
		return true;
	}

	/*
	 * For sharded collections, query->jointree->quals starts as NULL
	 * (no default shard key equality filter since each document has a
	 * different hash-based shard key value). When HandleMatch({})
	 * processes an empty match, it calls make_ands_explicit(NIL) which
	 * returns a BoolConst(TRUE) node (the PG representation of an
	 * always-true condition). Without this check, IsDefaultJoinTree
	 * would not recognize BoolConst(TRUE) as equivalent to "no filter",
	 * so HandleSample would conclude there was a user filter and skip
	 * the TABLESAMPLE optimization.
	 */
	if (EnableSampleScanFixOnSharded && IsBooleanTrueConst(node))
	{
		return true;
	}

	if (!IsA(node, OpExpr))
	{
		return false;
	}

	/* Check that it's a bigint equality on the shard_key_value column
	 * specifically, not just any bigint equality (e.g. collection_id). */
	OpExpr *opExpr = (OpExpr *) node;
	Expr *firstArg = linitial(opExpr->args);
	return opExpr->opno == BigintEqualOperatorId() &&
		   IsA(firstArg, Var) &&
		   ((Var *) firstArg)->varattno ==
		   DOCUMENT_DATA_TABLE_SHARD_KEY_VALUE_VAR_ATTR_NUMBER;
}


/*
 * Default helper for a stage that can always be inlined for a $lookup such as $match
 */
static bool
CanInlineLookupStageTrue(const bson_value_t *stageValue, const StringView *lookupPath,
						 bool hasLet)
{
	return true;
}


static bool
QueryDocumentHasExpr(bson_iter_t *queryIter)
{
	check_stack_depth();
	while (bson_iter_next(queryIter))
	{
		const char *key = bson_iter_key(queryIter);
		if (strcmp(key, "$expr") == 0)
		{
			return true;
		}

		if (strcmp(key, "$and") == 0 || strcmp(key, "$or") == 0)
		{
			if (!BSON_ITER_HOLDS_ARRAY(queryIter))
			{
				continue;
			}

			bson_iter_t andOrIter;
			if (bson_iter_recurse(queryIter, &andOrIter))
			{
				while (bson_iter_next(&andOrIter))
				{
					if (!BSON_ITER_HOLDS_DOCUMENT(&andOrIter))
					{
						continue;
					}

					bson_iter_t andElementIter;
					if (bson_iter_recurse(&andOrIter, &andElementIter) &&
						QueryDocumentHasExpr(&andElementIter))
					{
						return true;
					}
				}
			}
		}
	}

	return false;
}


/*
 * Checks if $match can be inlined for $lookup
 */
static bool
CanInlineLookupStageMatch(const bson_value_t *stageValue, const
						  StringView *lookupPath, bool hasLet)
{
	if (!hasLet)
	{
		/* Without let $match can always be inlined */
		return true;
	}

	if (stageValue->value_type != BSON_TYPE_DOCUMENT)
	{
		return false;
	}

	/* With let $match can be inlined if there's no $expr */
	bson_iter_t queryIter;
	BsonValueInitIterator(stageValue, &queryIter);
	return !QueryDocumentHasExpr(&queryIter);
}


/*
 * Helper for a projection stage on whether it can be inlined for a $lookup.
 * Can be inlined if all the projection functions are not prefixes or suffixes
 * of the lookup local path.
 * We can probably do better but that is left as an exercise for later.
 */
static bool
CanInlineLookupStageSetAddFields(const bson_value_t *stageValue, const
								 StringView *lookupPath, bool hasLet)
{
	if (stageValue->value_type != BSON_TYPE_DOCUMENT)
	{
		return false;
	}

	if (hasLet)
	{
		return false;
	}

	bson_iter_t projectIter;
	BsonValueInitIterator(stageValue, &projectIter);

	while (bson_iter_next(&projectIter))
	{
		const StringView keyView = bson_iter_key_string_view(&projectIter);
		if (StringViewStartsWithStringView(&keyView, lookupPath) ||
			StringViewStartsWithStringView(lookupPath, &keyView))
		{
			return false;
		}
	}

	return true;
}


/*
 * Helper for a $project stage on whether it can be inlined for a $lookup.
 * Can be inlined if all the $project fields does not exclude or overwrite the lookup path.
 */
static bool
CanInlineLookupStageProject(const bson_value_t *stageValue, const
							StringView *lookupPath, bool hasLet)
{
	if (stageValue->value_type != BSON_TYPE_DOCUMENT)
	{
		return false;
	}

	if (hasLet)
	{
		return false;
	}

	bson_iter_t projectIter;
	BsonValueInitIterator(stageValue, &projectIter);

	bool hasInclusion = false;
	bool hasExclusion = false;
	bool foundMatchInInclusion = false;
	bool foundMatchInExclusion = false;
	bool isIdModifiedorExcluded = false;
	bool isJoinOnIdField = StringViewEqualsCString(lookupPath, "_id");

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

		StringView keyViewPrefix = StringViewFindPrefix(&keyView, '.');
		if (keyViewPrefix.length != 0 && StringViewEquals(&keyViewPrefix, lookupPath))
		{
			/* it's a dotted projection and both prefix are matching so we safely return false */
			/* TODO: We can probably do better but that is left as an exercise for later. */
			return false;
		}

		/* handle _id cases */
		if (StringViewEqualsCString(&keyView, "_id"))
		{
			if (!isJoinOnIdField)
			{
				/* join is not on _id so we skip */
				continue;
			}

			if (BsonValueIsNumberOrBool(value))
			{
				/* join is on _id and excluded */
				if (BsonValueAsInt32(value) == 0)
				{
					isIdModifiedorExcluded = true;
				}
			}
			else
			{
				/* join is on _id and modified */
				isIdModifiedorExcluded = true;
			}
			continue;
		}

		if (BsonValueIsNumberOrBool(value))
		{
			if (BsonValueAsInt32(value) == 0)
			{
				hasExclusion = true;

				if (StringViewEquals(&keyView, lookupPath))
				{
					foundMatchInExclusion = true;
				}
			}
			else
			{
				hasInclusion = true;

				if (StringViewEquals(&keyView, lookupPath))
				{
					foundMatchInInclusion = true;
				}
			}
		}
		else
		{
			/* If a projection assigns a value (not 1, 0, true, or false) to a field */
			/* that's used in the join, we cannot inline it as it would overwrite the join field. */
			hasInclusion = true;
			if (StringViewEquals(&keyView, lookupPath))
			{
				return false;
			}
		}

		if (hasInclusion && hasExclusion)
		{
			/* Mixed inclusion and exclusion, cannot inline */
			/* Can we do better fail early, but that is left as an exercise for later.*/
			return false;
		}
	}

	if (isJoinOnIdField && isIdModifiedorExcluded)
	{
		/* join is on _id and _id is either excluded or modified */
		return false;
	}

	if (hasExclusion && foundMatchInExclusion)
	{
		/* Excluding the lookup path */
		return false;
	}

	if (hasInclusion && !foundMatchInInclusion)
	{
		/* Inclusion but path does not exist */
		return false;
	}

	return true;
}


/*
 * Helper for an unset stage on whether it can be inlined for a $lookup.
 */
static bool
CanInlineLookupStageUnset(const bson_value_t *stageValue, const StringView *lookupPath,
						  bool hasLet)
{
	/* An unset can be pushed up if */
	if (stageValue->value_type == BSON_TYPE_UTF8)
	{
		/* a) it is a string and it's not equal to or a suffix of the lookup path */
		StringView unsetStr = {
			.string = stageValue->value.v_utf8.str, .length = stageValue->value.v_utf8.len
		};
		return !StringViewStartsWithStringView(&unsetStr, lookupPath);
	}
	else if (stageValue->value_type == BSON_TYPE_ARRAY)
	{
		/* or b) it's an array, and none of the unset paths intersect with the lookup path */
		bson_iter_t unsetArrayIter;
		BsonValueInitIterator(stageValue, &unsetArrayIter);

		bool allIndependentOfLookup = true;
		while (bson_iter_next(&unsetArrayIter) && allIndependentOfLookup)
		{
			if (BSON_ITER_HOLDS_UTF8(&unsetArrayIter))
			{
				StringView unsetStr = { 0 };
				unsetStr.string = bson_iter_utf8(&unsetArrayIter, &unsetStr.length);
				allIndependentOfLookup = !StringViewStartsWithStringView(&unsetStr,
																		 lookupPath);
			}
			else
			{
				allIndependentOfLookup = false;
			}
		}

		return allIndependentOfLookup;
	}

	return false;
}


/*
 * Helper for an unwind stage on whether it can be inlined for a $lookup.
 */
static bool
CanInlineLookupStageUnwind(const bson_value_t *stageValue, const StringView *lookupPath,
						   bool hasLet)
{
	/* An unwind can be pushed up if */
	if (stageValue->value_type == BSON_TYPE_UTF8)
	{
		/* a) it is a string and it's not equal to or a suffix of the lookup path */
		StringView unwindStr = {
			.string = stageValue->value.v_utf8.str, .length = stageValue->value.v_utf8.len
		};
		return !StringViewStartsWithStringView(&unwindStr, lookupPath);
	}
	else if (stageValue->value_type == BSON_TYPE_DOCUMENT)
	{
		/* or b) it's an object, and the unwind path here is a suffix of the lookup path */
		bson_iter_t optionsDocIter;
		BsonValueInitIterator(stageValue, &optionsDocIter);
		if (bson_iter_find(&optionsDocIter, "path") &&
			BSON_ITER_HOLDS_UTF8(&optionsDocIter))
		{
			StringView unwindStr = { 0 };
			unwindStr.string = bson_iter_utf8(&optionsDocIter, &unwindStr.length);
			return !StringViewStartsWithStringView(&unwindStr, lookupPath);
		}
	}

	return false;
}


/*
 * parses the 'cursor' field in a query or aggregation spec.
 */
void
ParseCursorDocument(bson_iter_t *iterator, QueryData *queryData)
{
	EnsureTopLevelFieldType("cursor", iterator, BSON_TYPE_DOCUMENT);
	bson_iter_t cursorDocIter;
	if (!bson_iter_recurse(iterator, &cursorDocIter))
	{
		return;
	}

	while (bson_iter_next(&cursorDocIter))
	{
		const char *path = bson_iter_key(&cursorDocIter);
		const bson_value_t *value = bson_iter_value(&cursorDocIter);
		if (strcmp(path, "batchSize") == 0)
		{
			SetBatchSize("cursor.batchSize", value, queryData);
		}
		else if (strcmp(path, "singleBatch") == 0)
		{
			EnsureTopLevelFieldType("cursor.singleBatch", &cursorDocIter, BSON_TYPE_BOOL);
			if (value->value.v_bool)
			{
				queryData->cursorKind = QueryCursorType_SingleBatch;
			}
		}
		else
		{
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_FAILEDTOPARSE),
							errmsg("Field not recognized: %s",
								   path)));
		}
	}
}


/* Check $match stage for sort clause. If there is one, $near or $nearSphere was used which is not allowed. */
static void
ValidateQueryTreeForMatchStage(const Query *query)
{
	if (!query->sortClause)
	{
		return;
	}


	bool isGeoNear = false;
	ListCell *cell;
	foreach(cell, query->sortClause)
	{
		SortGroupClause *sortClause = (SortGroupClause *) lfirst(cell);
		TargetEntry *tle = get_sortgroupclause_tle(sortClause, query->targetList);
		if (tle->resjunk && IsA(tle->expr, OpExpr) &&
			((OpExpr *) tle->expr)->opno == BsonGeonearDistanceOperatorId())
		{
			if (isGeoNear)
			{
				ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_BADVALUE),
								errmsg("Too many geoNear expressions")));
			}
			isGeoNear = true;
		}
	}

	if (!isGeoNear)
	{
		return;
	}

	TargetEntry *targetEntry = linitial(query->targetList);
	Node *node = (Node *) targetEntry->expr;

	if (!(IsA(node, FuncExpr) && CheckFuncExprBsonDollarProjectGeonear(
			  (FuncExpr *) node)))
	{
		ThrowGeoNearNotAllowedInContextError();
	}
}


/*
 * Recursively check all func project func expression to check if it has $geoNear project
 * bson_dollar_add(bson_dollar_project...)
 */
static bool
CheckFuncExprBsonDollarProjectGeonear(const FuncExpr *funcExpr)
{
	CHECK_FOR_INTERRUPTS();
	check_stack_depth();
	bool isGeoNearProject = false;

	if (funcExpr->funcid == BsonDollarProjectGeonearFunctionOid())
	{
		isGeoNearProject = true;
	}
	else
	{
		/* loop all args */
		ListCell *cell;
		foreach(cell, funcExpr->args)
		{
			Expr *expr = (Expr *) lfirst(cell);
			if (IsA(expr, FuncExpr))
			{
				isGeoNearProject =
					CheckFuncExprBsonDollarProjectGeonear((FuncExpr *) expr);

				if (isGeoNearProject)
				{
					break;
				}
			}
		}
	}

	return isGeoNearProject;
}


/*
 * Handles $match aggregation stage and validates the query tree for geospatial queries.
 */
static Query *
HandleMatchAggregationStage(const bson_value_t *existingValue, Query *query,
							AggregationPipelineBuildContext *context)
{
	query = HandleMatch(existingValue, query, context);
	ValidateQueryTreeForMatchStage(query);
	return query;
}


/*
 * We will try to optimize the aggregation stages here. Currently this method checks
 * if all the stages are referring to a single collection that is not sharded then we
 * can convert the query to push it down to the particular shard (single node)
 * TODO: Other optimizations to be followed
 * 1- Merge consecutive projection stages to be merged.
 * 2- Improve match stage if preceded by a projection stage and the filter is on a renamed field which could
 *    potentially use index.
 */
static List *
TryOptimizeAggregationPipelines(List *aggregationStages,
								AggregationPipelineBuildContext *context)
{
	List *stagesList = aggregationStages;
	if (stagesList == NIL || list_length(stagesList) == 0)
	{
		return stagesList;
	}

	/* Whether or not we can safely push the aggregation pipeline query to the shard table directly depends on
	 * if all the stages refer to a single collection and it is not sharded, only in this case it is feasible to push
	 * these queries directly to shard table.
	 */
	bool allowShardBaseTable = true;
	int nextIndex = 0;
	int currentIndex = 0;

	/* Project-pushup-before-unwind-with-group state. The per-stage walk
	 * below detects the $unwind ... $match* ... $group shape: unwindIdx is
	 * set on the first $unwind, groupIdx on the first $group after it.
	 * projectPushupDisqualified flips true on a non-$match between them or
	 * a second $unwind, suppressing the rewrite for the whole pipeline. The
	 * rewrite is applied after the loop, so foreach_delete_current calls in
	 * the merge cases can't invalidate the recorded indices. */
	int unwindIdx = -1;
	int groupIdx = -1;
	bool projectPushupDisqualified = false;

	ListCell *cell;
	foreach(cell, stagesList)
	{
		currentIndex = foreach_current_index(cell);

		/* This may seem weird, but we do this since we delete cells directly
		 * from the list - after deletion the currentIndex moves back so we skip
		 * the stage we just visited and move this forward.
		 */
		if (currentIndex < nextIndex)
		{
			continue;
		}

		nextIndex = currentIndex + 1;
		AggregationStage *stage = (AggregationStage *) lfirst(cell);
		const AggregationStageDefinition *definition = stage->stageDefinition;
		if (!definition->allowBaseShardTablePushdown)
		{
			/* If any stage doesn't support it the final value is not supported */
			allowShardBaseTable = false;
		}

		Stage stageEnum = stage->stageDefinition->stageEnum;

		/* Project-pushup disqualifier: any non-Unwind/Match/Group
		 * stage inside the tracking window kills the rewrite. */
		if (unwindIdx >= 0 && groupIdx < 0 &&
			stageEnum != Stage_Unwind &&
			stageEnum != Stage_Match &&
			stageEnum != Stage_Group)
		{
			projectPushupDisqualified = true;
		}

		switch (stageEnum)
		{
			case Stage_Unwind:
			{
				/* second $unwind in the window disqualifies */
				if (unwindIdx < 0)
				{
					unwindIdx = currentIndex;
				}
				else if (groupIdx < 0)
				{
					projectPushupDisqualified = true;
				}
				continue;
			}

			case Stage_Match:
			{
				/* Allowed inside the project-pushup tracking window. */
				continue;
			}

			case Stage_Group:
			{
				/* Completes the $unwind ... $match* ... $group shape if
				 * we're tracking a $unwind. */
				if (unwindIdx >= 0 && groupIdx < 0)
				{
					groupIdx = currentIndex;
				}
				continue;
			}

			case Stage_Lookup:
			{
				if (IsPipelineStageFollowedByOtherStage(Stage_Lookup, Stage_Unwind,
														currentIndex, stagesList))
				{
					/* Optimization for $lookup stage
					 * If the next stage is $unwind, we can merge the $lookup and $unwind stages into a single stage.
					 * This is because $lookup followed by $unwind is a common pattern and can be optimized to a single stage,
					 * if $unwind is requested on the same field which is the "as" field in lookup stage.
					 */
					AggregationStage *unwindStage =
						(AggregationStage *) lfirst(list_nth_cell(stagesList, nextIndex));
					bool preserveEmptyArrays = false;
					if (CanInlineLookupWithUnwind(&stage->stageValue,
												  &unwindStage->stageValue,
												  &preserveEmptyArrays))
					{
						AggregationStage *lookupUnwindStage = unwindStage;

						/* merge preserve empty arrays and the lookup spec */
						pgbson_writer writer;
						PgbsonWriterInit(&writer);
						PgbsonWriterAppendBool(&writer, "preserveNullAndEmptyArrays", 26,
											   preserveEmptyArrays);
						PgbsonWriterAppendValue(&writer, "lookup", 6, &stage->stageValue);

						lookupUnwindStage->stageValue = ConvertPgbsonToBsonValue(
							PgbsonWriterGetPgbson(&writer));
						lookupUnwindStage->stageDefinition = &LookupUnwindStageDefinition;

						context->joinStatus = JoinStageStatus_HasJoinsOrUnions;
						stagesList = foreach_delete_current(stagesList, cell);
					}
				}

				continue;
			}

			case Stage_Sort:
			{
				if (EnableSortGroupStage &&
					IsPipelineStageFollowedByOtherStage(Stage_Sort, Stage_Group,
														currentIndex, stagesList))
				{
					/* If we have a sort followed by a group, create a combined stage that allows for
					 * cross stage optimization.
					 */
					AggregationStage *nextStage =
						(AggregationStage *) lfirst(list_nth_cell(stagesList, nextIndex));
					pgbson_writer writer;
					PgbsonWriterInit(&writer);
					PgbsonWriterAppendValue(&writer, "sort", 4, &stage->stageValue);
					PgbsonWriterAppendValue(&writer, "group", 5, &nextStage->stageValue);
					nextStage->stageValue = ConvertPgbsonToBsonValue(
						PgbsonWriterGetPgbson(&writer));
					nextStage->stageDefinition = &SortGroupStageDefinition;
					stagesList = foreach_delete_current(stagesList, cell);
				}

				continue;
			}

			case Stage_ChangeStream:
			{
				if (IsPipelineStageFollowedByOtherStage(Stage_ChangeStream, Stage_Match,
														currentIndex, stagesList))
				{
					/* Inline $match stage collection filters */
					AggregationStage *matchStage =
						(AggregationStage *) lfirst(list_nth_cell(stagesList, nextIndex));
					bool inlinedCompletely = false;
					if (TryInlineChangeStreamNamespaceFilters(stage, matchStage,
															  &inlinedCompletely))
					{
						/* If the match stage has only collection namespace filters, we can
						 * remove the next stage completely after inlining. */
						if (RemoveMatchNamespaceFilters && inlinedCompletely)
						{
							memcpy(matchStage, stage, sizeof(AggregationStage));

							/* delete the current stage */
							stagesList = foreach_delete_current(stagesList, cell);
						}
					}
				}

				continue;
			}

			default:
			{
				continue;
			}
		}
	}

	context->allowShardBaseTable = allowShardBaseTable;

	/* The rewrite can fire iff no disqualifying event was seen and the
	 * per-stage walk found the complete $unwind ... $match* ... $group
	 * shape.
	 * The groupIdx > unwindIdx check is defensive: the walk already
	 * guarantees the $group is recorded after the $unwind. */
	bool canPushProject = !projectPushupDisqualified &&
						  unwindIdx >= 0 && groupIdx >= 0 &&
						  groupIdx > unwindIdx;

	if (canPushProject)
	{
		/* The $unwind ... $match* ... $group shape is detected purely from
		 * the stages' state, so we know this pipeline is a candidate before
		 * the deeper spec validation runs.
		 */
		ReportFeatureUsage(FEATURE_STAGE_PROJECT_PUSHUP_BEFORE_UNWIND_WITH_GROUP);

		if (EnableProjectPushUpBeforeUnwindWithGroup)
		{
			stagesList = TryInjectProjectBeforeUnwindForGroup(stagesList,
															  unwindIdx, groupIdx,
															  context);
		}
	}

	return stagesList;
}


/* Utility to check if firstStage is followed by secondStage */
static bool
IsPipelineStageFollowedByOtherStage(Stage firstStage, Stage secondStage, int curIndx,
									List *stagesList)
{
	if (curIndx >= list_length(stagesList) - 1)
	{
		return false;
	}

	AggregationStage *stage =
		(AggregationStage *) lfirst(list_nth_cell(stagesList, curIndx));
	AggregationStage *nextStage =
		(AggregationStage *) lfirst(list_nth_cell(stagesList, curIndx + 1));
	return stage->stageDefinition->stageEnum == firstStage &&
		   nextStage->stageDefinition->stageEnum == secondStage;
}


/*
 * Try inlining the match filters on namespace followed by a change stream stage.
 * If successful, the match filters are added to the change stream stage as internalfilters.
 * Returns true if inlining was successful.
 * Also the match stage if completely inlined is removed otherwise the namespace filters are removed from the stage.
 */
static bool
TryInlineChangeStreamNamespaceFilters(AggregationStage *changeStreamStage,
									  AggregationStage *matchStage,
									  bool *inlinedCompletely)
{
	/* Validate inputs */
	if (changeStreamStage == NULL || matchStage == NULL ||
		inlinedCompletely == NULL)
	{
		return false;
	}

	bson_iter_t changeValueIter;
	BsonValueInitIterator(&changeStreamStage->stageValue, &changeValueIter);
	bool isAllClusterChangeStream = false;
	while (bson_iter_next(&changeValueIter))
	{
		const char *key = bson_iter_key(&changeValueIter);
		if (strcmp(key, "allChangesForCluster") == 0)
		{
			const bson_value_t *allChangesValue = bson_iter_value(&changeValueIter);
			if (allChangesValue->value_type != BSON_TYPE_BOOL ||
				!allChangesValue->value.v_bool)
			{
				/* It's non bool type or specifically marked as false, no optimizations can be made here */
				return false;
			}
			else
			{
				isAllClusterChangeStream = true;
			}
		}
		else if (strcmp(key, "internalFilters_ns") == 0)
		{
			/* Changestream spec can't have internal filters
			 * treat it is an unknown field and throw error.
			 */
			ereport(ERROR, (errcode(ERRCODE_DOCUMENTDB_UNKNOWNBSONFIELD),
							errmsg("BSON field '%s' is an unknown field", key),
							errdetail_log("BSON field '%s' is an unknown field",
										  key)));
		}
	}

	if (!isAllClusterChangeStream)
	{
		/* This is not an allChangesForCluster stream, bail out */
		return false;
	}

	bson_iter_t matchIter;
	BsonValueInitIterator(&matchStage->stageValue, &matchIter);
	List *nsFilters = NIL;

	bool multipleNSFilters = false;
	bool isInvalidNSFilters = false;
	pgbson *newFilters = FindNamespaceFiltersInMatchSpec(&matchIter, &nsFilters,
														 &multipleNSFilters,
														 &isInvalidNSFilters);
	*inlinedCompletely = IsPgbsonEmptyDocument(newFilters);

	if (multipleNSFilters || isInvalidNSFilters)
	{
		/* No complete namespace filters found, bail out */
		return false;
	}

	pgbson *existingSpec = PgbsonInitFromDocumentBsonValue(
		&changeStreamStage->stageValue);
	pgbson_writer writer;
	PgbsonWriterInit(&writer);
	PgbsonWriterConcat(&writer, existingSpec);

	if (nsFilters != NIL)
	{
		pgbson_array_writer nsValueArrayWriter;
		PgbsonWriterStartArray(&writer, "internalFilters_ns", strlen(
								   "internalFilters_ns"), &nsValueArrayWriter);

		ListCell *cell;
		foreach(cell, nsFilters)
		{
			List *nsValueFilter = (List *) lfirst(cell);
			pgbson_writer nsValueDocWriter;
			PgbsonWriterInit(&nsValueDocWriter);
			PgbsonWriterAppendUtf8(&nsValueDocWriter, "db", 2,
								   (const char *) linitial(nsValueFilter));
			PgbsonWriterAppendUtf8(&nsValueDocWriter, "coll", 4,
								   (const char *) lsecond(nsValueFilter));
			PgbsonArrayWriterWriteDocument(&nsValueArrayWriter,
										   PgbsonWriterGetPgbson(&nsValueDocWriter));

			list_free(nsValueFilter);
		}
		PgbsonWriterEndArray(&writer, &nsValueArrayWriter);

		list_free(nsFilters);
		nsFilters = NIL;
	}

	/* Update changeStream stage value to include filteredCollection */
	changeStreamStage->stageValue = ConvertPgbsonToBsonValue(PgbsonWriterGetPgbson(
																 &writer));
	if (RemoveMatchNamespaceFilters && !(*inlinedCompletely))
	{
		/* If not inlined completely, we need to update the match stage value after removeing ns filters */
		matchStage->stageValue = ConvertPgbsonToBsonValue(newFilters);
	}

	return true;
}


/*
 * Adds topLevel to consumedFields if not already present. Empty views are
 * ignored. Returns false when the set is already at
 * PROJECT_PUSHUP_MAX_FIELDS and topLevel would be a new entry.
 */
static bool
AddTopLevelFieldToSet(HTAB *consumedFields, StringView topLevel)
{
	if (topLevel.length == 0)
	{
		return true;
	}

	bool found = false;
	hash_search(consumedFields, &topLevel, HASH_FIND, &found);
	if (found)
	{
		return true;
	}

	if (hash_get_num_entries(consumedFields) >= PROJECT_PUSHUP_MAX_FIELDS)
	{
		return false;
	}

	hash_search(consumedFields, &topLevel, HASH_ENTER, NULL);
	return true;
}


/*
 * Extracts the top-level component of an already-dollar-stripped field path
 * (the segment up to the first '.', or the whole path if there's no dot).
 * Returns false for empty paths or paths with a leading '.', which have no
 * usable top-level field.
 */
static bool
ExtractTopLevelFieldPrefix(StringView fieldPath, StringView *outTopLevel)
{
	outTopLevel->string = NULL;
	outTopLevel->length = 0;
	if (fieldPath.length == 0 || fieldPath.string[0] == '.')
	{
		return false;
	}

	/* Prefix up to the first '.'; an empty result means no '.' was found, so
	 * the whole path is the top-level field. */
	StringView topLevel = StringViewFindPrefix(&fieldPath, '.');
	*outTopLevel = topLevel.length == 0 ? fieldPath : topLevel;
	return true;
}


/*
 * Adds the top-level component of fieldPath (up to the first '.') to the
 * set and returns it via *outTopLevel for caller-side semantic checks
 * (e.g. _id detection). Returns false (bailing the rewrite) on set overflow
 * or on paths with no usable top-level field (empty or leading '.').
 */
static bool
AddFieldPathTopLevelToSet(HTAB *consumedFields,
						  StringView fieldPath, StringView *outTopLevel)
{
	if (!ExtractTopLevelFieldPrefix(fieldPath, outTopLevel))
	{
		/* Empty or leading-'.' paths reference an empty-named top-level field
		 * (e.g. { "": 5 } or { ".foo": 5 }) that we can't represent in the
		 * consumed-field set. Recording nothing here would let the synthetic
		 * inclusion $project strip a field the query actually references, so
		 * bail the rewrite defensively. */
		return false;
	}

	return AddTopLevelFieldToSet(consumedFields, *outTopLevel);
}


/*
 * Adds the top-level of every field path in a $match filter to the set,
 * recursing into $and/$or/$nor. Sets *outReferencesId if any top-level is
 * "_id". Returns false on any operator we can't statically analyze, or on
 * set overflow.
 */
static bool
CollectMatchTopLevelPaths(const bson_value_t *filterValue,
						  HTAB *consumedFields,
						  bool *outReferencesId)
{
	check_stack_depth();
	CHECK_FOR_INTERRUPTS();

	*outReferencesId = false;

	if (filterValue->value_type != BSON_TYPE_DOCUMENT)
	{
		return false;
	}

	bson_iter_t filterIter;
	BsonValueInitIterator(filterValue, &filterIter);
	while (bson_iter_next(&filterIter))
	{
		const char *key = bson_iter_key(&filterIter);

		/* Example: { $match: { "customer.region": "US" } }
		 * Non-$ keys are field paths (e.g. "customer.region"); record the
		 * top-level component. */
		if (key[0] != '$')
		{
			StringView fieldPath = { .string = key, .length = strlen(key) };
			StringView topLevel;
			if (!AddFieldPathTopLevelToSet(consumedFields, fieldPath, &topLevel))
			{
				return false;
			}
			if (StringViewEquals(&topLevel, &IdFieldStringView))
			{
				/* Example: { $match: { _id: 2 } } */
				*outReferencesId = true;
			}
			continue;
		}

		/* $-prefixed key: classify via the central operator registry so
		 * we don't have to maintain our own string list. */
		const MongoQueryOperator *queryOp =
			GetMongoQueryOperatorByMongoOpName(key, MongoQueryOperatorInputType_Bson);

		switch (queryOp->operatorType)
		{
			case QUERY_OPERATOR_AND:
			case QUERY_OPERATOR_OR:
			case QUERY_OPERATOR_NOR:
			{
				/* Example: { $match: { $or: [ { a: 1 }, { b: 2 } ] } }
				 * Recurse into every branch — we need fields touched
				 * anywhere in the predicate, not just equality predicates.
				 * (TraverseQueryDocumentAndProcess recurses differently.) */
				const bson_value_t *arrayValue = bson_iter_value(&filterIter);
				if (arrayValue->value_type != BSON_TYPE_ARRAY)
				{
					return false;
				}

				bson_iter_t arrayIter;
				BsonValueInitIterator(arrayValue, &arrayIter);
				while (bson_iter_next(&arrayIter))
				{
					if (!BSON_ITER_HOLDS_DOCUMENT(&arrayIter))
					{
						return false;
					}
					bool subReferencesId = false;
					if (!CollectMatchTopLevelPaths(bson_iter_value(&arrayIter),
												   consumedFields,
												   &subReferencesId))
					{
						return false;
					}
					*outReferencesId = *outReferencesId || subReferencesId;
				}
				continue;
			}

			case QUERY_OPERATOR_COMMENT:
			{
				/* Example: { $match: { $comment: "note" } } — metadata only */
				continue;
			}

			default:
			{
				/* Example: { $match: { $expr: { $gt: ["$a", "$b"] } } }
				 * $expr, $where, $jsonSchema, $text, $sampleRate, etc.
				 * May reference fields we can't statically analyze. */
				return false;
			}
		}
	}

	return true;
}


/*
 * Handles a single accumulator argument. Simple "$path" strings add their
 * top-level to the set; pure scalar constants and empty docs contribute
 * nothing. Sets *outReferencesId if the path's top-level is "_id". Returns
 * false on $$ROOT/$$CURRENT, sub-expressions, or arrays.
 */
static bool
CollectAccumulatorArgPath(const bson_value_t *argValue,
						  HTAB *consumedFields,
						  bool *outReferencesId)
{
	*outReferencesId = false;

	switch (argValue->value_type)
	{
		case BSON_TYPE_UTF8:
		{
			const char *path = argValue->value.v_utf8.str;
			uint32_t pathLen = argValue->value.v_utf8.len;
			if (pathLen > 0 && path[0] == '$')
			{
				if (pathLen < 2 || path[1] == '$')
				{
					/* Example: { $first: "$$ROOT" } — "$$ROOT", "$$CURRENT",
					 * and similar variable refs. */
					return false;
				}

				/* Example: { $sum: "$price" } */
				StringView fieldPath = {
					.string = path + 1, .length = pathLen - 1
				};
				StringView topLevel;
				if (!AddFieldPathTopLevelToSet(consumedFields, fieldPath, &topLevel))
				{
					return false;
				}

				if (StringViewEquals(&topLevel, &IdFieldStringView))
				{
					*outReferencesId = true;
				}
				return true;
			}

			/* Literal non-'$' string (e.g. { $sum: "label" }): not an
			 * accumulator path, so fall through to the shared classifier
			 * below rather than deciding it here. */
			break;
		}

		default:
		{
			/* Scalars, {}, arrays, non-empty docs: no accumulator-specific
			 * handling; fall through to the shared classifier below. */
			break;
		}
	}

	/* "$path" extraction above is the only behavior unique to accumulators.
	 * Everything else — scalars, literal strings, {} (e.g. { $count: {} }) —
	 * references no source fields and needs nothing collected; non-empty docs
	 * (sub-expression trees) and arrays may reference fields and bail. */
	return ValueReferencesNoSourceFields(argValue);
}


/*
 * Given a $unwind path value (a UTF8 string of the form "$field" or
 * "$field.sub.path"), extracts the top-level field name (the segment
 * between the leading '$' and the first '.', or the full remainder if
 * there's no dot). Returns false if the value is not a usable path
 * ($$-variables, empty after '$', non-UTF8).
 */
static bool
ExtractTopLevelFieldFromDollarPath(const bson_value_t *pathValue,
								   StringView *outTopLevel)
{
	if (pathValue->value_type != BSON_TYPE_UTF8 ||
		pathValue->value.v_utf8.len < 2 ||
		pathValue->value.v_utf8.str[0] != '$' ||
		pathValue->value.v_utf8.str[1] == '$')
	{
		return false;
	}

	StringView fieldPath = {
		.string = pathValue->value.v_utf8.str + 1,
		.length = pathValue->value.v_utf8.len - 1
	};
	return ExtractTopLevelFieldPrefix(fieldPath, outTopLevel);
}


/*
 * Extracts the $unwind path's top-level field name and whether the path's
 * top-level is "_id". Returns false if the path is unusable here
 * ($$-variables, etc), or if the $unwind specifies includeArrayIndex (the
 * synthesized index field is not safe to push the optimization across).
 * preserveNullAndEmptyArrays doesn't affect required source fields.
 */
static bool
ExtractUnwindInfo(const bson_value_t *unwindStageValue,
				  StringView *outTopLevel,
				  bool *outReferencesId)
{
	*outReferencesId = false;

	UnwindArgs unwindArgs;
	UnwindParseErrorHandler onErrorIgnored = NULL;
	if (!TryParseUnwindStage(unwindStageValue, &unwindArgs, onErrorIgnored))
	{
		return false;
	}

	if (unwindArgs.includeArrayIndex.length > 0)
	{
		return false;
	}

	if (!ExtractTopLevelFieldFromDollarPath(&unwindArgs.pathValue, outTopLevel))
	{
		return false;
	}

	*outReferencesId = StringViewEquals(outTopLevel, &IdFieldStringView);
	return true;
}


/*
 * True if value provably references no source fields: null, scalars, literal
 * (no leading '$') strings, and the empty document. UTF8 starting with '$'
 * (path or $$-variable), non-empty documents, and arrays may reference fields
 * and return false; the callers handle those role-specific cases themselves.
 *
 * For a $group (context for callers): _id this also means "constant key": such an _id groups every
 * input row into a single bucket. For an accumulator argument it means the
 * arg is trivially accounted for and needs nothing collected.
 */
static bool
ValueReferencesNoSourceFields(const bson_value_t *value)
{
	switch (value->value_type)
	{
		case BSON_TYPE_NULL:
		case BSON_TYPE_INT32:
		case BSON_TYPE_INT64:
		case BSON_TYPE_DOUBLE:
		case BSON_TYPE_DECIMAL128:
		case BSON_TYPE_BOOL:
		case BSON_TYPE_DATE_TIME:
		case BSON_TYPE_TIMESTAMP:
		case BSON_TYPE_OID:
		{
			/* Examples: { $group: { _id: 42 } }, { $sum: 1 } */
			return true;
		}

		case BSON_TYPE_UTF8:
		{
			const char *s = value->value.v_utf8.str;
			uint32_t len = value->value.v_utf8.len;

			/* Examples: { _id: "label" } / { $sum: "label" } reference nothing;
			 * { _id: "$region" } / { $sum: "$price" } reference a field. */
			return len == 0 || s[0] != '$';
		}

		case BSON_TYPE_DOCUMENT:
		{
			/* Empty document references nothing ({ _id: {} }, { $count: {} }).
			 * Non-empty documents are compound _id specs / sub-expression
			 * trees that may reference fields.
			 * Example: { _id: { region: "$region" } } */
			return IsBsonValueEmptyDocument(value);
		}

		default:

			/* Example: { _id: [ "$region" ] } — arrays may reference fields. */
			return false;
	}
}


/*
 * Adds the top-level fields referenced by the $group _id and accumulator
 * args to the set. Sets *outReferencesId if any reference's top-level is
 * "_id". Returns false on unsupported shapes (duplicate _id, non-document
 * accumulator spec, complex accumulator arg, complex _id expression).
 */
static bool
CollectGroupReferencedPaths(const bson_value_t *groupStageValue,
							HTAB *consumedFields,
							bool *outReferencesId)
{
	*outReferencesId = false;

	if (groupStageValue->value_type != BSON_TYPE_DOCUMENT)
	{
		return false;
	}

	bson_iter_t groupIter;
	BsonValueInitIterator(groupStageValue, &groupIter);

	bson_value_t idValue = { 0 };
	bool sawId = false;
	while (bson_iter_next(&groupIter))
	{
		StringView keyView = bson_iter_key_string_view(&groupIter);
		if (StringViewEquals(&keyView, &IdFieldStringView))
		{
			if (sawId)
			{
				/* Example: { $group: { _id: "$a", _id: "$b" } } */
				return false;
			}
			idValue = *bson_iter_value(&groupIter);
			sawId = true;
			continue;
		}

		const bson_value_t *accSpec = bson_iter_value(&groupIter);
		if (accSpec->value_type != BSON_TYPE_DOCUMENT)
		{
			/* Example: { $group: { _id: "$a", total: 5 } } */
			return false;
		}

		/* Every accumulator must be a single-key document like { $sum: ... }.
		 * Example: { total: { $sum: "$qty" } } is fine;
		 * { total: { $sum: "$a", $avg: "$b" } } bails.
		 * TryGetSinglePgbsonElementFromBsonIterator is the canonical helper
		 * for "this BSON must have exactly one element".
		 */
		bson_iter_t accIter;
		BsonValueInitIterator(accSpec, &accIter);
		pgbsonelement accElement;
		if (!TryGetSinglePgbsonElementFromBsonIterator(&accIter, &accElement))
		{
			return false;
		}
		bool accReferencesId = false;
		if (!CollectAccumulatorArgPath(&accElement.bsonValue,
									   consumedFields,
									   &accReferencesId))
		{
			return false;
		}
		*outReferencesId = *outReferencesId || accReferencesId;
	}

	if (!sawId)
	{
		return false;
	}

	/* Constant _id values ($group{_id:null}, {_id:42}, {_id:"label"} without
	 * a leading '$', or {_id:{}}) group every input row into a single bucket
	 * and reference no source fields, so they contribute nothing to the
	 * consumed-field set. Skip TryExtractGroupByFieldPaths in that case —
	 * it would otherwise reject these forms and bail the whole rewrite. */
	if (!ValueReferencesNoSourceFields(&idValue))
	{
		StringView idKeys[INDEX_MAX_KEYS] = { 0 };
		int numIdKeys = 0;
		if (!TryExtractGroupByFieldPaths(&idValue, idKeys, INDEX_MAX_KEYS,
										 &numIdKeys))
		{
			return false;
		}

		for (int i = 0; i < numIdKeys; i++)
		{
			StringView topLevel;
			if (!AddFieldPathTopLevelToSet(consumedFields, idKeys[i],
										   &topLevel))
			{
				return false;
			}
			if (StringViewEquals(&topLevel, &IdFieldStringView))
			{
				*outReferencesId = true;
			}
		}
	}

	return true;
}


/*
 * Splices a synthetic $project before stages[unwindIdx] that keeps only
 * the top-level fields referenced by the $unwind, any $match stages
 * between it and stages[groupIdx], and the $group itself. Bails on
 * anything not provably safe to push across (complex match operators,
 * complex group accumulators, $$ROOT/$$CURRENT, joins/unions).
 *
 * Caller is responsible for detecting the eligible $unwind/$group shape.
 */
static List *
TryInjectProjectBeforeUnwindForGroup(List *aggregationStages,
									 int unwindIdx, int groupIdx,
									 AggregationPipelineBuildContext *context)
{
	HTAB *consumedFields = CreateStringViewHashSet();

	List *stages = TryInjectProjectBeforeUnwindForGroupCore(aggregationStages,
															unwindIdx, groupIdx,
															context, consumedFields);

	hash_destroy(consumedFields);

	return stages;
}


/*
 * Core implementation of TryInjectProjectBeforeUnwindForGroup. The caller owns
 * the consumedFields hashset and is responsible for destroying it; this lets
 * the various early-return bail-out paths below avoid freeing it individually.
 */
static List *
TryInjectProjectBeforeUnwindForGroupCore(List *aggregationStages,
										 int unwindIdx, int groupIdx,
										 AggregationPipelineBuildContext *context,
										 HTAB *consumedFields)
{
	List *stages = aggregationStages;

	/* Joined/nested downstream stages may need fields the project would
	 * strip. */
	if (context->joinStatus == JoinStageStatus_HasJoinsOrUnions)
	{
		return stages;
	}

	AggregationStage *unwindStage = (AggregationStage *) list_nth(stages, unwindIdx);
	AggregationStage *groupStage = (AggregationStage *) list_nth(stages, groupIdx);

	/* Synthetic $project keeps _id iff any of the unwind/match/group
	 * analyses below reports a reference to it. */
	bool includeId = false;

	StringView unwindTopLevel = { 0 };
	bool unwindReferencesId = false;
	if (!ExtractUnwindInfo(&unwindStage->stageValue, &unwindTopLevel,
						   &unwindReferencesId))
	{
		return stages;
	}
	includeId = includeId || unwindReferencesId;

	if (!AddTopLevelFieldToSet(consumedFields, unwindTopLevel))
	{
		return stages;
	}

	for (int i = unwindIdx + 1; i < groupIdx; i++)
	{
		AggregationStage *interMatch = (AggregationStage *) list_nth(stages, i);
		bool matchReferencesId = false;
		if (!CollectMatchTopLevelPaths(&interMatch->stageValue,
									   consumedFields,
									   &matchReferencesId))
		{
			return stages;
		}
		includeId = includeId || matchReferencesId;
	}

	bool groupReferencesId = false;
	if (!CollectGroupReferencedPaths(&groupStage->stageValue,
									 consumedFields,
									 &groupReferencesId))
	{
		return stages;
	}
	includeId = includeId || groupReferencesId;

	if (hash_get_num_entries(consumedFields) == 0)
	{
		return stages;
	}

	AggregationStage *projStage = BuildSyntheticInclusionProjectStage(
		consumedFields, includeId);

	return list_insert_nth(stages, unwindIdx, projStage);
}


/*
 * Builds a synthetic $project stage with inclusion spec
 * { <field>: 1, ..., _id: <0|1> } from the consumed-field hashset.
 */
static AggregationStage *
BuildSyntheticInclusionProjectStage(HTAB *consumedFields, bool includeId)
{
	pgbson_writer projWriter;
	PgbsonWriterInit(&projWriter);

	HASH_SEQ_STATUS seqStatus;
	hash_seq_init(&seqStatus, consumedFields);
	StringView *field;
	while ((field = (StringView *) hash_seq_search(&seqStatus)) != NULL)
	{
		if (StringViewEquals(field, &IdFieldStringView))
		{
			/* _id is emitted out-of-band below to honor includeId; skip
			 * here to avoid a duplicate key. */
			continue;
		}
		PgbsonWriterAppendInt32(&projWriter, field->string, field->length, 1);
	}

	PgbsonWriterAppendInt32(&projWriter, IdFieldStringView.string,
							IdFieldStringView.length, includeId ? 1 : 0);

	AggregationStage *projStage = palloc0(sizeof(AggregationStage));
	projStage->stageDefinition = (AggregationStageDefinition *) &ProjectStageDefinition;
	projStage->stageValue = ConvertPgbsonToBsonValue(
		PgbsonWriterGetPgbson(&projWriter));
	return projStage;
}
