/*-------------------------------------------------------------------------
 *
 * function.c
 *    Commands for FUNCTION statements.
 *
 *    We currently support replicating function definitions on the
 *    coordinator in all the worker nodes in the form of
 *
 *    CREATE OR REPLACE FUNCTION ... queries and
 *    GRANT ... ON FUNCTION queries
 *
 *
 *    ALTER or DROP operations are not yet propagated.
 *
 * Copyright (c) Citus Data, Inc.
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "funcapi.h"
#include "miscadmin.h"

#include "access/genam.h"
#include "access/htup_details.h"
#include "access/xact.h"
#include "catalog/dependency.h"
#include "catalog/namespace.h"
#include "catalog/pg_aggregate.h"
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "commands/extension.h"
#include "nodes/makefuncs.h"
#include "parser/parse_coerce.h"
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/fmgrprotos.h"
#include "utils/lsyscache.h"
#include "utils/regproc.h"
#include "utils/syscache.h"

#include "pg_version_constants.h"

#include "distributed/citus_depended_object.h"
#include "distributed/citus_ruleutils.h"
#include "distributed/citus_safe_lib.h"
#include "distributed/colocation_utils.h"
#include "distributed/commands.h"
#include "distributed/commands/utility_hook.h"
#include "distributed/coordinator_protocol.h"
#include "distributed/deparser.h"
#include "distributed/listutils.h"
#include "distributed/maintenanced.h"
#include "distributed/metadata/dependency.h"
#include "distributed/metadata/distobject.h"
#include "distributed/metadata/pg_dist_object.h"
#include "distributed/metadata_sync.h"
#include "distributed/metadata_utility.h"
#include "distributed/multi_executor.h"
#include "distributed/namespace_utils.h"
#include "distributed/pg_dist_node.h"
#include "distributed/reference_table_utils.h"
#include "distributed/relation_access_tracking.h"
#include "distributed/version_compat.h"
#include "distributed/worker_create_or_replace.h"
#include "distributed/worker_transaction.h"

#define DISABLE_LOCAL_CHECK_FUNCTION_BODIES "SET LOCAL check_function_bodies TO off;"
#define RESET_CHECK_FUNCTION_BODIES "RESET check_function_bodies;"
#define argumentStartsWith(arg, prefix) \
	(strncmp(arg, prefix, strlen(prefix)) == 0)

/* forward declaration for helper functions*/
static bool RecreateSameNonColocatedFunction(ObjectAddress functionAddress,
											 char *distributionArgumentName,
											 bool colocateWithTableNameDefault,
											 bool *forceDelegationAddress);
static void ErrorIfAnyNodeDoesNotHaveMetadata(void);
static char * GetAggregateDDLCommand(const RegProcedure funcOid, bool useCreateOrReplace);
static char * GetFunctionAlterOwnerCommand(const RegProcedure funcOid);
static int GetDistributionArgIndex(Oid functionOid, char *distributionArgumentName,
								   Oid *distributionArgumentOid);
static int GetFunctionColocationId(Oid functionOid, char *colocateWithName, Oid
								   distributionArgumentOid);
static void EnsureFunctionCanBeColocatedWithTable(Oid functionOid, Oid
												  distributionColumnType, Oid
												  sourceRelationId);
static bool ShouldPropagateCreateFunction(CreateFunctionStmt *stmt);
static bool ShouldPropagateAlterFunction(const ObjectAddress *address);
static bool ShouldAddFunctionSignature(FunctionParameterMode mode);
static List * FunctionToObjectAddress(ObjectType objectType,
									  ObjectWithArgs *objectWithArgs,
									  bool missing_ok);
static void ErrorIfUnsupportedAlterFunctionStmt(AlterFunctionStmt *stmt);
static char * quote_qualified_func_name(Oid funcOid);
static void DistributeFunctionWithDistributionArgument(RegProcedure funcOid,
													   char *distributionArgumentName,
													   Oid distributionArgumentOid,
													   char *colocateWithTableName,
													   bool *forceDelegationAddress,
													   const ObjectAddress *
													   functionAddress);
static void DistributeFunctionColocatedWithDistributedTable(RegProcedure funcOid,
															char *colocateWithTableName,
															const ObjectAddress *
															functionAddress);
static void DistributeFunctionColocatedWithSingleShardTable(const
															ObjectAddress *functionAddress,
															text *colocateWithText);
static void DistributeFunctionColocatedWithReferenceTable(const
														  ObjectAddress *functionAddress);
static List * FilterDistributedFunctions(GrantStmt *grantStmt);

static void EnsureExtensionFunctionCanBeDistributed(const ObjectAddress functionAddress,
													const ObjectAddress extensionAddress,
													char *distributionArgumentName);

PG_FUNCTION_INFO_V1(create_distributed_function);


/*
 * create_distributed_function gets a function or procedure name with their list of
 * argument types in parantheses, then it creates a new distributed function.
 */
Datum
create_distributed_function(PG_FUNCTION_ARGS)
{
	RegProcedure funcOid = PG_GETARG_OID(0);

	text *distributionArgumentNameText = NULL; /* optional */
	text *colocateWithText = NULL; /* optional */

	StringInfoData ddlCommand = { 0 };
	ObjectAddress *functionAddress = palloc0(sizeof(ObjectAddress));

	Oid distributionArgumentOid = InvalidOid;
	bool colocatedWithReferenceTable = false;
	bool colocatedWithSingleShardTable = false;

	char *distributionArgumentName = NULL;
	char *colocateWithTableName = NULL;
	bool colocateWithTableNameDefault = false;
	bool *forceDelegationAddress = NULL;
	bool forceDelegation = false;
	ObjectAddress extensionAddress = { 0 };

	/* if called on NULL input, error out */
	if (funcOid == InvalidOid)
	{
		ereport(ERROR, (errmsg("the first parameter for create_distributed_function() "
							   "should be a single a valid function or procedure name "
							   "followed by a list of parameters in parantheses"),
						errhint("skip the parameters with OUT argtype as they are not "
								"part of the signature in PostgreSQL")));
	}

	if (PG_ARGISNULL(1))
	{
		/*
		 * Using the default value, so distribute the function but do not set
		 * the distribution argument.
		 */
		distributionArgumentName = NULL;
	}
	else
	{
		distributionArgumentNameText = PG_GETARG_TEXT_P(1);
		distributionArgumentName = text_to_cstring(distributionArgumentNameText);
	}

	if (PG_ARGISNULL(2))
	{
		ereport(ERROR, (errmsg("colocate_with parameter should not be NULL"),
						errhint("To use the default value, set colocate_with option "
								"to \"default\"")));
	}
	else
	{
		colocateWithText = PG_GETARG_TEXT_P(2);
		colocateWithTableName = text_to_cstring(colocateWithText);

		if (pg_strncasecmp(colocateWithTableName, "default", NAMEDATALEN) == 0)
		{
			colocateWithTableNameDefault = true;
		}

		/* check if the colocation belongs to a reference table */
		if (!colocateWithTableNameDefault)
		{
			Oid colocationRelationId = ResolveRelationId(colocateWithText, false);
			colocatedWithReferenceTable = IsCitusTableType(colocationRelationId,
														   REFERENCE_TABLE);
			colocatedWithSingleShardTable = IsCitusTableType(colocationRelationId,
															 SINGLE_SHARD_DISTRIBUTED);
		}
	}

	/* check if the force_delegation flag is explicitly set (default is NULL) */
	if (PG_ARGISNULL(3))
	{
		forceDelegationAddress = NULL;
	}
	else
	{
		forceDelegation = PG_GETARG_BOOL(3);
		forceDelegationAddress = &forceDelegation;
	}

	EnsureCoordinator();
	EnsureFunctionOwner(funcOid);

	ObjectAddressSet(*functionAddress, ProcedureRelationId, funcOid);

	if (RecreateSameNonColocatedFunction(*functionAddress,
										 distributionArgumentName,
										 colocateWithTableNameDefault,
										 forceDelegationAddress))
	{
		char *schemaName = get_namespace_name(get_func_namespace(funcOid));
		char *functionName = get_func_name(funcOid);
		char *qualifiedName = quote_qualified_identifier(schemaName, functionName);
		ereport(NOTICE, (errmsg("procedure %s is already distributed", qualifiedName),
						 errdetail("Citus distributes procedures with CREATE "
								   "[PROCEDURE|FUNCTION|AGGREGATE] commands")));
		PG_RETURN_VOID();
	}

	/*
	 * If the function is owned by an extension, only update the
	 * pg_dist_object, and not propagate the CREATE FUNCTION. Function
	 * will be created by the virtue of the extension creation.
	 */
	if (IsAnyObjectAddressOwnedByExtension(list_make1(functionAddress),
										   &extensionAddress))
	{
		EnsureExtensionFunctionCanBeDistributed(*functionAddress, extensionAddress,
												distributionArgumentName);
	}
	else
	{
		/*
		 * when we allow propagation within a transaction block we should make sure
		 * to only allow this in sequential mode.
		 */
		EnsureSequentialMode(OBJECT_FUNCTION);

		EnsureAllObjectDependenciesExistOnAllNodes(list_make1(functionAddress));

		const char *createFunctionSQL = GetFunctionDDLCommand(funcOid, true);
		const char *alterFunctionOwnerSQL = GetFunctionAlterOwnerCommand(funcOid);
		initStringInfo(&ddlCommand);
		appendStringInfo(&ddlCommand, "%s;%s;%s", DISABLE_METADATA_SYNC,
						 createFunctionSQL, alterFunctionOwnerSQL);
		List *grantDDLCommands = GrantOnFunctionDDLCommands(funcOid);
		char *grantOnFunctionSQL = NULL;
		foreach_declared_ptr(grantOnFunctionSQL, grantDDLCommands)
		{
			appendStringInfo(&ddlCommand, ";%s", grantOnFunctionSQL);
		}

		appendStringInfo(&ddlCommand, ";%s", ENABLE_METADATA_SYNC);

		SendCommandToWorkersAsUser(NON_COORDINATOR_NODES, CurrentUserName(),
								   ddlCommand.data);
	}

	MarkObjectDistributed(functionAddress);

	if (distributionArgumentName != NULL)
	{
		/*
		 * Prior to Citus 11, this code was triggering metadata
		 * syncing. However, with Citus 11+, we expect the metadata
		 * has already been synced.
		 */
		ErrorIfAnyNodeDoesNotHaveMetadata();

		DistributeFunctionWithDistributionArgument(funcOid, distributionArgumentName,
												   distributionArgumentOid,
												   colocateWithTableName,
												   forceDelegationAddress,
												   functionAddress);
	}
	else if (!colocatedWithReferenceTable && !colocatedWithSingleShardTable)
	{
		DistributeFunctionColocatedWithDistributedTable(funcOid, colocateWithTableName,
														functionAddress);
	}
	else if (colocatedWithSingleShardTable)
	{
		DistributeFunctionColocatedWithSingleShardTable(functionAddress,
														colocateWithText);
	}
	else if (colocatedWithReferenceTable)
	{
		/*
		 * Prior to Citus 11, this code was triggering metadata
		 * syncing. However, with Citus 11+, we expect the metadata
		 * has already been synced.
		 */
		ErrorIfAnyNodeDoesNotHaveMetadata();

		DistributeFunctionColocatedWithReferenceTable(functionAddress);
	}

	PG_RETURN_VOID();
}


