mirror of https://github.com/citusdata/citus.git
2175 lines
63 KiB
C
2175 lines
63 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* 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;
|
|
}
|