/*
 * This file and its contents are licensed under the Apache License 2.0.
 * Please see the included NOTICE for copyright information and
 * LICENSE-APACHE for a copy of the license.
 */

/*
 *	  Optimization for FIRST/LAST aggregate functions.
 *
 * This module tries to replace FIRST/LAST aggregate functions by subqueries
 * of the form
 *		(SELECT value FROM tab
 *		 WHERE sort IS NOT NULL AND existing-quals
 *		 ORDER BY sort ASC/DESC
 *		 LIMIT 1)
 * Given a suitable index on sort column, this can be much faster than the
 * generic scan-all-the-rows aggregation plan.  We can handle multiple
 * FIRST/LAST aggregates by generating multiple subqueries, and their
 * orderings can be different.  However, if the query also contains some
 * other aggregates (eg. MIN/MAX), we will skip optimization since we can't
 * optimize across different aggregate functions.
 *
 *	  Most of the code is borrowed from:
 *	  src/backend/optimizer/plan/planagg.c
 *
 *
 */

/*
 * This file contains source code that was copied and/or modified from
 * the PostgreSQL database, which is licensed under the open-source
 * PostgreSQL License. Please see the NOTICE at the top level
 * directory for a copy of the PostgreSQL License.
 */

#include <postgres.h>

#include <access/htup_details.h>
#include <access/stratnum.h>
#include <catalog/namespace.h>
#include <catalog/pg_aggregate.h>
#include <catalog/pg_proc.h>
#include <catalog/pg_type.h>
#include <nodes/makefuncs.h>
#include <nodes/nodeFuncs.h>
#include <optimizer/cost.h>
#include <optimizer/optimizer.h>
#include <optimizer/pathnode.h>
#include <optimizer/paths.h>
#include <optimizer/planmain.h>
#include <optimizer/subselect.h>
#include <optimizer/tlist.h>
#include <parser/parse_clause.h>
#include <parser/parse_func.h>
#include <parser/parsetree.h>
#include <rewrite/rewriteManip.h>
#include <utils/builtins.h>
#include <utils/lsyscache.h>
#include <utils/regproc.h>
#include <utils/syscache.h>
#include <utils/typcache.h>

#include "compat/compat.h"
#include "extension.h"
#include "func_cache.h"
#include "planner.h"
#include "utils.h"

typedef struct FirstLastAggInfo
{
	MinMaxAggInfo *m_agg_info; /* reusing MinMaxAggInfo to avoid code
								* duplication */
	Expr *sort;				   /* Expression to use for ORDER BY */
} FirstLastAggInfo;

typedef struct MutatorContext
{
	MinMaxAggPath *mm_path;
} MutatorContext;

static bool find_first_last_aggs_walker(Node *node, List **context);
static bool build_first_last_path(PlannerInfo *root, FirstLastAggInfo *fl_info, Oid eqop,
								  Oid sortop, bool reverse_sort, bool nulls_first);
static void first_last_qp_callback(PlannerInfo *root, void *extra);
static Node *mutate_aggref_node(Node *node, MutatorContext *context);
static void replace_aggref_in_tlist(MinMaxAggPath *minmaxagg_path);

/*
 * mutate_aggref_node
 *
 * Mutator function used by recursive `expression_tree_mutator`
 * to replace Aggref node with Param node
 */
Node *
mutate_aggref_node(Node *node, MutatorContext *context)
{
	if (node == NULL)
		return NULL;
	if (IsA(node, Aggref))
	{
		Aggref *aggref = (Aggref *) node;

		/* See if the Aggref should be replaced by a Param */
		if (context->mm_path != NULL && list_length(aggref->args) == 2)
		{
			TargetEntry *curTarget = (TargetEntry *) linitial(aggref->args);
			ListCell *cell;

			foreach (cell, context->mm_path->mmaggregates)
			{
				MinMaxAggInfo *mminfo = (MinMaxAggInfo *) lfirst(cell);

				if (mminfo->aggfnoid == aggref->aggfnoid && equal(mminfo->target, curTarget->expr))
					return (Node *) copyObject(mminfo->param);
			}
		}
	}
	return expression_tree_mutator(node, mutate_aggref_node, (void *) context);
}