/*
 * RecreateSameNonColocatedFunction returns true if the given parameters of
 * create_distributed_function will not change anything on the given function.
 * Returns false otherwise.
 */
static bool
RecreateSameNonColocatedFunction(ObjectAddress functionAddress,
								 char *distributionArgumentName,
								 bool colocateWithTableNameDefault,
								 bool *forceDelegationAddress)
{
	DistObjectCacheEntry *cacheEntry =
		LookupDistObjectCacheEntry(ProcedureRelationId,
								   functionAddress.objectId,
								   InvalidOid);

	if (cacheEntry == NULL || !cacheEntry->isValid || !cacheEntry->isDistributed)
	{
		return false;
	}

	/*
	 * If the colocationId, forceDelegation and distributionArgIndex fields of a
	 * pg_dist_object entry of a distributed function are all set to zero, it means
	 * that function is either automatically distributed by ddl propagation, without
	 * calling create_distributed_function. Or, it could be distributed via
	 * create_distributed_function, but with no parameters.
	 *
	 * For these cases, calling create_distributed_function for that function,
	 * without parameters would be idempotent. Hence we can simply early return here,
	 * by providing a notice message to the user.
	 */

	/* are pg_dist_object fields set to zero? */
	bool functionDistributedWithoutParams =
		cacheEntry->colocationId == 0 &&
		cacheEntry->forceDelegation == 0 &&
		cacheEntry->distributionArgIndex == 0;

	/* called create_distributed_function without parameters? */
	bool distributingAgainWithNoParams =
		distributionArgumentName == NULL &&
		colocateWithTableNameDefault &&
		forceDelegationAddress == NULL;

	return functionDistributedWithoutParams && distributingAgainWithNoParams;
}


/*
 * ErrorIfAnyNodeDoesNotHaveMetadata throws error if any
 * of the worker nodes does not have the metadata.
 */
static void
ErrorIfAnyNodeDoesNotHaveMetadata(void)
{
	List *workerNodeList =
		ActivePrimaryNonCoordinatorNodeList(ShareLock);

	WorkerNode *workerNode = NULL;
	foreach_declared_ptr(workerNode, workerNodeList)
	{
		if (!workerNode->hasMetadata)
		{
			ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
							errmsg("cannot process the distributed function "
								   "since the node %s:%d does not have metadata "
								   "synced and this command requires all the nodes "
								   "have the metadata sycned", workerNode->workerName,
								   workerNode->workerPort),
							errhint("To sync the metadata execute: "
									"SELECT enable_citus_mx_for_pre_citus11();")));
		}
	}
}


/*
 * DistributeFunctionWithDistributionArgument updates pg_dist_object records for
 * a function/procedure that has a distribution argument, and triggers metadata
 * sync so that the functions can be delegated on workers.
 */
static void
DistributeFunctionWithDistributionArgument(RegProcedure funcOid,
										   char *distributionArgumentName,
										   Oid distributionArgumentOid,
										   char *colocateWithTableName,
										   bool *forceDelegationAddress,
										   const ObjectAddress *functionAddress)
{
	/* get the argument index, or error out if we cannot find a valid index */
	int distributionArgumentIndex =
		GetDistributionArgIndex(funcOid, distributionArgumentName,
								&distributionArgumentOid);

	/* get the colocation id, or error out if we cannot find an appropriate one */
	int colocationId =
		GetFunctionColocationId(funcOid, colocateWithTableName,
								distributionArgumentOid);

	/* record the distribution argument and colocationId */
	UpdateFunctionDistributionInfo(functionAddress, &distributionArgumentIndex,
								   &colocationId,
								   forceDelegationAddress);
}


/*
 * DistributeFunctionColocatedWithDistributedTable updates pg_dist_object records for
 * a function/procedure that is colocated with a distributed table.
 */
static void
DistributeFunctionColocatedWithDistributedTable(RegProcedure funcOid,
												char *colocateWithTableName,
												const ObjectAddress *functionAddress)
{
	/*
	 * cannot provide colocate_with without distribution_arg_name when the function
	 * is not colocated with a reference table
	 */
	if (pg_strncasecmp(colocateWithTableName, "default", NAMEDATALEN) != 0)
	{
		char *functionName = get_func_name(funcOid);


		ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						errmsg("cannot distribute the function \"%s\" since the "
							   "distribution argument is not valid ", functionName),
						errhint("To provide \"colocate_with\" option with a"
								" distributed table, the distribution argument"
								" parameter should also be provided")));
	}

	/* set distribution argument and colocationId to NULL */
	UpdateFunctionDistributionInfo(functionAddress, NULL, NULL, NULL);
}


/*
 * DistributeFunctionColocatedWithSingleShardTable updates pg_dist_object records for
 * a function/procedure that is colocated with a single shard table.
 */
static void
DistributeFunctionColocatedWithSingleShardTable(const ObjectAddress *functionAddress,
												text *colocateWithText)
{
	/* get the single shard table's colocation id */
	int colocationId = TableColocationId(ResolveRelationId(colocateWithText, false));

	/* set distribution argument to NULL */
	int *distributionArgumentIndex = NULL;
	UpdateFunctionDistributionInfo(functionAddress, distributionArgumentIndex,
								   &colocationId,
								   NULL);
}


/*
 * DistributeFunctionColocatedWithReferenceTable updates pg_dist_object records for
 * a function/procedure that is colocated with a reference table.
 */
static void
DistributeFunctionColocatedWithReferenceTable(const ObjectAddress *functionAddress)
{
	/* get the reference table colocation id */
	int colocationId = CreateReferenceTableColocationId();

	/* set distribution argument to NULL and colocationId to the reference table colocation id */
	int *distributionArgumentIndex = NULL;
	UpdateFunctionDistributionInfo(functionAddress, distributionArgumentIndex,
								   &colocationId,
								   NULL);
}


/*
 * CreateFunctionDDLCommandsIdempotent returns a list of DDL statements (const char *) to be
 * executed on a node to recreate the function addressed by the functionAddress.
 */
List *
CreateFunctionDDLCommandsIdempotent(const ObjectAddress *functionAddress)
{
	Assert(functionAddress->classId == ProcedureRelationId);

	char *ddlCommand = GetFunctionDDLCommand(functionAddress->objectId, true);
	char *alterFunctionOwnerSQL = GetFunctionAlterOwnerCommand(functionAddress->objectId);

	return list_make4(
		DISABLE_LOCAL_CHECK_FUNCTION_BODIES,
		ddlCommand,
		alterFunctionOwnerSQL,
		RESET_CHECK_FUNCTION_BODIES);
}


/*
 * GetDistributionArgIndex calculates the distribution argument with the given
 * parameters. The function errors out if no valid argument is found.
 */
static int
GetDistributionArgIndex(Oid functionOid, char *distributionArgumentName,
						Oid *distributionArgumentOid)
{
	int distributionArgumentIndex = -1;

	Oid *argTypes = NULL;
	char **argNames = NULL;
	char *argModes = NULL;


	*distributionArgumentOid = InvalidOid;

	HeapTuple proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(functionOid));
	if (!HeapTupleIsValid(proctup))
	{
		elog(ERROR, "cache lookup failed for function %u", functionOid);
	}

	int numberOfArgs = get_func_arg_info(proctup, &argTypes, &argNames, &argModes);

	if (argumentStartsWith(distributionArgumentName, "$"))
	{
		/* skip the first character, we're safe because text_to_cstring pallocs */
		distributionArgumentName++;

		/* throws error if the input is not an integer */
		distributionArgumentIndex = pg_strtoint32(distributionArgumentName);

		if (distributionArgumentIndex < 1 || distributionArgumentIndex > numberOfArgs)
		{
			char *functionName = get_func_name(functionOid);

			ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
							errmsg("cannot distribute the function \"%s\" since "
								   "the distribution argument is not valid",
								   functionName),
							errhint("Either provide a valid function argument name "
									"or a valid \"$paramIndex\" to "
									"create_distributed_function()")));
		}

		/*
		 * Internal representation for the distributionArgumentIndex
		 * starts from 0 whereas user facing API starts from 1.
		 */
		distributionArgumentIndex -= 1;
		*distributionArgumentOid = argTypes[distributionArgumentIndex];

		ReleaseSysCache(proctup);

		Assert(*distributionArgumentOid != InvalidOid);

		return distributionArgumentIndex;
	}

	/*
	 * The user didn't provid "$paramIndex" but potentially the name of the parameter.
	 * So, loop over the arguments and try to find the argument name that matches
	 * the parameter that user provided.
	 */
	for (int argIndex = 0; argIndex < numberOfArgs; ++argIndex)
	{
		char *argNameOnIndex = argNames != NULL ? argNames[argIndex] : NULL;

		if (argNameOnIndex != NULL &&
			pg_strncasecmp(argNameOnIndex, distributionArgumentName, NAMEDATALEN) == 0)
		{
			distributionArgumentIndex = argIndex;

			*distributionArgumentOid = argTypes[argIndex];

			/* we found, no need to continue */
			break;
		}
	}

	/* we still couldn't find the argument, so error out */
	if (distributionArgumentIndex == -1)
	{
		char *functionName = get_func_name(functionOid);

		ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
						errmsg("cannot distribute the function \"%s\" since the "
							   "distribution argument is not valid ", functionName),
						errhint("Either provide a valid function argument name "
								"or a valid \"$paramIndex\" to "
								"create_distributed_function()")));
	}

	ReleaseSysCache(proctup);

	Assert(*distributionArgumentOid != InvalidOid);

	return distributionArgumentIndex;
}


/*
 * GetFunctionColocationId gets the parameters for deciding the colocationId
 * of the function that is being distributed. The function errors out if it is
 * not possible to assign a colocationId to the input function.
 */
static int
GetFunctionColocationId(Oid functionOid, char *colocateWithTableName,
						Oid distributionArgumentOid)
{
	int colocationId = INVALID_COLOCATION_ID;
	Relation pgDistColocation = table_open(DistColocationRelationId(), ShareLock);

	if (pg_strncasecmp(colocateWithTableName, "default", NAMEDATALEN) == 0)
	{
		/* check for default colocation group */
		colocationId = ColocationId(ShardCount, ShardReplicationFactor,
									distributionArgumentOid, get_typcollation(
										distributionArgumentOid));

		if (colocationId == INVALID_COLOCATION_ID)
		{
			char *functionName = get_func_name(functionOid);

			ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
							errmsg("cannot distribute the function \"%s\" since there "
								   "is no table to colocate with", functionName),
							errhint("Provide a distributed table via \"colocate_with\" "
									"option to create_distributed_function()")));
		}

		Oid colocatedTableId = ColocatedTableId(colocationId);
		if (colocatedTableId != InvalidOid)
		{
			EnsureFunctionCanBeColocatedWithTable(functionOid, distributionArgumentOid,
												  colocatedTableId);
		}
	}
	else
	{
		Oid sourceRelationId =
			ResolveRelationId(cstring_to_text(colocateWithTableName), false);

		EnsureFunctionCanBeColocatedWithTable(functionOid, distributionArgumentOid,
											  sourceRelationId);

		colocationId = TableColocationId(sourceRelationId);
	}

	/* keep the lock */
	table_close(pgDistColocation, NoLock);

	return colocationId;
}


/*
 * EnsureFunctionCanBeColocatedWithTable checks whether the given arguments are
 * suitable to distribute the function to be colocated with given source table.
 */
static void
EnsureFunctionCanBeColocatedWithTable(Oid functionOid, Oid distributionColumnType,
									  Oid sourceRelationId)
{
	CitusTableCacheEntry *sourceTableEntry = GetCitusTableCacheEntry(sourceRelationId);
	char sourceReplicationModel = sourceTableEntry->replicationModel;

	if (IsCitusTableTypeCacheEntry(sourceTableEntry, SINGLE_SHARD_DISTRIBUTED) &&
		distributionColumnType != InvalidOid)
	{
		char *functionName = get_func_name(functionOid);
		char *sourceRelationName = get_rel_name(sourceRelationId);

		ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						errmsg("cannot colocate function \"%s\" and table \"%s\" because "
							   "distribution arguments are not supported when "
							   "colocating with single shard distributed tables.",
							   functionName, sourceRelationName)));
	}

	if (!IsCitusTableTypeCacheEntry(sourceTableEntry, HASH_DISTRIBUTED) &&
		!IsCitusTableTypeCacheEntry(sourceTableEntry, REFERENCE_TABLE))
	{
		char *functionName = get_func_name(functionOid);
		char *sourceRelationName = get_rel_name(sourceRelationId);

		ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						errmsg("cannot colocate function \"%s\" and table \"%s\" because "
							   "colocate_with option is only supported for hash "
							   "distributed tables and reference tables.",
							   functionName, sourceRelationName)));
	}

	if (IsCitusTableTypeCacheEntry(sourceTableEntry, REFERENCE_TABLE) &&
		distributionColumnType != InvalidOid)
	{
		char *functionName = get_func_name(functionOid);
		char *sourceRelationName = get_rel_name(sourceRelationId);

		ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						errmsg("cannot colocate function \"%s\" and table \"%s\" because "
							   "distribution arguments are not supported when "
							   "colocating with reference tables.",
							   functionName, sourceRelationName)));
	}

	if (sourceReplicationModel != REPLICATION_MODEL_STREAMING)
	{
		char *functionName = get_func_name(functionOid);
		char *sourceRelationName = get_rel_name(sourceRelationId);

		ereport(ERROR, (errmsg("cannot colocate function \"%s\" and table \"%s\"",
							   functionName, sourceRelationName),
						errdetail("Citus currently only supports colocating function "
								  "with distributed tables that are created using "
								  "streaming replication model."),
						errhint("When distributing tables make sure that "
								"citus.shard_replication_factor = 1")));
	}

	/*
	 * If the types are the same, we're good. If not, we still check if there
	 * is any coercion path between the types.
	 */
	Var *sourceDistributionColumn = DistPartitionKeyOrError(sourceRelationId);
	Oid sourceDistributionColumnType = sourceDistributionColumn->vartype;
	if (sourceDistributionColumnType != distributionColumnType)
	{
		Oid coercionFuncId = InvalidOid;

		CoercionPathType coercionType =
			find_coercion_pathway(distributionColumnType, sourceDistributionColumnType,
								  COERCION_EXPLICIT, &coercionFuncId);

		/* if there is no path for coercion, error out*/
		if (coercionType == COERCION_PATH_NONE)
		{
			char *functionName = get_func_name(functionOid);
			char *sourceRelationName = get_rel_name(sourceRelationId);

			ereport(ERROR, (errmsg("cannot colocate function \"%s\" and table \"%s\" "
								   "because distribution column types don't match and "
								   "there is no coercion path", sourceRelationName,
								   functionName)));
		}
	}
}


/*
 * UpdateFunctionDistributionInfo gets object address of a function and
 * updates its distribution_argument_index and colocationId in pg_dist_object.
 * Then update pg_dist_object on nodes with metadata if object propagation is on.
 */