/*
 * replace_aggref_in_tlist
 *
 * If MinMaxAggPath is chosen, instead of running aggregate
 * function we will execute subquery that we've generated. Since we
 * use subquery we need to replace target list Aggref node with Param
 * node. Param node passes output value from the subquery.
 *
 */
void
replace_aggref_in_tlist(MinMaxAggPath *minmaxagg_path)
{
	MutatorContext context;

	context.mm_path = minmaxagg_path;

	((Path *) minmaxagg_path)->pathtarget->exprs =
		(List *) mutate_aggref_node((Node *) ((Path *) minmaxagg_path)->pathtarget->exprs,
									(void *) &context);
}

static StrategyNumber
get_func_strategy(Oid func_oid)
{
	/* Ensure function cache is initialized */
	if (!OidIsValid(ts_first_func_oid) || !OidIsValid(ts_last_func_oid))
		ts_func_cache_init();

	if (func_oid == ts_first_func_oid)
		return BTLessStrategyNumber;
	if (func_oid == ts_last_func_oid)
		return BTGreaterStrategyNumber;

	return InvalidStrategy;
}

static bool
is_first_last_node(Node *node, List **context)
{
	if (node == NULL)
		return false;
	if (IsA(node, Aggref))
	{
		Aggref *aggref = (Aggref *) node;

		if (aggref->aggfnoid == ts_first_func_oid || aggref->aggfnoid == ts_last_func_oid)
			return true;
	}
	return expression_tree_walker(node, is_first_last_node, (void *) context);
}

static bool
contains_first_last_node(List *sortClause, List *targetList)
{
	List *exprs = get_sortgrouplist_exprs(sortClause, targetList);
	ListCell *cell;
	List *context = NIL;

	foreach (cell, exprs)
	{
		Node *expr = lfirst(cell);
		bool found = is_first_last_node(expr, &context);

		if (found)
			return true;
	}
	return false;
}

/*
 * preprocess_first_last_aggregates - preprocess FIRST/LAST aggregates
 *
 * Check to see whether the query contains FIRST/LAST aggregate functions that
 * might be optimizable via index scans.  If it does, and all the aggregates
 * are potentially optimizable, then create a MinMaxAggPath(reusing MinMax path implementation)\
 * and add it to the (UPPERREL_GROUP_AGG, NULL) upperrel.
 *
 * This method is called from create_upper_paths_hook in the UPPERREL_GROUP_AGG stage.
 *
 * Note: we are passed the preprocessed targetlist separately, because it's
 * not necessarily equal to root->parse->targetList.
 *
 * Most of the code is borrowed from: preprocess_minmax_aggregates (planagg.c). Few
 * major differences:
 *  - generate FirstLastAggInfo that wraps MinMaxAggInfo
 *  - generate subquery (path) for FIRST/LAST (we reuse MinMaxAggPath)
 *  - replace Aggref node with Param node
 *	- reject ORDER BY on FIRST/LAST
 */