void
UpdateFunctionDistributionInfo(const ObjectAddress *distAddress,
							   int *distribution_argument_index,
							   int *colocationId,
							   bool *forceDelegation)
{
	const bool indexOK = true;

	ScanKeyData scanKey[3];
	Datum values[Natts_pg_dist_object];
	bool isnull[Natts_pg_dist_object];
	bool replace[Natts_pg_dist_object];

	Relation pgDistObjectRel = table_open(DistObjectRelationId(), RowExclusiveLock);
	TupleDesc tupleDescriptor = RelationGetDescr(pgDistObjectRel);

	/* scan pg_dist_object for classid = $1 AND objid = $2 AND objsubid = $3 via index */
	ScanKeyInit(&scanKey[0], Anum_pg_dist_object_classid, BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(distAddress->classId));
	ScanKeyInit(&scanKey[1], Anum_pg_dist_object_objid, BTEqualStrategyNumber, F_OIDEQ,
				ObjectIdGetDatum(distAddress->objectId));
	ScanKeyInit(&scanKey[2], Anum_pg_dist_object_objsubid, BTEqualStrategyNumber,
				F_INT4EQ, Int32GetDatum(distAddress->objectSubId));

	SysScanDesc scanDescriptor = systable_beginscan(pgDistObjectRel,
													DistObjectPrimaryKeyIndexId(),
													indexOK,
													NULL, 3, scanKey);

	HeapTuple heapTuple = systable_getnext(scanDescriptor);
	if (!HeapTupleIsValid(heapTuple))
	{
		ereport(ERROR, (errmsg("could not find valid entry for node \"%d,%d,%d\" "
							   "in pg_dist_object", distAddress->classId,
							   distAddress->objectId, distAddress->objectSubId)));
	}

	memset(values, 0, sizeof(values));
	memset(isnull, 0, sizeof(isnull));
	memset(replace, 0, sizeof(replace));

	replace[Anum_pg_dist_object_distribution_argument_index - 1] = true;

	if (distribution_argument_index != NULL)
	{
		values[Anum_pg_dist_object_distribution_argument_index - 1] = Int32GetDatum(
			*distribution_argument_index);
		isnull[Anum_pg_dist_object_distribution_argument_index - 1] = false;
	}
	else
	{
		isnull[Anum_pg_dist_object_distribution_argument_index - 1] = true;
	}

	replace[Anum_pg_dist_object_colocationid - 1] = true;
	if (colocationId != NULL)
	{
		values[Anum_pg_dist_object_colocationid - 1] = Int32GetDatum(*colocationId);
		isnull[Anum_pg_dist_object_colocationid - 1] = false;
	}
	else
	{
		isnull[Anum_pg_dist_object_colocationid - 1] = true;
	}

	replace[Anum_pg_dist_object_force_delegation - 1] = true;
	if (forceDelegation != NULL)
	{
		values[Anum_pg_dist_object_force_delegation - 1] = BoolGetDatum(
			*forceDelegation);
		isnull[Anum_pg_dist_object_force_delegation - 1] = false;
	}
	else
	{
		isnull[Anum_pg_dist_object_force_delegation - 1] = true;
	}

	heapTuple = heap_modify_tuple(heapTuple, tupleDescriptor, values, isnull, replace);

	CatalogTupleUpdate(pgDistObjectRel, &heapTuple->t_self, heapTuple);

	CitusInvalidateRelcacheByRelid(DistObjectRelationId());

	CommandCounterIncrement();

	systable_endscan(scanDescriptor);

	table_close(pgDistObjectRel, NoLock);

	if (EnableMetadataSync)
	{
		List *objectAddressList = list_make1((ObjectAddress *) distAddress);
		List *distArgumentIndexList = NIL;
		List *colocationIdList = NIL;
		List *forceDelegationList = NIL;

		if (distribution_argument_index == NULL)
		{
			distArgumentIndexList = list_make1_int(INVALID_DISTRIBUTION_ARGUMENT_INDEX);
		}
		else
		{
			distArgumentIndexList = list_make1_int(*distribution_argument_index);
		}

		if (colocationId == NULL)
		{
			colocationIdList = list_make1_int(INVALID_COLOCATION_ID);
		}
		else
		{
			colocationIdList = list_make1_int(*colocationId);
		}

		if (forceDelegation == NULL)
		{
			forceDelegationList = list_make1_int(NO_FORCE_PUSHDOWN);
		}
		else
		{
			forceDelegationList = list_make1_int(*forceDelegation);
		}

		char *workerPgDistObjectUpdateCommand =
			MarkObjectsDistributedCreateCommand(objectAddressList,
												NIL,
												distArgumentIndexList,
												colocationIdList,
												forceDelegationList);
		SendCommandToWorkersWithMetadata(workerPgDistObjectUpdateCommand);
	}
}


/*
 * GetFunctionDDLCommand returns the complete "CREATE OR REPLACE FUNCTION ..." statement for
 * the specified function.
 *
 * useCreateOrReplace is ignored for non-aggregate functions.
 */
char *
GetFunctionDDLCommand(const RegProcedure funcOid, bool useCreateOrReplace)
{
	char *createFunctionSQL = NULL;

	if (get_func_prokind(funcOid) == PROKIND_AGGREGATE)
	{
		createFunctionSQL = GetAggregateDDLCommand(funcOid, useCreateOrReplace);
	}
	else
	{
		Datum sqlTextDatum = (Datum) 0;
		int saveNestLevel = PushEmptySearchPath();

		sqlTextDatum = DirectFunctionCall1(pg_get_functiondef,
										   ObjectIdGetDatum(funcOid));
		createFunctionSQL = TextDatumGetCString(sqlTextDatum);

		/* revert back to original search_path */
		PopEmptySearchPath(saveNestLevel);
	}

	return createFunctionSQL;
}


/*
 * GetFunctionAlterOwnerCommand returns "ALTER FUNCTION .. SET OWNER .." statement for
 * the specified function.
 */
static char *
GetFunctionAlterOwnerCommand(const RegProcedure funcOid)
{
	HeapTuple proctup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcOid));
	StringInfo alterCommand = makeStringInfo();
	Oid procOwner = InvalidOid;


	if (HeapTupleIsValid(proctup))
	{
		Form_pg_proc procform = (Form_pg_proc) GETSTRUCT(proctup);

		procOwner = procform->proowner;

		ReleaseSysCache(proctup);
	}
	else if (!OidIsValid(funcOid) || !HeapTupleIsValid(proctup))
	{
		ereport(ERROR, (errmsg("cannot find function with oid: %d", funcOid)));
	}

	/*
	 * If the function exists we want to use format_procedure_qualified to
	 * serialize its canonical arguments
	 */
	char *functionSignature = format_procedure_qualified(funcOid);
	char *functionOwner = GetUserNameFromId(procOwner, false);

	appendStringInfo(alterCommand, "ALTER ROUTINE %s OWNER TO %s;",
					 functionSignature,
					 quote_identifier(functionOwner));

	return alterCommand->data;
}


/*
 * GetAggregateDDLCommand returns a string for creating an aggregate.
 * A second parameter useCreateOrReplace signals whether to
 * to create a plain CREATE AGGREGATE or not.
 */
static char *
GetAggregateDDLCommand(const RegProcedure funcOid, bool useCreateOrReplace)
{
	StringInfoData buf = { 0 };
	int i = 0;
	Oid *argtypes = NULL;
	char **argnames = NULL;
	char *argmodes = NULL;
	int insertorderbyat = -1;
	int argsprinted = 0;

	HeapTuple proctup = SearchSysCache1(PROCOID, funcOid);
	if (!HeapTupleIsValid(proctup))
	{
		elog(ERROR, "cache lookup failed for %d", funcOid);
	}

	Form_pg_proc proc = (Form_pg_proc) GETSTRUCT(proctup);

	Assert(proc->prokind == PROKIND_AGGREGATE);

	initStringInfo(&buf);

	const char *name = NameStr(proc->proname);
	const char *nsp = get_namespace_name(proc->pronamespace);

	if (useCreateOrReplace)
	{
		appendStringInfo(&buf, "CREATE OR REPLACE AGGREGATE %s(",
						 quote_qualified_identifier(nsp, name));
	}
	else
	{
		appendStringInfo(&buf, "CREATE AGGREGATE %s(",
						 quote_qualified_identifier(nsp, name));
	}

	/* Parameters, borrows heavily from print_function_arguments in postgres */
	int numargs = get_func_arg_info(proctup, &argtypes, &argnames, &argmodes);

	HeapTuple aggtup = SearchSysCache1(AGGFNOID, funcOid);
	if (!HeapTupleIsValid(aggtup))
	{
		elog(ERROR, "cache lookup failed for %d", funcOid);
	}
	Form_pg_aggregate agg = (Form_pg_aggregate) GETSTRUCT(aggtup);

	if (AGGKIND_IS_ORDERED_SET(agg->aggkind))
	{
		insertorderbyat = agg->aggnumdirectargs;
	}

	/*
	 * For zero-argument aggregate, write * in place of the list of arguments
	 */
	if (numargs == 0)
	{
		appendStringInfo(&buf, "*");
	}

	for (i = 0; i < numargs; i++)
	{
		Oid argtype = argtypes[i];
		char *argname = argnames ? argnames[i] : NULL;
		char argmode = argmodes ? argmodes[i] : PROARGMODE_IN;
		const char *modename;

		switch (argmode)
		{
			case PROARGMODE_IN:
			{
				modename = "";
				break;
			}

			case PROARGMODE_VARIADIC:
			{
				modename = "VARIADIC ";
				break;
			}

			default:
			{
				elog(ERROR, "unexpected parameter mode '%c'", argmode);
				modename = NULL;
				break;
			}
		}

		if (argsprinted == insertorderbyat)
		{
			appendStringInfoString(&buf, " ORDER BY ");
		}
		else if (argsprinted)
		{
			appendStringInfoString(&buf, ", ");
		}

		appendStringInfoString(&buf, modename);

		if (argname && argname[0])
		{
			appendStringInfo(&buf, "%s ", quote_identifier(argname));
		}

		appendStringInfoString(&buf, format_type_be_qualified(argtype));

		argsprinted++;

		/* nasty hack: print the last arg twice for variadic ordered-set agg */
		if (argsprinted == insertorderbyat && i == numargs - 1)
		{
			i--;
		}
	}

	appendStringInfo(&buf, ") (STYPE = %s,SFUNC = %s",
					 format_type_be_qualified(agg->aggtranstype),
					 quote_qualified_func_name(agg->aggtransfn));

	if (agg->aggtransspace != 0)
	{
		appendStringInfo(&buf, ", SSPACE = %d", agg->aggtransspace);
	}

	if (agg->aggfinalfn != InvalidOid)
	{
		const char *finalmodifystring = NULL;
		switch (agg->aggfinalmodify)
		{
			case AGGMODIFY_READ_ONLY:
			{
				finalmodifystring = "READ_ONLY";
				break;
			}

			case AGGMODIFY_SHAREABLE:
			{
				finalmodifystring = "SHAREABLE";
				break;
			}

			case AGGMODIFY_READ_WRITE:
			{
				finalmodifystring = "READ_WRITE";
				break;
			}
		}

		appendStringInfo(&buf, ", FINALFUNC = %s",
						 quote_qualified_func_name(agg->aggfinalfn));

		if (finalmodifystring != NULL)
		{
			appendStringInfo(&buf, ", FINALFUNC_MODIFY = %s", finalmodifystring);
		}

		if (agg->aggfinalextra)
		{
			appendStringInfoString(&buf, ", FINALFUNC_EXTRA");
		}
	}

	if (agg->aggmtransspace != 0)
	{
		appendStringInfo(&buf, ", MSSPACE = %d", agg->aggmtransspace);
	}

	if (agg->aggmfinalfn)
	{
		const char *mfinalmodifystring = NULL;
		switch (agg->aggfinalmodify)
		{
			case AGGMODIFY_READ_ONLY:
			{
				mfinalmodifystring = "READ_ONLY";
				break;
			}

			case AGGMODIFY_SHAREABLE:
			{
				mfinalmodifystring = "SHAREABLE";
				break;
			}

			case AGGMODIFY_READ_WRITE:
			{
				mfinalmodifystring = "READ_WRITE";
				break;
			}
		}

		appendStringInfo(&buf, ", MFINALFUNC = %s",
						 quote_qualified_func_name(agg->aggmfinalfn));

		if (mfinalmodifystring != NULL)
		{
			appendStringInfo(&buf, ", MFINALFUNC_MODIFY = %s", mfinalmodifystring);
		}

		if (agg->aggmfinalextra)
		{
			appendStringInfoString(&buf, ", MFINALFUNC_EXTRA");
		}
	}

	if (agg->aggmtransfn)
	{
		appendStringInfo(&buf, ", MSFUNC = %s",
						 quote_qualified_func_name(agg->aggmtransfn));

		if (agg->aggmtranstype)
		{
			appendStringInfo(&buf, ", MSTYPE = %s",
							 format_type_be_qualified(agg->aggmtranstype));
		}
	}

	if (agg->aggtransspace != 0)
	{
		appendStringInfo(&buf, ", SSPACE = %d", agg->aggtransspace);
	}

	if (agg->aggminvtransfn)
	{
		appendStringInfo(&buf, ", MINVFUNC = %s",
						 quote_qualified_func_name(agg->aggminvtransfn));
	}

	if (agg->aggcombinefn)
	{
		appendStringInfo(&buf, ", COMBINEFUNC = %s",
						 quote_qualified_func_name(agg->aggcombinefn));
	}

	if (agg->aggserialfn)
	{
		appendStringInfo(&buf, ", SERIALFUNC = %s",
						 quote_qualified_func_name(agg->aggserialfn));
	}

	if (agg->aggdeserialfn)
	{
		appendStringInfo(&buf, ", DESERIALFUNC = %s",
						 quote_qualified_func_name(agg->aggdeserialfn));
	}

	if (agg->aggsortop != InvalidOid)
	{
		appendStringInfo(&buf, ", SORTOP = %s",
						 generate_operator_name(agg->aggsortop, argtypes[0],
												argtypes[0]));
	}

	{
		const char *parallelstring = NULL;
		switch (proc->proparallel)
		{
			case PROPARALLEL_SAFE:
			{
				parallelstring = "SAFE";
				break;
			}

			case PROPARALLEL_RESTRICTED:
			{
				parallelstring = "RESTRICTED";
				break;
			}

			case PROPARALLEL_UNSAFE:
			{
				break;
			}

			default:
			{
				elog(WARNING, "Unknown parallel option, ignoring: %c", proc->proparallel);
				break;
			}
		}

		if (parallelstring != NULL)
		{
			appendStringInfo(&buf, ", PARALLEL = %s", parallelstring);
		}
	}

	{
		bool isNull = false;
		Datum textInitVal = SysCacheGetAttr(AGGFNOID, aggtup,
											Anum_pg_aggregate_agginitval,
											&isNull);
		if (!isNull)
		{
			char *strInitVal = TextDatumGetCString(textInitVal);
			char *strInitValQuoted = quote_literal_cstr(strInitVal);

			appendStringInfo(&buf, ", INITCOND = %s", strInitValQuoted);

			pfree(strInitValQuoted);
			pfree(strInitVal);
		}
	}

	{
		bool isNull = false;
		Datum textInitVal = SysCacheGetAttr(AGGFNOID, aggtup,
											Anum_pg_aggregate_aggminitval,
											&isNull);
		if (!isNull)
		{
			char *strInitVal = TextDatumGetCString(textInitVal);
			char *strInitValQuoted = quote_literal_cstr(strInitVal);

			appendStringInfo(&buf, ", MINITCOND = %s", strInitValQuoted);

			pfree(strInitValQuoted);
			pfree(strInitVal);
		}
	}

	if (agg->aggkind == AGGKIND_HYPOTHETICAL)
	{
		appendStringInfoString(&buf, ", HYPOTHETICAL");
	}

	appendStringInfoChar(&buf, ')');

	ReleaseSysCache(aggtup);
	ReleaseSysCache(proctup);

	return buf.data;
}


/*
 * ShouldPropagateCreateFunction tests if we need to propagate a CREATE FUNCTION
 * statement.
 */
static bool
ShouldPropagateCreateFunction(CreateFunctionStmt *stmt)
{
	if (!ShouldPropagate())
	{
		return false;
	}

	if (!ShouldPropagateCreateInCoordinatedTransction())
	{
		return false;
	}

	return true;
}


/*
 * ShouldPropagateAlterFunction returns, based on the address of a function, if alter
 * statements targeting the function should be propagated.
 */
static bool
ShouldPropagateAlterFunction(const ObjectAddress *address)
{
	if (creating_extension)
	{
		/*
		 * extensions should be created separately on the workers, functions cascading
		 * from an extension should therefore not be propagated.
		 */
		return false;
	}

	if (!EnableMetadataSync)
	{
		/*
		 * we are configured to disable object propagation, should not propagate anything
		 */
		return false;
	}

	if (!IsAnyObjectDistributed(list_make1((ObjectAddress *) address)))
	{
		/* do not propagate alter function for non-distributed functions */
		return false;
	}

	return true;
}


/*
 * PreprocessCreateFunctionStmt is called during the planning phase for CREATE [OR REPLACE]
 * FUNCTION before it is created on the local node internally.
 *
 * Since we use pg_get_functiondef to get the ddl command we actually do not do any
 * planning here, instead we defer the plan creation to the postprocessing step.
 *
 * Instead we do our basic housekeeping where we make sure we are on the coordinator and
 * can propagate the function in sequential mode.
 */