void
ts_preprocess_first_last_aggregates(PlannerInfo *root, List *tlist)
{
	Query *parse = root->parse;
	FromExpr *jtnode;
	RangeTblRef *rtr;
	RangeTblEntry *rte;
	List *first_last_aggs;
	RelOptInfo *grouped_rel;
	ListCell *lc;
	List *mm_agg_list;
	MinMaxAggPath *minmaxagg_path;

	/* minmax_aggs list should be empty at this point */
	Assert(root->minmax_aggs == NIL);

	/* Nothing to do if query has no aggregates */
	if (!parse->hasAggs)
		return;

	Assert(!parse->setOperations);	/* shouldn't get here if a setop */
	Assert(parse->rowMarks == NIL); /* nor if FOR UPDATE */

	/*
	 * Reject unoptimizable cases.
	 *
	 * We don't handle the case when agg function is in ORDER BY. The reason
	 * being is that we replace Aggref node before sort keys are being
	 * generated.
	 *
	 * We don't handle GROUP BY or windowing, because our current
	 * implementations of grouping require looking at all the rows anyway, and
	 * so there's not much point in optimizing FIRST/LAST.
	 */
	if (parse->groupClause || list_length(parse->groupingSets) > 1 || parse->hasWindowFuncs ||
		contains_first_last_node(parse->sortClause, tlist))
		return;

	/*
	 * Reject if query contains any CTEs; there's no way to build an indexscan
	 * on one so we couldn't succeed here.  (If the CTEs are unreferenced,
	 * that's not true, but it doesn't seem worth expending cycles to check.)
	 */
	if (parse->cteList)
		return;

	/*
	 * We also restrict the query to reference exactly one table, since join
	 * conditions can't be handled reasonably.  (We could perhaps handle a
	 * query containing cartesian-product joins, but it hardly seems worth the
	 * trouble.)  However, the single table could be buried in several levels
	 * of FromExpr due to subqueries.  Note the "single" table could be an
	 * inheritance parent, too, including the case of a UNION ALL subquery
	 * that's been flattened to an appendrel.
	 */
	jtnode = parse->jointree;
	while (IsA(jtnode, FromExpr))
	{
		if (list_length(jtnode->fromlist) != 1)
			return;
		jtnode = linitial(jtnode->fromlist);
	}
	if (!IsA(jtnode, RangeTblRef))
		return;
	rtr = (RangeTblRef *) jtnode;
	rte = planner_rt_fetch(rtr->rtindex, root);
	if (rte->rtekind == RTE_RELATION)
		/* ordinary relation, ok */;
	else if (rte->rtekind == RTE_SUBQUERY && rte->inh)
		/* flattened UNION ALL subquery, ok */;
	else
		return;

	/*
	 * Scan the tlist and HAVING qual to find all the aggregates and verify
	 * all are FIRST/LAST aggregates.  Stop as soon as we find one that isn't.
	 */
	first_last_aggs = NIL;
	if (find_first_last_aggs_walker((Node *) tlist, &first_last_aggs))
		return;
	if (find_first_last_aggs_walker(parse->havingQual, &first_last_aggs))
		return;

	/*
	 * OK, there is at least the possibility of performing the optimization.
	 * Build an access path for each aggregate.  If any of the aggregates
	 * prove to be non-indexable, give up; there is no point in optimizing
	 * just some of them.
	 */
	foreach (lc, first_last_aggs)
	{
		FirstLastAggInfo *fl_info = (FirstLastAggInfo *) lfirst(lc);
		MinMaxAggInfo *mminfo = fl_info->m_agg_info;
		Oid eqop;
		bool reverse;

		/*
		 * We'll need the equality operator that goes with the aggregate's
		 * ordering operator.
		 */
		eqop = get_equality_op_for_ordering_op(mminfo->aggsortop, &reverse);
		if (!OidIsValid(eqop)) /* shouldn't happen */
			elog(ERROR,
				 "could not find equality operator for ordering operator %u",
				 mminfo->aggsortop);

		/*
		 * We can use either an ordering that gives NULLS FIRST or one that
		 * gives NULLS LAST; furthermore there's unlikely to be much
		 * performance difference between them, so it doesn't seem worth
		 * costing out both ways if we get a hit on the first one.  NULLS
		 * FIRST is more likely to be available if the operator is a
		 * reverse-sort operator, so try that first if reverse.
		 */
		if (build_first_last_path(root, fl_info, eqop, mminfo->aggsortop, reverse, reverse))
			continue;
		if (build_first_last_path(root, fl_info, eqop, mminfo->aggsortop, reverse, !reverse))
			continue;

		/* No indexable path for this aggregate, so fail */
		return;
	}

	/*
	 * OK, we can do the query this way. We are using MinMaxAggPath to store
	 * First/Last Agg path since the logic is almost the same. MinMaxAggPath
	 * is used later on by planner so by reusing it we don't need to re-invent
	 * planner.
	 *
	 * Prepare to create a MinMaxAggPath node.
	 *
	 * First, create an output Param node for each agg.  (If we end up not
	 * using the MinMaxAggPath, we'll waste a PARAM_EXEC slot for each agg,
	 * which is not worth worrying about.  We can't wait till create_plan time
	 * to decide whether to make the Param, unfortunately.)
	 */
	mm_agg_list = NIL;
	foreach (lc, first_last_aggs)
	{
		FirstLastAggInfo *fl_info = (FirstLastAggInfo *) lfirst(lc);
		MinMaxAggInfo *mminfo = fl_info->m_agg_info;

		mminfo->param = SS_make_initplan_output_param(root,
													  exprType((Node *) mminfo->target),
													  -1,
													  exprCollation((Node *) mminfo->target));
		mm_agg_list = lcons(mminfo, mm_agg_list);
	}

	/*
	 * Create a MinMaxAggPath node with the appropriate estimated costs and
	 * other needed data, and add it to the UPPERREL_GROUP_AGG upperrel, where
	 * it will compete against the standard aggregate implementation.  (It
	 * will likely always win, but we need not assume that here.)
	 *
	 * Note: grouping_planner won't have created this upperrel yet, but it's
	 * fine for us to create it first.  We will not have inserted the correct
	 * consider_parallel value in it, but MinMaxAggPath paths are currently
	 * never parallel-safe anyway, so that doesn't matter.  Likewise, it
	 * doesn't matter that we haven't filled FDW-related fields in the rel.
	 */
	grouped_rel = fetch_upper_rel(root, UPPERREL_GROUP_AGG, NULL);
	minmaxagg_path = create_minmaxagg_path(root,
										   grouped_rel,
										   create_pathtarget(root, tlist),
										   mm_agg_list,
										   (List *) parse->havingQual);
	/* Let's replace Aggref node since we will use subquery we've generated  */
	replace_aggref_in_tlist(minmaxagg_path);
	add_path(grouped_rel, (Path *) minmaxagg_path);
}