List *
PreprocessCreateFunctionStmt(Node *node, const char *queryString,
							 ProcessUtilityContext processUtilityContext)
{
	CreateFunctionStmt *stmt = castNode(CreateFunctionStmt, node);

	if (!ShouldPropagateCreateFunction(stmt))
	{
		return NIL;
	}

	EnsureCoordinator();

	EnsureSequentialMode(OBJECT_FUNCTION);

	/*
	 * ddl jobs will be generated during the postprocessing phase as we need the function to
	 * be updated in the catalog to get its sql representation
	 */
	return NIL;
}


/*
 * PostprocessCreateFunctionStmt actually creates the plan we need to execute for function
 * propagation. This is the downside of using pg_get_functiondef to get the sql statement.
 *
 * If function depends on any non-distributed relation (except sequence and composite type),
 * Citus can not distribute it. In order to not to prevent users from creating local
 * functions on the coordinator WARNING message will be sent to the customer about the case
 * instead of erroring out.
 *
 * Besides creating the plan we also make sure all (new) dependencies of the function are
 * created on all nodes.
 */
List *
PostprocessCreateFunctionStmt(Node *node, const char *queryString)
{
	CreateFunctionStmt *stmt = castNode(CreateFunctionStmt, node);

	if (!ShouldPropagateCreateFunction(stmt))
	{
		return NIL;
	}

	List *functionAddresses = GetObjectAddressListFromParseTree((Node *) stmt, false,
																true);

	/*  the code-path only supports a single object */
	Assert(list_length(functionAddresses) == 1);

	if (IsAnyObjectAddressOwnedByExtension(functionAddresses, NULL))
	{
		return NIL;
	}

	/* If the function has any unsupported dependency, create it locally */
	DeferredErrorMessage *errMsg = DeferErrorIfAnyObjectHasUnsupportedDependency(
		functionAddresses);

	if (errMsg != NULL)
	{
		if (EnableUnsupportedFeatureMessages)
		{
			RaiseDeferredError(errMsg, WARNING);
		}

		return NIL;
	}

	EnsureAllObjectDependenciesExistOnAllNodes(functionAddresses);

	/* We have already asserted that we have exactly 1 address in the addresses. */
	ObjectAddress *functionAddress = linitial(functionAddresses);

	List *commands = list_make1(DISABLE_DDL_PROPAGATION);
	commands = list_concat(commands, CreateFunctionDDLCommandsIdempotent(
							   functionAddress));
	commands = list_concat(commands, list_make1(ENABLE_DDL_PROPAGATION));

	return NodeDDLTaskList(NON_COORDINATOR_NODES, commands);
}


/*
 * CreateFunctionStmtObjectAddress returns the ObjectAddress for the subject of the
 * CREATE [OR REPLACE] FUNCTION statement. If missing_ok is false it will error with the
 * normal postgres error for unfound functions.
 */
List *
CreateFunctionStmtObjectAddress(Node *node, bool missing_ok, bool isPostprocess)
{
	CreateFunctionStmt *stmt = castNode(CreateFunctionStmt, node);
	ObjectType objectType = OBJECT_FUNCTION;

	if (stmt->is_procedure)
	{
		objectType = OBJECT_PROCEDURE;
	}

	ObjectWithArgs *objectWithArgs = makeNode(ObjectWithArgs);
	objectWithArgs->objname = stmt->funcname;

	FunctionParameter *funcParam = NULL;
	foreach_declared_ptr(funcParam, stmt->parameters)
	{
		if (ShouldAddFunctionSignature(funcParam->mode))
		{
			objectWithArgs->objargs = lappend(objectWithArgs->objargs,
											  funcParam->argType);
		}
	}

	int OldClientMinMessage = client_min_messages;

	/* suppress NOTICE if running under pg vanilla tests */
	SetLocalClientMinMessagesIfRunningPGTests(WARNING);

	List *funcAddresses = FunctionToObjectAddress(objectType, objectWithArgs, missing_ok);

	/* set it back */
	SetLocalClientMinMessagesIfRunningPGTests(OldClientMinMessage);

	return funcAddresses;
}


/*
 * DefineAggregateStmtObjectAddress finds the ObjectAddress for the composite type described
 * by the DefineStmtObjectAddress. If missing_ok is false this function throws an error if the
 * aggregate does not exist.
 *
 * objectId in the address can be invalid if missing_ok was set to true.
 */
List *
DefineAggregateStmtObjectAddress(Node *node, bool missing_ok, bool isPostprocess)
{
	DefineStmt *stmt = castNode(DefineStmt, node);

	Assert(stmt->kind == OBJECT_AGGREGATE);

	ObjectWithArgs *objectWithArgs = makeNode(ObjectWithArgs);
	objectWithArgs->objname = stmt->defnames;

	if (stmt->args != NIL)
	{
		FunctionParameter *funcParam = NULL;
		foreach_declared_ptr(funcParam, linitial(stmt->args))
		{
			objectWithArgs->objargs = lappend(objectWithArgs->objargs,
											  funcParam->argType);
		}
	}
	else
	{
		DefElem *defItem = NULL;
		foreach_declared_ptr(defItem, stmt->definition)
		{
			/*
			 * If no explicit args are given, pg includes basetype in the signature.
			 * If the basetype given is a type, like int4, we should include it in the
			 * signature. In that case, defItem->arg would be a TypeName.
			 * If the basetype given is a string, like "ANY", we shouldn't include it.
			 */
			if (strcmp(defItem->defname, "basetype") == 0 && IsA(defItem->arg, TypeName))
			{
				objectWithArgs->objargs = lappend(objectWithArgs->objargs,
												  defItem->arg);
			}
		}
	}

	return FunctionToObjectAddress(OBJECT_AGGREGATE, objectWithArgs, missing_ok);
}


/*
 * PreprocessAlterFunctionStmt is invoked for alter function statements with actions. Here we
 * plan the jobs to be executed on the workers for functions that have been distributed in
 * the cluster.
 */
List *
PreprocessAlterFunctionStmt(Node *node, const char *queryString,
							ProcessUtilityContext processUtilityContext)
{
	AlterFunctionStmt *stmt = castNode(AlterFunctionStmt, node);
	AssertObjectTypeIsFunctional(stmt->objtype);

	List *addresses = GetObjectAddressListFromParseTree((Node *) stmt, false, false);

	/*  the code-path only supports a single object */
	Assert(list_length(addresses) == 1);

	/* We have already asserted that we have exactly 1 address in the addresses. */
	ObjectAddress *address = linitial(addresses);

	if (!ShouldPropagateAlterFunction(address))
	{
		return NIL;
	}

	EnsureCoordinator();
	ErrorIfUnsupportedAlterFunctionStmt(stmt);
	EnsureSequentialMode(OBJECT_FUNCTION);
	QualifyTreeNode((Node *) stmt);
	const char *sql = DeparseTreeNode((Node *) stmt);

	List *commands = list_make3(DISABLE_DDL_PROPAGATION,
								(void *) sql,
								ENABLE_DDL_PROPAGATION);

	return NodeDDLTaskList(NON_COORDINATOR_NODES, commands);
}


/*
 * PreprocessAlterFunctionDependsStmt is called during the planning phase of an
 * ALTER FUNCION ... DEPENDS ON EXTENSION ... statement. Since functions depending on
 * extensions are assumed to be Owned by an extension we assume the extension to keep the
 * function in sync.
 *
 * If we would allow users to create a dependency between a distributed function and an
 * extension our pruning logic for which objects to distribute as dependencies of other
 * objects will change significantly which could cause issues adding new workers. Hence we
 * don't allow this dependency to be created.
 */
List *
PreprocessAlterFunctionDependsStmt(Node *node, const char *queryString,
								   ProcessUtilityContext processUtilityContext)
{
	AlterObjectDependsStmt *stmt = castNode(AlterObjectDependsStmt, node);
	AssertObjectTypeIsFunctional(stmt->objectType);

	if (creating_extension)
	{
		/*
		 * extensions should be created separately on the workers, types cascading from an
		 * extension should therefore not be propagated here.
		 */
		return NIL;
	}

	if (!EnableMetadataSync)
	{
		/*
		 * we are configured to disable object propagation, should not propagate anything
		 */
		return NIL;
	}

	List *addresses = GetObjectAddressListFromParseTree((Node *) stmt, true, false);

	/*  the code-path only supports a single object */
	Assert(list_length(addresses) == 1);

	if (!IsAnyObjectDistributed(addresses))
	{
		return NIL;
	}

	/* We have already asserted that we have exactly 1 address in the addresses. */
	ObjectAddress *address = linitial(addresses);

	/*
	 * Distributed objects should not start depending on an extension, this will break
	 * the dependency resolving mechanism we use to replicate distributed objects to new
	 * workers
	 */
	const char *functionName =
		getObjectIdentity(address, /* missingOk: */ false);
	ereport(ERROR, (errmsg("distrtibuted functions are not allowed to depend on an "
						   "extension"),
					errdetail("Function \"%s\" is already distributed. Functions from "
							  "extensions are expected to be created on the workers by "
							  "the extension they depend on.", functionName)));
}


/*
 * AlterFunctionDependsStmtObjectAddress resolves the ObjectAddress of the function that
 * is the subject of an ALTER FUNCTION ... DEPENS ON EXTENSION ... statement. If
 * missing_ok is set to false the lookup will raise an error.
 */
List *
AlterFunctionDependsStmtObjectAddress(Node *node, bool missing_ok, bool isPostprocess)
{
	AlterObjectDependsStmt *stmt = castNode(AlterObjectDependsStmt, node);
	AssertObjectTypeIsFunctional(stmt->objectType);

	return FunctionToObjectAddress(stmt->objectType,
								   castNode(ObjectWithArgs, stmt->object), missing_ok);
}


/*
 * AlterFunctionStmtObjectAddress returns the ObjectAddress of the subject in the
 * AlterFunctionStmt. If missing_ok is set to false an error will be raised if postgres
 * was unable to find the function/procedure that was the target of the statement.
 */
List *
AlterFunctionStmtObjectAddress(Node *node, bool missing_ok, bool isPostprocess)
{
	AlterFunctionStmt *stmt = castNode(AlterFunctionStmt, node);
	return FunctionToObjectAddress(stmt->objtype, stmt->func, missing_ok);
}


/*
 * RenameFunctionStmtObjectAddress returns the ObjectAddress of the function that is the
 * subject of the RenameStmt. Errors if missing_ok is false.
 */
List *
RenameFunctionStmtObjectAddress(Node *node, bool missing_ok, bool isPostprocess)
{
	RenameStmt *stmt = castNode(RenameStmt, node);
	return FunctionToObjectAddress(stmt->renameType,
								   castNode(ObjectWithArgs, stmt->object), missing_ok);
}


/*
 * AlterFunctionOwnerObjectAddress returns the ObjectAddress of the function that is the
 * subject of the AlterOwnerStmt. Errors if missing_ok is false.
 */
List *
AlterFunctionOwnerObjectAddress(Node *node, bool missing_ok, bool isPostprocess)
{
	AlterOwnerStmt *stmt = castNode(AlterOwnerStmt, node);
	return FunctionToObjectAddress(stmt->objectType,
								   castNode(ObjectWithArgs, stmt->object), missing_ok);
}


/*
 * AlterFunctionSchemaStmtObjectAddress returns the ObjectAddress of the function that is
 * the subject of the AlterObjectSchemaStmt. Errors if missing_ok is false.
 *
 * This could be called both before or after it has been applied locally. It will look in
 * the old schema first, if the function cannot be found in that schema it will look in
 * the new schema. Errors if missing_ok is false and the type cannot be found in either of
 * the schemas.
 */
List *
AlterFunctionSchemaStmtObjectAddress(Node *node, bool missing_ok, bool isPostprocess)
{
	AlterObjectSchemaStmt *stmt = castNode(AlterObjectSchemaStmt, node);
	AssertObjectTypeIsFunctional(stmt->objectType);

	ObjectWithArgs *objectWithArgs = castNode(ObjectWithArgs, stmt->object);
	Oid funcOid = LookupFuncWithArgs(stmt->objectType, objectWithArgs, true);
	List *names = objectWithArgs->objname;

	if (funcOid == InvalidOid)
	{
		/*
		 * couldn't find the function, might have already been moved to the new schema, we
		 * construct a new objname that uses the new schema to search in.
		 */

		/* the name of the function is the last in the list of names */
		String *funcNameStr = lfirst(list_tail(names));
		List *newNames = list_make2(makeString(stmt->newschema), funcNameStr);

		/*
		 * we don't error here either, as the error would be not a good user facing
		 * error if the type didn't exist in the first place.
		 */
		objectWithArgs->objname = newNames;
		funcOid = LookupFuncWithArgs(stmt->objectType, objectWithArgs, true);
		objectWithArgs->objname = names; /* restore the original names */

		/*
		 * if the function is still invalid we couldn't find the function, cause postgres
		 * to error by preforming a lookup once more. Since we know the
		 */
		if (!missing_ok && funcOid == InvalidOid)
		{
			/*
			 * this will most probably throw an error, unless for some reason the function
			 * has just been created (if possible at all). For safety we assign the
			 * funcOid.
			 */
			funcOid = LookupFuncWithArgs(stmt->objectType, objectWithArgs,
										 missing_ok);
		}
	}

	ObjectAddress *address = palloc0(sizeof(ObjectAddress));
	ObjectAddressSet(*address, ProcedureRelationId, funcOid);

	return list_make1(address);
}


/*
 * GenerateBackupNameForProcCollision generates a new proc name for an existing proc. The
 * name is generated in such a way that the new name doesn't overlap with an existing proc
 * by adding a suffix with incrementing number after the new name.
 */
char *
GenerateBackupNameForProcCollision(const ObjectAddress *address)
{
	char *newName = palloc0(NAMEDATALEN);
	char suffix[NAMEDATALEN] = { 0 };
	int count = 0;
	String *namespace = makeString(get_namespace_name(get_func_namespace(
														  address->objectId)));
	char *baseName = get_func_name(address->objectId);
	int baseLength = strlen(baseName);
	Oid *argtypes = NULL;
	char **argnames = NULL;
	char *argmodes = NULL;
	HeapTuple proctup = SearchSysCache1(PROCOID, address->objectId);

	if (!HeapTupleIsValid(proctup))
	{
		elog(ERROR, "citus cache lookup failed.");
	}

	int numargs = get_func_arg_info(proctup, &argtypes, &argnames, &argmodes);
	ReleaseSysCache(proctup);

	while (true)
	{
		int suffixLength = SafeSnprintf(suffix, NAMEDATALEN - 1, "(citus_backup_%d)",
										count);

		/* trim the base name at the end to leave space for the suffix and trailing \0 */
		baseLength = Min(baseLength, NAMEDATALEN - suffixLength - 1);

		/* clear newName before copying the potentially trimmed baseName and suffix */
		memset(newName, 0, NAMEDATALEN);
		strncpy_s(newName, NAMEDATALEN, baseName, baseLength);
		strncpy_s(newName + baseLength, NAMEDATALEN - baseLength, suffix,
				  suffixLength);

		List *newProcName = list_make2(namespace, makeString(newName));

		/* don't need to rename if the input arguments don't match */
		FuncCandidateList clist = FuncnameGetCandidates(newProcName, numargs, NIL,
														false, false, false, true);
		for (; clist; clist = clist->next)
		{
			if (memcmp(clist->args, argtypes, sizeof(Oid) * numargs) == 0)
			{
				break;
			}
		}

		if (!clist)
		{
			return newName;
		}

		count++;
	}
}


/*
 * ObjectWithArgsFromOid returns the corresponding ObjectWithArgs node for a given pg_proc oid
 */
ObjectWithArgs *
ObjectWithArgsFromOid(Oid funcOid)
{
	ObjectWithArgs *objectWithArgs = makeNode(ObjectWithArgs);
	List *objargs = NIL;
	Oid *argTypes = NULL;
	char **argNames = NULL;
	char *argModes = NULL;
	HeapTuple proctup = SearchSysCache1(PROCOID, funcOid);

	if (!HeapTupleIsValid(proctup))
	{
		elog(ERROR, "citus cache lookup failed.");
	}

	int numargs = get_func_arg_info(proctup, &argTypes, &argNames, &argModes);

	objectWithArgs->objname = list_make2(
		makeString(get_namespace_name(get_func_namespace(funcOid))),
		makeString(get_func_name(funcOid))
		);

	for (int i = 0; i < numargs; i++)
	{
		if (argModes == NULL || ShouldAddFunctionSignature(argModes[i]))
		{
			objargs = lappend(objargs, makeTypeNameFromOid(argTypes[i], -1));
		}
	}
	objectWithArgs->objargs = objargs;

	ReleaseSysCache(proctup);

	return objectWithArgs;
}


/*
 * ShouldAddFunctionSignature takes a FunctionParameterMode and returns true if it should
 * be included in the function signature. Returns false otherwise.
 */
static bool
ShouldAddFunctionSignature(FunctionParameterMode mode)
{
	/* only input parameters should be added to the generated signature */
	switch (mode)
	{
		case FUNC_PARAM_IN:
		case FUNC_PARAM_INOUT:
		case FUNC_PARAM_VARIADIC:
		{
			return true;
		}

		case FUNC_PARAM_OUT:
		case FUNC_PARAM_TABLE:
		{
			return false;
		}

		default:
			return true;
	}
}


/*
 * FunctionToObjectAddress returns the ObjectAddress of a Function, Procedure or
 * Aggregate based on its type and ObjectWithArgs describing the
 * Function/Procedure/Aggregate. If missing_ok is set to false an error will be
 * raised by postgres explaining the Function/Procedure could not be found.
 */