/*
 * find_first_last_aggs_walker
 *		Recursively scan the Aggref nodes in an expression tree, and check
 *		that each one is a FIRST/LAST aggregate.  If so, build a list of the
 *		distinct aggregate calls in the tree.
 *
 * Returns TRUE if a non-FIRST/LAST aggregate is found, FALSE otherwise.
 * (This seemingly-backward definition is used because expression_tree_walker
 * aborts the scan on TRUE return, which is what we want.)
 *
 * Found aggregates are added to the list at *context; it's up to the caller
 * to initialize the list to NIL.
 *
 * This does not descend into subqueries, and so should be used only after
 * reduction of sublinks to subplans.  There mustn't be outer-aggregate
 * references either.
 *
 * Major differences from find_minmax_aggs_walker (planagg.c):
 * - only allow Aggref with two arguments
 * - wrap agg info in FirstLastAggInfo
 */
static bool
find_first_last_aggs_walker(Node *node, List **context)
{
	if (node == NULL)
		return false;
	if (IsA(node, Aggref))
	{
		Aggref *aggref = (Aggref *) node;
		Oid aggsortop;
		TargetEntry *value;
		TargetEntry *sort;
		MinMaxAggInfo *mminfo;
		ListCell *l;
		FirstLastAggInfo *fl_info;
		Oid sort_oid;
		TypeCacheEntry *sort_tce;
		StrategyNumber func_strategy;

		Assert(aggref->agglevelsup == 0);
		if (list_length(aggref->args) != 2)
			return true; /* it couldn't be first/last */

		/*
		 * ORDER BY is usually irrelevant for FIRST/LAST, but it can change
		 * the outcome if the aggsortop's operator class recognizes
		 * non-identical values as equal.  For example, 4.0 and 4.00 are equal
		 * according to numeric_ops, yet distinguishable.  If FIRST() receives
		 * more than one value equal to 4.0 and no value less than 4.0, it is
		 * unspecified which of those equal values FIRST() returns.  An ORDER
		 * BY expression that differs for each of those equal values of the
		 * argument expression makes the result predictable once again.  This
		 * is a niche requirement, and we do not implement it with subquery
		 * paths. In any case, this test lets us reject ordered-set aggregates
		 * quickly.
		 */
		if (aggref->aggorder != NIL)
			return true;
		/* note: we do not care if DISTINCT is mentioned ... */

		/*
		 * We might implement the optimization when a FILTER clause is present
		 * by adding the filter to the quals of the generated subquery.  For
		 * now, just punt.
		 */
		if (aggref->aggfilter != NULL)
			return true;

		/* We sort by second argument (eg. time) */
		sort_oid = lsecond_oid(aggref->aggargtypes);

		func_strategy = get_func_strategy(aggref->aggfnoid);
		if (func_strategy == InvalidStrategy)
			return true; /* not first/last aggregate */

		sort_tce = lookup_type_cache(sort_oid, TYPECACHE_BTREE_OPFAMILY);
		aggsortop = get_opfamily_member(sort_tce->btree_opf, sort_oid, sort_oid, func_strategy);
		if (!OidIsValid(aggsortop))
			elog(ERROR,
				 "Cannot resolve sort operator for function \"%s\" and type \"%s\"",
				 format_procedure(aggref->aggfnoid),
				 format_type_be(sort_oid));

		/* Used in projection */
		value = (TargetEntry *) linitial(aggref->args);
		/* Used in ORDER BY */
		sort = (TargetEntry *) lsecond(aggref->args);

		if (contain_mutable_functions((Node *) sort->expr))
			return true; /* not potentially indexable */

		if (type_is_rowtype(exprType((Node *) sort->expr)))
			return true; /* IS NOT NULL would have weird semantics */

		/*
		 * Check whether it's already in the list, and add it if not.
		 */
		foreach (l, *context)
		{
			mminfo = (MinMaxAggInfo *) lfirst(l);
			if (mminfo->aggfnoid == aggref->aggfnoid && equal(mminfo->target, value->expr))
				return false;
		}

		mminfo = makeNode(MinMaxAggInfo);
		mminfo->aggfnoid = aggref->aggfnoid;
		mminfo->aggsortop = aggsortop;
		mminfo->target = value->expr;
		mminfo->subroot = NULL;
		mminfo->path = NULL;
		mminfo->pathcost = 0;
		mminfo->param = NULL;

		fl_info = palloc(sizeof(FirstLastAggInfo));

		fl_info->m_agg_info = mminfo;
		fl_info->sort = sort->expr;
		*context = lappend(*context, fl_info);

		/*
		 * We need not recurse into the argument, since it can't contain any
		 * aggregates.
		 */
		return false;
	}
	Assert(!IsA(node, SubLink));
	return expression_tree_walker(node, find_first_last_aggs_walker, (void *) context);
}

/*
 * build_first_last_path
 *		Given a FIRST/LAST aggregate, try to build an indexscan Path it can be
 *		optimized with.
 *		We will generate subquery with value and sort target, where we
 *		SELECT value and we ORDER BY sort.
 *
 * If successful, stash the best path in *mminfo and return TRUE.
 * Otherwise, return FALSE.
 *
 * Major differences when compared to build_minmax_path(planagg.c):
 * - generates different subquery
 * - works with two target entries (value and sortby)
 * - resets EquivalenceClass(es)
 *
 */
static bool
build_first_last_path(PlannerInfo *root, FirstLastAggInfo *fl_info, Oid eqop, Oid sortop,
					  bool reverse_sort, bool nulls_first)
{
	PlannerInfo *subroot;
	Query *parse;
	TargetEntry *value_target;
	TargetEntry *sort_target;
	List *tlist;
	NullTest *ntest;
	SortGroupClause *sortcl;
	RelOptInfo *final_rel;
	Path *sorted_path;
	Cost path_cost;
	double path_fraction;
	MinMaxAggInfo *mminfo;
	ListCell *lc;

	/*
	 * We are going to construct what is effectively a sub-SELECT query, so
	 * clone the current query level's state and adjust it to make it look
	 * like a subquery.  Any outer references will now be one level higher
	 * than before.  (This means that when we are done, there will be no Vars
	 * of level 1, which is why the subquery can become an initplan.)
	 */
	subroot = (PlannerInfo *) palloc(sizeof(PlannerInfo));
	memcpy(subroot, root, sizeof(PlannerInfo));
	subroot->query_level++;
	subroot->parent_root = root;
	/* reset subplan-related stuff */
	subroot->plan_params = NIL;
	subroot->outer_params = NULL;
	subroot->init_plans = NIL;
	/* reset EquivalenceClass since we will create it later on */
	subroot->eq_classes = NIL;

	subroot->parse = parse = copyObject(root->parse);
	IncrementVarSublevelsUp((Node *) parse, 1, 1);

#if PG16_GE
	/* Reset placeholdersFrozen: https://github.com/postgres/postgres/commit/b3ff6c74 */
	subroot->placeholdersFrozen = false;
#endif

	/* append_rel_list might contain outer Vars? */
	subroot->append_rel_list = copyObject(root->append_rel_list);
	IncrementVarSublevelsUp((Node *) subroot->append_rel_list, 1, 1);
	/* There shouldn't be any OJ info to translate, as yet */
	Assert(subroot->join_info_list == NIL);
	/* and we haven't made equivalence classes, either */
	Assert(subroot->eq_classes == NIL);
	/* and we haven't created PlaceHolderInfos, either */
	Assert(subroot->placeholder_list == NIL);

	mminfo = fl_info->m_agg_info;

	/*----------
	 * Generate modified query of the form
	 *		(SELECT value FROM tab
	 *		 WHERE sort IS NOT NULL AND existing-quals
	 *		 ORDER BY sort ASC/DESC
	 *		 LIMIT 1)
	 *----------
	 */

	/*
	 * Value and sort target entries but sort target is eliminated later on
	 * from target list
	 */
	value_target =
		makeTargetEntry(copyObject(mminfo->target), (AttrNumber) 1, pstrdup("value"), false);
	sort_target = makeTargetEntry(copyObject(fl_info->sort), (AttrNumber) 2, pstrdup("sort"), true);
	tlist = list_make2(value_target, sort_target);
	subroot->processed_tlist = parse->targetList = tlist;

	/* No HAVING, no DISTINCT, no aggregates anymore */
	parse->havingQual = NULL;
	subroot->hasHavingQual = false;
	parse->distinctClause = NIL;
	parse->hasDistinctOn = false;
	parse->hasAggs = false;

	/*
	 * Build "sort IS NOT NULL" expression. Note that target can still be NULL.
	 * We don't need it if the order is NULLS LAST.
	 */
	if (nulls_first)
	{
		ntest = makeNode(NullTest);
		ntest->nulltesttype = IS_NOT_NULL;
		ntest->arg = copyObject(fl_info->sort);
		/* we checked it wasn't a rowtype in find_minmax_aggs_walker */
		ntest->argisrow = false;
		ntest->location = -1;

		/* User might have had that in WHERE already */
		if (!list_member((List *) parse->jointree->quals, ntest))
			parse->jointree->quals = (Node *) lcons(ntest, (List *) parse->jointree->quals);
	}

	/* Build suitable ORDER BY clause */
	sortcl = makeNode(SortGroupClause);
	sortcl->tleSortGroupRef = assignSortGroupRef(sort_target, tlist);
	sortcl->eqop = eqop;
	sortcl->sortop = sortop;
	sortcl->nulls_first = nulls_first;
	sortcl->hashable = false; /* no need to make this accurate */
#if PG18_GE
	/* Track sort direction in SortGroupClause
	 * https://github.com/postgres/postgres/commit/0d2aa4d4
	 */
	sortcl->reverse_sort = reverse_sort;
#endif
	parse->sortClause = list_make1(sortcl);

	/* set up expressions for LIMIT 1 */
	parse->limitOffset = NULL;
	parse->limitCount = (Node *)
		makeConst(INT8OID, -1, InvalidOid, sizeof(int64), Int64GetDatum(1), false, FLOAT8PASSBYVAL);

	/*
	 * Generate the best paths for this query, telling query_planner that we
	 * have LIMIT 1.
	 */
	subroot->tuple_fraction = 1.0;
	subroot->limit_tuples = 1.0;

	/* min/max optimizations usually happen before
	 * inheritance-relations are expanded, and thus query_planner will
	 * try to expand our hypertables if they are marked as
	 * inheritance-relations. Since we do not want this, we must mark
	 * hypertables as non-inheritance now.
	 */
	foreach (lc, subroot->parse->rtable)
	{
		RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);

		if (ts_rte_is_hypertable(rte))
		{
			ListCell *prev = NULL;
			ListCell *next = list_head(subroot->append_rel_list);
			Assert(rte->inh);
			rte->inh = false;
			/* query planner gets confused when entries in the
			 * append_rel_list refer to entries in the relarray that
			 * don't exist. Since we need to expand hypertables in the
			 * subquery, all of the chunk entries will be invalid in
			 * this manner, so we remove them from the list.
			 */
			/*  Performance Enhancement: This can be made non-quadratic by:
			 *  1) Loop once over all RTEs, storing the relid of any RTE that is a hypertable in
			 * a bitset and setting its 'inh' flag to false 2) Loop over the append_rel_list,
			 * removing any AppendRelInfo that has a parent relid which is in the previously
			 * created bitset (i.e., is a hypertable)
			 */
			while (next != NULL)
			{
				AppendRelInfo *app = lfirst(next);
				if (app->parent_reloid == rte->relid)
				{
					subroot->append_rel_list = list_delete_cell(subroot->append_rel_list, next);
					next = prev != NULL ? lnext(subroot->append_rel_list, next) :
										  list_head(subroot->append_rel_list);
				}
				else
				{
					prev = next;
					next = lnext(subroot->append_rel_list, next);
				}
			}
		}
	}

	final_rel = query_planner(subroot, first_last_qp_callback, NULL);

	/* we need to disable inheritance so the chunks are re-expanded correctly in the subroot */
	foreach (lc, root->parse->rtable)
	{
		RangeTblEntry *rte = lfirst_node(RangeTblEntry, lc);

		if (ts_rte_is_hypertable(rte))
			rte->inh = true;
	}

	/*
	 * Since we didn't go through subquery_planner() to handle the subquery,
	 * we have to do some of the same cleanup it would do, in particular cope
	 * with params and initplans used within this subquery.  (This won't
	 * matter if we end up not using the subplan.)
	 */
	SS_identify_outer_params(subroot);
	SS_charge_for_initplans(subroot, final_rel);

	/*
	 * Get the best presorted path, that being the one that's cheapest for
	 * fetching just one row.  If there's no such path, fail.
	 */
	if (final_rel->rows > 1.0)
		path_fraction = 1.0 / final_rel->rows;
	else
		path_fraction = 1.0;

	sorted_path = get_cheapest_fractional_path_for_pathkeys(final_rel->pathlist,
															subroot->query_pathkeys,
															NULL,
															path_fraction);
	if (!sorted_path)
		return false;

	/*
	 * The path might not return exactly what we want, so fix that.  (We
	 * assume that this won't change any conclusions about which was the
	 * cheapest path.)
	 */
	sorted_path = apply_projection_to_path(subroot,
										   final_rel,
										   sorted_path,
										   create_pathtarget(subroot, tlist));

	/*
	 * Determine cost to get just the first row of the presorted path.
	 *
	 * Note: cost calculation here should match
	 * compare_fractional_path_costs().
	 */
	path_cost = sorted_path->startup_cost +
				path_fraction * (sorted_path->total_cost - sorted_path->startup_cost);

	/* Save state for further processing */
	mminfo->subroot = subroot;
	mminfo->path = sorted_path;
	mminfo->pathcost = path_cost;

	return true;
}

/*
 * Compute query_pathkeys and other pathkeys during query_planner()
 */
static void
first_last_qp_callback(PlannerInfo *root, void *extra)
{
	root->group_pathkeys = NIL;
	root->window_pathkeys = NIL;
	root->distinct_pathkeys = NIL;

	root->sort_pathkeys =
		make_pathkeys_for_sortclauses(root, root->parse->sortClause, root->parse->targetList);

	root->query_pathkeys = root->sort_pathkeys;
}