static List *
FunctionToObjectAddress(ObjectType objectType, ObjectWithArgs *objectWithArgs,
						bool missing_ok)
{
	AssertObjectTypeIsFunctional(objectType);

	Oid funcOid = LookupFuncWithArgs(objectType, objectWithArgs, missing_ok);
	ObjectAddress *address = palloc0(sizeof(ObjectAddress));
	ObjectAddressSet(*address, ProcedureRelationId, funcOid);

	return list_make1(address);
}


/*
 * ErrorIfUnsupportedAlterFunctionStmt raises an error if the AlterFunctionStmt contains a
 * construct that is not supported to be altered on a distributed function. It is assumed
 * the statement passed in is already tested to be targeting a distributed function, and
 * will only execute the checks to error on unsupported constructs.
 *
 * Unsupported Constructs:
 *  - ALTER FUNCTION ... SET ... FROM CURRENT
 */
static void
ErrorIfUnsupportedAlterFunctionStmt(AlterFunctionStmt *stmt)
{
	DefElem *action = NULL;
	foreach_declared_ptr(action, stmt->actions)
	{
		if (strcmp(action->defname, "set") == 0)
		{
			VariableSetStmt *setStmt = castNode(VariableSetStmt, action->arg);
			if (setStmt->kind == VAR_SET_CURRENT)
			{
				/* check if the set action is a SET ... FROM CURRENT */
				ereport(ERROR, (errmsg("unsupported ALTER FUNCTION ... SET ... FROM "
									   "CURRENT for a distributed function"),
								errhint("SET FROM CURRENT is not supported for "
										"distributed functions, instead use the SET ... "
										"TO ... syntax with a constant value.")));
			}
		}
	}
}


/* returns the quoted qualified name of a given function oid */
static char *
quote_qualified_func_name(Oid funcOid)
{
	return quote_qualified_identifier(
		get_namespace_name(get_func_namespace(funcOid)),
		get_func_name(funcOid));
}


/*
 * EnsureExtensionFuncionCanBeCreated checks if the dependent objects
 * (including extension) exists on all nodes, if not, creates them. In
 * addition, it also checks if distribution argument is passed.
 */
static void
EnsureExtensionFunctionCanBeDistributed(const ObjectAddress functionAddress,
										const ObjectAddress extensionAddress,
										char *distributionArgumentName)
{
	if (CitusExtensionObject(&extensionAddress))
	{
		/*
		 * Citus extension is a special case. It's the extension that
		 * provides the 'distributed capabilities' in the first place.
		 * Trying to distribute its own function(s) doesn't make sense.
		 */
		ereport(ERROR, (errmsg("Citus extension functions(%s) "
							   "cannot be distributed.",
							   get_func_name(functionAddress.objectId))));
	}

	/*
	 * Distributing functions from extensions has the most benefit when
	 * distribution argument is specified.
	 */
	if (distributionArgumentName == NULL)
	{
		ereport(ERROR, (errmsg("Extension functions(%s) "
							   "without distribution argument "
							   "are not supported.",
							   get_func_name(functionAddress.objectId))));
	}

	/*
	 * Ensure corresponding extension is in pg_dist_object.
	 * Functions owned by an extension are depending internally on that extension,
	 * hence EnsureAllObjectDependenciesExistOnAllNodes() creates the extension, which in
	 * turn creates the function, and thus we don't have to create it ourself like
	 * we do for non-extension functions.
	 */
	ereport(DEBUG1, (errmsg("Extension(%s) owning the "
							"function(%s) is not distributed, "
							"attempting to propogate the extension",
							get_extension_name(extensionAddress.objectId),
							get_func_name(functionAddress.objectId))));

	ObjectAddress *copyFunctionAddress = palloc0(sizeof(ObjectAddress));
	*copyFunctionAddress = functionAddress;
	EnsureAllObjectDependenciesExistOnAllNodes(list_make1(copyFunctionAddress));
}


/*
 * PreprocessGrantOnFunctionStmt is executed before the statement is applied to the local
 * postgres instance.
 *
 * In this stage we can prepare the commands that need to be run on all workers to grant
 * on distributed functions, procedures, routines.
 */
List *
PreprocessGrantOnFunctionStmt(Node *node, const char *queryString,
							  ProcessUtilityContext processUtilityContext)
{
	GrantStmt *stmt = castNode(GrantStmt, node);
	Assert(isFunction(stmt->objtype));

	List *distributedFunctions = FilterDistributedFunctions(stmt);

	if (list_length(distributedFunctions) == 0 || !ShouldPropagate())
	{
		return NIL;
	}

	EnsureCoordinator();

	List *grantFunctionList = NIL;
	ObjectAddress *functionAddress = NULL;
	foreach_declared_ptr(functionAddress, distributedFunctions)
	{
		ObjectWithArgs *distFunction = ObjectWithArgsFromOid(
			functionAddress->objectId);
		grantFunctionList = lappend(grantFunctionList, distFunction);
	}

	List *originalObjects = stmt->objects;
	GrantTargetType originalTargtype = stmt->targtype;

	stmt->objects = grantFunctionList;
	stmt->targtype = ACL_TARGET_OBJECT;

	char *sql = DeparseTreeNode((Node *) stmt);

	stmt->objects = originalObjects;
	stmt->targtype = originalTargtype;

	List *commandList = list_make3(DISABLE_DDL_PROPAGATION,
								   (void *) sql,
								   ENABLE_DDL_PROPAGATION);

	return NodeDDLTaskList(NON_COORDINATOR_NODES, commandList);
}


/*
 * PostprocessGrantOnFunctionStmt makes sure dependencies of each
 * distributed function in the statement exist on all nodes
 */
List *
PostprocessGrantOnFunctionStmt(Node *node, const char *queryString)
{
	GrantStmt *stmt = castNode(GrantStmt, node);

	List *distributedFunctions = FilterDistributedFunctions(stmt);

	if (list_length(distributedFunctions) == 0)
	{
		return NIL;
	}

	ObjectAddress *functionAddress = NULL;
	foreach_declared_ptr(functionAddress, distributedFunctions)
	{
		EnsureAllObjectDependenciesExistOnAllNodes(list_make1(functionAddress));
	}
	return NIL;
}


/*
 * FilterDistributedFunctions determines and returns a list of distributed functions
 * ObjectAddress-es from given grant statement.
 */
static List *
FilterDistributedFunctions(GrantStmt *grantStmt)
{
	List *grantFunctionList = NIL;

	bool grantOnFunctionCommand = (grantStmt->targtype == ACL_TARGET_OBJECT &&
								   isFunction(grantStmt->objtype));
	bool grantAllFunctionsOnSchemaCommand = (grantStmt->targtype ==
											 ACL_TARGET_ALL_IN_SCHEMA &&
											 isFunction(grantStmt->objtype));

	/* we are only interested in function/procedure/routine level grants */
	if (!grantOnFunctionCommand && !grantAllFunctionsOnSchemaCommand)
	{
		return NIL;
	}

	if (grantAllFunctionsOnSchemaCommand)
	{
		List *distributedFunctionList = DistributedFunctionList();
		ObjectAddress *distributedFunction = NULL;
		List *namespaceOidList = NIL;

		/* iterate over all namespace names provided to get their oid's */
		String *namespaceValue = NULL;
		foreach_declared_ptr(namespaceValue, grantStmt->objects)
		{
			char *nspname = strVal(namespaceValue);
			bool missing_ok = false;
			Oid namespaceOid = get_namespace_oid(nspname, missing_ok);
			namespaceOidList = list_append_unique_oid(namespaceOidList, namespaceOid);
		}

		/*
		 * iterate over all distributed functions to filter the ones
		 * that belong to one of the namespaces from above
		 */
		foreach_declared_ptr(distributedFunction, distributedFunctionList)
		{
			Oid namespaceOid = get_func_namespace(distributedFunction->objectId);

			/*
			 * if this distributed function's schema is one of the schemas
			 * specified in the GRANT .. ALL FUNCTIONS IN SCHEMA ..
			 * add it to the list
			 */
			if (list_member_oid(namespaceOidList, namespaceOid))
			{
				grantFunctionList = lappend(grantFunctionList, distributedFunction);
			}
		}
	}
	else
	{
		bool missingOk = false;
		ObjectWithArgs *objectWithArgs = NULL;
		foreach_declared_ptr(objectWithArgs, grantStmt->objects)
		{
			ObjectAddress *functionAddress = palloc0(sizeof(ObjectAddress));
			functionAddress->classId = ProcedureRelationId;
			functionAddress->objectId = LookupFuncWithArgs(grantStmt->objtype,
														   objectWithArgs,
														   missingOk);
			functionAddress->objectSubId = 0;

			/*
			 * if this function from GRANT .. ON FUNCTION .. is a distributed
			 * function, add it to the list
			 */
			if (IsAnyObjectDistributed(list_make1(functionAddress)))
			{
				grantFunctionList = lappend(grantFunctionList, functionAddress);
			}
		}
	}
	return grantFunctionList;
}
