diff --git a/Makefile b/Makefile index 64af08caf8b..b3bcb59f999 100644 --- a/Makefile +++ b/Makefile @@ -5,16 +5,28 @@ EXTENSION = citus MODULE_big = citus OBJS = src/backend/distributed/shared_library_init.o \ + src/backend/distributed/commands/cluster.o \ src/backend/distributed/commands/create_distributed_table.o \ src/backend/distributed/commands/drop_distributed_table.o \ + src/backend/distributed/commands/extension.o \ + src/backend/distributed/commands/foreign_constraint.o \ + src/backend/distributed/commands/grant.o \ + src/backend/distributed/commands/index.o \ src/backend/distributed/commands/multi_copy.o \ + src/backend/distributed/commands/policy.o \ + src/backend/distributed/commands/rename.o \ + src/backend/distributed/commands/schema.o \ + src/backend/distributed/commands/sequence.o \ + src/backend/distributed/commands/subscription.o \ + src/backend/distributed/commands/table.o \ src/backend/distributed/commands/transmit.o \ + src/backend/distributed/commands/truncate.o \ + src/backend/distributed/commands/utility_hook.o \ + src/backend/distributed/commands/vacuum.o \ src/backend/distributed/connection/connection_configuration.o \ src/backend/distributed/connection/connection_management.o \ src/backend/distributed/connection/placement_connection.o \ src/backend/distributed/connection/remote_commands.o \ - src/backend/distributed/ddl/foreign_constraint.o \ - src/backend/distributed/ddl/policy.o \ src/backend/distributed/executor/citus_custom_scan.o \ src/backend/distributed/executor/insert_select_executor.o \ src/backend/distributed/executor/intermediate_results.o \ @@ -24,7 +36,6 @@ OBJS = src/backend/distributed/shared_library_init.o \ src/backend/distributed/executor/multi_router_executor.o \ src/backend/distributed/executor/multi_server_executor.o \ src/backend/distributed/executor/multi_task_tracker_executor.o \ - src/backend/distributed/executor/multi_utility.o \ src/backend/distributed/executor/query_stats.o \ src/backend/distributed/executor/subplan_execution.o \ src/backend/distributed/master/citus_create_restore_point.o \ diff --git a/src/backend/distributed/commands/README.md b/src/backend/distributed/commands/README.md new file mode 100644 index 00000000000..bd1a2a7cb2d --- /dev/null +++ b/src/backend/distributed/commands/README.md @@ -0,0 +1,54 @@ +# Commands + +The commands module is modeled after `backend/commands` from the postgres repository and +contains the logic for Citus on how to run these commands on distributed objects. Even +though the structure of the directory has some resemblence to its postgres relative files +here are somewhat more finegrained. This is due to the nature of citus commands that are +heavily focused on distributed tables. Instead of having all commands in `tablecmds.c` +they are often moved to files that are named after the command. + +| File | Description | +|------------------------------|-------------| +| `create_distributed_table.c` | Implementation of UDF's for creating distributed tables | +| `drop_distributed_table.c` | Implementation for dropping metadata for partitions of distributed tables | +| `extension.c` | Implementation of `CREATE EXTENSION` commands for citus specific checks | +| `foreign_constraint.c` | Implementation of and helper functions for foreign key constraints | +| `grant.c` | Placeholder for code granting users access to relations, implemented as enterprise feature | +| `index.c` | Implementation of commands specific to indices on distributed tables | +| `multi_copy.c` | Implementation of `COPY` command. There are multiple different copy modes which are described in detail below | +| `policy.c` | Implementation of `CREATE\ALTER POLICY` commands. | +| `rename.c` | Implementation of `ALTER ... RENAME ...` commands. It implements the renaming of applicable objects, otherwise provides the user with a warning | +| `schema.c` | | +| `sequence.c` | Implementation of `CREATE/ALTER SEQUENCE` commands. Primarily checks correctness of sequence statements as they are not propagated to the worker nodes | +| `table.c` | | +| `transmit.c` | Implementation of `COPY` commands with `format transmit` set in the options. This format is used to transfer files from one node to another node | +| `truncate.c` | Implementation of `TRUNCATE` commands on distributed tables | +| `utility_hook.c` | This is the entry point from postgres into the commands module of citus. It contains the implementation that gets registered in postgres' `ProcessUtility_hook` callback to extends the functionality of the original ProcessUtility. This code is used to route the incomming commands to their respective implementation in Citus | +| `vacuum.c` | Implementation of `VACUUM` commands on distributed tables | + +# COPY + +The copy command is overloaded for a couple of use-cases specific to citus. The syntax of +the command stays the same, however the implementation might slightly differ from the +stock implementation. The overloading is mostly done via extra options that Citus uses to +indicate how to operate the copy. The options used are described below. + +## FORMAT transmit + +Implemented in `transmit.c` + +TODO: to be written by someone with enough knowledge to write how, when and why it is used. + +## FORMAT result + +Implemented in `multi_copy.c` + +TODO: to be written by someone with enough knowledge to write how, when and why it is used. + +## MASTER_HOST host + +Implemented in `multi_copy.c` + +Triggered by the `MASTER_HOST` option being set on the copy command. Also accepts `MASTER_PORT` + +TODO: to be written by someone with enough knowledge to write how, when and why it is used. diff --git a/src/backend/distributed/commands/cluster.c b/src/backend/distributed/commands/cluster.c new file mode 100644 index 00000000000..cef292fe811 --- /dev/null +++ b/src/backend/distributed/commands/cluster.c @@ -0,0 +1,49 @@ +/*------------------------------------------------------------------------- + * + * cluster.c + * Commands for CLUSTER statement + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/namespace.h" +#include "distributed/commands.h" +#include "distributed/metadata_cache.h" + + +/* placeholder for PlanClusterStmt */ +List * +PlanClusterStmt(ClusterStmt *clusterStmt, const char *clusterCommand) +{ + bool showPropagationWarning = false; + + /* CLUSTER all */ + if (clusterStmt->relation == NULL) + { + showPropagationWarning = true; + } + else + { + Oid relationId = InvalidOid; + bool missingOK = false; + + relationId = RangeVarGetRelid(clusterStmt->relation, AccessShareLock, + missingOK); + + if (OidIsValid(relationId)) + { + showPropagationWarning = IsDistributedTable(relationId); + } + } + + if (showPropagationWarning) + { + ereport(WARNING, (errmsg("not propagating CLUSTER command to worker nodes"))); + } + + return NIL; +} diff --git a/src/backend/distributed/commands/create_distributed_table.c b/src/backend/distributed/commands/create_distributed_table.c index 76ea6495887..28557cda39b 100644 --- a/src/backend/distributed/commands/create_distributed_table.c +++ b/src/backend/distributed/commands/create_distributed_table.c @@ -31,22 +31,20 @@ #include "commands/defrem.h" #include "commands/extension.h" #include "commands/trigger.h" +#include "distributed/commands/multi_copy.h" #include "distributed/citus_ruleutils.h" #include "distributed/colocation_utils.h" +#include "distributed/commands.h" #include "distributed/distribution_column.h" -#include "distributed/foreign_constraint.h" #include "distributed/master_metadata_utility.h" #include "distributed/master_protocol.h" #include "distributed/metadata_cache.h" #include "distributed/metadata_sync.h" -#include "distributed/multi_copy.h" #include "distributed/multi_executor.h" #include "distributed/multi_logical_planner.h" #include "distributed/multi_partitioning_utils.h" -#include "distributed/multi_utility.h" #include "distributed/pg_dist_colocation.h" #include "distributed/pg_dist_partition.h" -#include "distributed/policy.h" #include "distributed/reference_table_utils.h" #include "distributed/relation_access_tracking.h" #include "distributed/remote_commands.h" diff --git a/src/backend/distributed/commands/drop_distributed_table.c b/src/backend/distributed/commands/drop_distributed_table.c index acf669b9f49..045af9321a9 100644 --- a/src/backend/distributed/commands/drop_distributed_table.c +++ b/src/backend/distributed/commands/drop_distributed_table.c @@ -11,10 +11,10 @@ #include "postgres.h" #include "miscadmin.h" +#include "distributed/commands/utility_hook.h" #include "distributed/master_metadata_utility.h" #include "distributed/master_protocol.h" #include "distributed/metadata_sync.h" -#include "distributed/multi_utility.h" #include "distributed/worker_transaction.h" #include "utils/builtins.h" #include "utils/lsyscache.h" @@ -112,7 +112,7 @@ master_remove_distributed_table_metadata_from_workers(PG_FUNCTION_ARGS) /* * MasterRemoveDistributedTableMetadataFromWorkers drops the table and removes - * all the metadata beloning the distributed table in the worker nodes + * all the metadata belonging the distributed table in the worker nodes * with metadata. The function doesn't drop the tables that are * the shards on the workers. * diff --git a/src/backend/distributed/commands/extension.c b/src/backend/distributed/commands/extension.c new file mode 100644 index 00000000000..d450dd61a64 --- /dev/null +++ b/src/backend/distributed/commands/extension.c @@ -0,0 +1,117 @@ +/*------------------------------------------------------------------------- + * + * extension.c + * Commands for creating and altering extensions. + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "citus_version.h" +#include "distributed/commands.h" +#include "distributed/metadata_cache.h" +#include "nodes/parsenodes.h" + +/* Local functions forward declarations for helper functions */ +static char * ExtractNewExtensionVersion(Node *parsetree); + + +/* + * IsCitusExtensionStmt returns whether a given utility is a CREATE or ALTER + * EXTENSION statement which references the citus extension. This function + * returns false for all other inputs. + */ +bool +IsCitusExtensionStmt(Node *parsetree) +{ + char *extensionName = ""; + + if (IsA(parsetree, CreateExtensionStmt)) + { + extensionName = ((CreateExtensionStmt *) parsetree)->extname; + } + else if (IsA(parsetree, AlterExtensionStmt)) + { + extensionName = ((AlterExtensionStmt *) parsetree)->extname; + } + + return (strcmp(extensionName, "citus") == 0); +} + + +/* + * ErrorIfUnstableCreateOrAlterExtensionStmt compares CITUS_EXTENSIONVERSION + * and version given CREATE/ALTER EXTENSION statement will create/update to. If + * they are not same in major or minor version numbers, this function errors + * out. It ignores the schema version. + */ +void +ErrorIfUnstableCreateOrAlterExtensionStmt(Node *parsetree) +{ + char *newExtensionVersion = ExtractNewExtensionVersion(parsetree); + + if (newExtensionVersion != NULL) + { + /* explicit version provided in CREATE or ALTER EXTENSION UPDATE; verify */ + if (!MajorVersionsCompatible(newExtensionVersion, CITUS_EXTENSIONVERSION)) + { + ereport(ERROR, (errmsg("specified version incompatible with loaded " + "Citus library"), + errdetail("Loaded library requires %s, but %s was specified.", + CITUS_MAJORVERSION, newExtensionVersion), + errhint("If a newer library is present, restart the database " + "and try the command again."))); + } + } + else + { + /* + * No version was specified, so PostgreSQL will use the default_version + * from the citus.control file. + */ + CheckAvailableVersion(ERROR); + } +} + + +/* + * ExtractNewExtensionVersion returns the new extension version specified by + * a CREATE or ALTER EXTENSION statement. Other inputs are not permitted. This + * function returns NULL for statements with no explicit version specified. + */ +static char * +ExtractNewExtensionVersion(Node *parsetree) +{ + char *newVersion = NULL; + List *optionsList = NIL; + ListCell *optionsCell = NULL; + + if (IsA(parsetree, CreateExtensionStmt)) + { + optionsList = ((CreateExtensionStmt *) parsetree)->options; + } + else if (IsA(parsetree, AlterExtensionStmt)) + { + optionsList = ((AlterExtensionStmt *) parsetree)->options; + } + else + { + /* input must be one of the two above types */ + Assert(false); + } + + foreach(optionsCell, optionsList) + { + DefElem *defElement = (DefElem *) lfirst(optionsCell); + if (strncmp(defElement->defname, "new_version", NAMEDATALEN) == 0) + { + newVersion = strVal(defElement->arg); + break; + } + } + + return newVersion; +} diff --git a/src/backend/distributed/ddl/foreign_constraint.c b/src/backend/distributed/commands/foreign_constraint.c similarity index 99% rename from src/backend/distributed/ddl/foreign_constraint.c rename to src/backend/distributed/commands/foreign_constraint.c index 5bc77db3cbf..dc022e51a89 100644 --- a/src/backend/distributed/ddl/foreign_constraint.c +++ b/src/backend/distributed/commands/foreign_constraint.c @@ -20,7 +20,7 @@ #endif #include "catalog/pg_type.h" #include "distributed/colocation_utils.h" -#include "distributed/foreign_constraint.h" +#include "distributed/commands.h" #include "distributed/master_protocol.h" #include "distributed/multi_join_order.h" #include "distributed/version_compat.h" diff --git a/src/backend/distributed/commands/grant.c b/src/backend/distributed/commands/grant.c new file mode 100644 index 00000000000..65b9974c274 --- /dev/null +++ b/src/backend/distributed/commands/grant.c @@ -0,0 +1,19 @@ +/*------------------------------------------------------------------------- + * + * grant.c + * Commands for granting access to distributed tables. + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "distributed/commands.h" + + +/* placeholder for PlanGrantStmt */ +List * +PlanGrantStmt(GrantStmt *grantStmt) +{ + return NIL; +} diff --git a/src/backend/distributed/commands/index.c b/src/backend/distributed/commands/index.c new file mode 100644 index 00000000000..df14fa3742d --- /dev/null +++ b/src/backend/distributed/commands/index.c @@ -0,0 +1,678 @@ +/*------------------------------------------------------------------------- + * + * index.c + * Commands for creating and altering indices on distributed tables. + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/xact.h" +#include "catalog/catalog.h" +#include "catalog/index.h" +#include "catalog/namespace.h" +#include "catalog/pg_class.h" +#include "distributed/citus_ruleutils.h" +#include "distributed/commands.h" +#include "distributed/commands/utility_hook.h" +#include "distributed/distributed_planner.h" +#include "distributed/master_protocol.h" +#include "distributed/metadata_cache.h" +#include "distributed/multi_physical_planner.h" +#include "distributed/resource_lock.h" +#include "distributed/version_compat.h" +#include "lib/stringinfo.h" +#include "miscadmin.h" +#include "nodes/parsenodes.h" +#include "storage/lmgr.h" +#include "utils/builtins.h" +#include "utils/inval.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + +/* Local functions forward declarations for helper functions */ +static List * CreateIndexTaskList(Oid relationId, IndexStmt *indexStmt); +static void RangeVarCallbackForDropIndex(const RangeVar *rel, Oid relOid, Oid oldRelOid, + void *arg); +static void ErrorIfUnsupportedIndexStmt(IndexStmt *createIndexStatement); +static void ErrorIfUnsupportedDropIndexStmt(DropStmt *dropIndexStatement); +static List * DropIndexTaskList(Oid relationId, Oid indexId, DropStmt *dropStmt); + + +/* + * This struct defines the state for the callback for drop statements. + * It is copied as it is from commands/tablecmds.c in Postgres source. + */ +struct DropRelationCallbackState +{ + char relkind; + Oid heapOid; + bool concurrent; +}; + + +/* + * IsIndexRenameStmt returns whether the passed-in RenameStmt is the following + * form: + * + * - ALTER INDEX RENAME + */ +bool +IsIndexRenameStmt(RenameStmt *renameStmt) +{ + bool isIndexRenameStmt = false; + + if (renameStmt->renameType == OBJECT_INDEX) + { + isIndexRenameStmt = true; + } + + return isIndexRenameStmt; +} + + +/* + * PlanIndexStmt determines whether a given CREATE INDEX statement involves + * a distributed table. If so (and if the statement does not use unsupported + * options), it modifies the input statement to ensure proper execution against + * the master node table and creates a DDLJob to encapsulate information needed + * during the worker node portion of DDL execution before returning that DDLJob + * in a List. If no distributed table is involved, this function returns NIL. + */ +List * +PlanIndexStmt(IndexStmt *createIndexStatement, const char *createIndexCommand) +{ + List *ddlJobs = NIL; + + /* + * We first check whether a distributed relation is affected. For that, we need to + * open the relation. To prevent race conditions with later lookups, lock the table, + * and modify the rangevar to include the schema. + */ + if (createIndexStatement->relation != NULL) + { + Relation relation = NULL; + Oid relationId = InvalidOid; + bool isDistributedRelation = false; + char *namespaceName = NULL; + LOCKMODE lockmode = ShareLock; + MemoryContext relationContext = NULL; + + /* + * We don't support concurrently creating indexes for distributed + * tables, but till this point, we don't know if it is a regular or a + * distributed table. + */ + if (createIndexStatement->concurrent) + { + lockmode = ShareUpdateExclusiveLock; + } + + /* + * XXX: Consider using RangeVarGetRelidExtended with a permission + * checking callback. Right now we'll acquire the lock before having + * checked permissions, and will only fail when executing the actual + * index statements. + */ + relation = heap_openrv(createIndexStatement->relation, lockmode); + relationId = RelationGetRelid(relation); + + isDistributedRelation = IsDistributedTable(relationId); + + /* + * Before we do any further processing, fix the schema name to make sure + * that a (distributed) table with the same name does not appear on the + * search path in front of the current schema. We do this even if the + * table is not distributed, since a distributed table may appear on the + * search path by the time postgres starts processing this statement. + */ + namespaceName = get_namespace_name(RelationGetNamespace(relation)); + + /* ensure we copy string into proper context */ + relationContext = GetMemoryChunkContext(createIndexStatement->relation); + namespaceName = MemoryContextStrdup(relationContext, namespaceName); + createIndexStatement->relation->schemaname = namespaceName; + + heap_close(relation, NoLock); + + if (isDistributedRelation) + { + Oid namespaceId = InvalidOid; + Oid indexRelationId = InvalidOid; + char *indexName = createIndexStatement->idxname; + + ErrorIfUnsupportedIndexStmt(createIndexStatement); + + namespaceId = get_namespace_oid(namespaceName, false); + indexRelationId = get_relname_relid(indexName, namespaceId); + + /* if index does not exist, send the command to workers */ + if (!OidIsValid(indexRelationId)) + { + DDLJob *ddlJob = palloc0(sizeof(DDLJob)); + ddlJob->targetRelationId = relationId; + ddlJob->concurrentIndexCmd = createIndexStatement->concurrent; + ddlJob->commandString = createIndexCommand; + ddlJob->taskList = CreateIndexTaskList(relationId, createIndexStatement); + + ddlJobs = list_make1(ddlJob); + } + } + } + + return ddlJobs; +} + + +/* + * PlanDropIndexStmt determines whether a given DROP INDEX statement involves + * a distributed table. If so (and if the statement does not use unsupported + * options), it modifies the input statement to ensure proper execution against + * the master node table and creates a DDLJob to encapsulate information needed + * during the worker node portion of DDL execution before returning that DDLJob + * in a List. If no distributed table is involved, this function returns NIL. + */ +List * +PlanDropIndexStmt(DropStmt *dropIndexStatement, const char *dropIndexCommand) +{ + List *ddlJobs = NIL; + ListCell *dropObjectCell = NULL; + Oid distributedIndexId = InvalidOid; + Oid distributedRelationId = InvalidOid; + + Assert(dropIndexStatement->removeType == OBJECT_INDEX); + + /* check if any of the indexes being dropped belong to a distributed table */ + foreach(dropObjectCell, dropIndexStatement->objects) + { + Oid indexId = InvalidOid; + Oid relationId = InvalidOid; + bool isDistributedRelation = false; + struct DropRelationCallbackState state; + uint32 rvrFlags = RVR_MISSING_OK; + LOCKMODE lockmode = AccessExclusiveLock; + + List *objectNameList = (List *) lfirst(dropObjectCell); + RangeVar *rangeVar = makeRangeVarFromNameList(objectNameList); + + /* + * We don't support concurrently dropping indexes for distributed + * tables, but till this point, we don't know if it is a regular or a + * distributed table. + */ + if (dropIndexStatement->concurrent) + { + lockmode = ShareUpdateExclusiveLock; + } + + /* + * The next few statements are based on RemoveRelations() in + * commands/tablecmds.c in Postgres source. + */ + AcceptInvalidationMessages(); + + state.relkind = RELKIND_INDEX; + state.heapOid = InvalidOid; + state.concurrent = dropIndexStatement->concurrent; + + indexId = RangeVarGetRelidInternal(rangeVar, lockmode, rvrFlags, + RangeVarCallbackForDropIndex, + (void *) &state); + + /* + * If the index does not exist, we don't do anything here, and allow + * postgres to throw appropriate error or notice message later. + */ + if (!OidIsValid(indexId)) + { + continue; + } + + relationId = IndexGetRelation(indexId, false); + isDistributedRelation = IsDistributedTable(relationId); + if (isDistributedRelation) + { + distributedIndexId = indexId; + distributedRelationId = relationId; + break; + } + } + + if (OidIsValid(distributedIndexId)) + { + DDLJob *ddlJob = palloc0(sizeof(DDLJob)); + + ErrorIfUnsupportedDropIndexStmt(dropIndexStatement); + + ddlJob->targetRelationId = distributedRelationId; + ddlJob->concurrentIndexCmd = dropIndexStatement->concurrent; + ddlJob->commandString = dropIndexCommand; + ddlJob->taskList = DropIndexTaskList(distributedRelationId, distributedIndexId, + dropIndexStatement); + + ddlJobs = list_make1(ddlJob); + } + + return ddlJobs; +} + + +/* + * PostProcessIndexStmt marks new indexes invalid if they were created using the + * CONCURRENTLY flag. This (non-transactional) change provides the fallback + * state if an error is raised, otherwise a sub-sequent change to valid will be + * committed. + */ +void +PostProcessIndexStmt(IndexStmt *indexStmt) +{ + Relation relation = NULL; + Oid indexRelationId = InvalidOid; + Relation indexRelation = NULL; + Relation pg_index = NULL; + HeapTuple indexTuple = NULL; + Form_pg_index indexForm = NULL; + + /* we are only processing CONCURRENT index statements */ + if (!indexStmt->concurrent) + { + return; + } + + /* this logic only applies to the coordinator */ + if (!IsCoordinator()) + { + return; + } + + /* commit the current transaction and start anew */ + CommitTransactionCommand(); + StartTransactionCommand(); + + /* get the affected relation and index */ + relation = heap_openrv(indexStmt->relation, ShareUpdateExclusiveLock); + indexRelationId = get_relname_relid(indexStmt->idxname, + RelationGetNamespace(relation)); + indexRelation = index_open(indexRelationId, RowExclusiveLock); + + /* close relations but retain locks */ + heap_close(relation, NoLock); + index_close(indexRelation, NoLock); + + /* mark index as invalid, in-place (cannot be rolled back) */ + index_set_state_flags(indexRelationId, INDEX_DROP_CLEAR_VALID); + + /* re-open a transaction command from here on out */ + CommitTransactionCommand(); + StartTransactionCommand(); + + /* now, update index's validity in a way that can roll back */ + pg_index = heap_open(IndexRelationId, RowExclusiveLock); + + indexTuple = SearchSysCacheCopy1(INDEXRELID, ObjectIdGetDatum(indexRelationId)); + Assert(HeapTupleIsValid(indexTuple)); /* better be present, we have lock! */ + + /* mark as valid, save, and update pg_index indexes */ + indexForm = (Form_pg_index) GETSTRUCT(indexTuple); + indexForm->indisvalid = true; + + CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple); + + /* clean up; index now marked valid, but ROLLBACK will mark invalid */ + heap_freetuple(indexTuple); + heap_close(pg_index, RowExclusiveLock); +} + + +/* + * ErrorIfUnsupportedAlterIndexStmt checks if the corresponding alter index + * statement is supported for distributed tables and errors out if it is not. + * Currently, only the following commands are supported. + * + * ALTER INDEX SET () + * ALTER INDEX RESET () + */ +void +ErrorIfUnsupportedAlterIndexStmt(AlterTableStmt *alterTableStatement) +{ + List *commandList = alterTableStatement->cmds; + ListCell *commandCell = NULL; + + /* error out if any of the subcommands are unsupported */ + foreach(commandCell, commandList) + { + AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); + AlterTableType alterTableType = command->subtype; + + switch (alterTableType) + { + case AT_SetRelOptions: /* SET (...) */ + case AT_ResetRelOptions: /* RESET (...) */ + case AT_ReplaceRelOptions: /* replace entire option list */ + { + /* this command is supported by Citus */ + break; + } + + /* unsupported create index statements */ + case AT_SetTableSpace: + default: + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("alter index ... set tablespace ... " + "is currently unsupported"), + errdetail("Only RENAME TO, SET (), and RESET () " + "are supported."))); + return; /* keep compiler happy */ + } + } + } +} + + +/* + * CreateIndexTaskList builds a list of tasks to execute a CREATE INDEX command + * against a specified distributed table. + */ +static List * +CreateIndexTaskList(Oid relationId, IndexStmt *indexStmt) +{ + List *taskList = NIL; + List *shardIntervalList = LoadShardIntervalList(relationId); + ListCell *shardIntervalCell = NULL; + StringInfoData ddlString; + uint64 jobId = INVALID_JOB_ID; + int taskId = 1; + + initStringInfo(&ddlString); + + /* lock metadata before getting placement lists */ + LockShardListMetadata(shardIntervalList, ShareLock); + + foreach(shardIntervalCell, shardIntervalList) + { + ShardInterval *shardInterval = (ShardInterval *) lfirst(shardIntervalCell); + uint64 shardId = shardInterval->shardId; + Task *task = NULL; + + deparse_shard_index_statement(indexStmt, relationId, shardId, &ddlString); + + task = CitusMakeNode(Task); + task->jobId = jobId; + task->taskId = taskId++; + task->taskType = DDL_TASK; + task->queryString = pstrdup(ddlString.data); + task->replicationModel = REPLICATION_MODEL_INVALID; + task->dependedTaskList = NULL; + task->anchorShardId = shardId; + task->taskPlacementList = FinalizedShardPlacementList(shardId); + + taskList = lappend(taskList, task); + + resetStringInfo(&ddlString); + } + + return taskList; +} + + +/* + * Before acquiring a table lock, check whether we have sufficient rights. + * In the case of DROP INDEX, also try to lock the table before the index. + * + * This code is heavily borrowed from RangeVarCallbackForDropRelation() in + * commands/tablecmds.c in Postgres source. We need this to ensure the right + * order of locking while dealing with DROP INDEX statements. Because we are + * exclusively using this callback for INDEX processing, the PARTITION-related + * logic from PostgreSQL's similar callback has been omitted as unneeded. + */ +static void +RangeVarCallbackForDropIndex(const RangeVar *rel, Oid relOid, Oid oldRelOid, void *arg) +{ + /* *INDENT-OFF* */ + HeapTuple tuple; + struct DropRelationCallbackState *state; + char relkind; + char expected_relkind; + Form_pg_class classform; + LOCKMODE heap_lockmode; + + state = (struct DropRelationCallbackState *) arg; + relkind = state->relkind; + heap_lockmode = state->concurrent ? + ShareUpdateExclusiveLock : AccessExclusiveLock; + + Assert(relkind == RELKIND_INDEX); + + /* + * If we previously locked some other index's heap, and the name we're + * looking up no longer refers to that relation, release the now-useless + * lock. + */ + if (relOid != oldRelOid && OidIsValid(state->heapOid)) + { + UnlockRelationOid(state->heapOid, heap_lockmode); + state->heapOid = InvalidOid; + } + + /* Didn't find a relation, so no need for locking or permission checks. */ + if (!OidIsValid(relOid)) + return; + + tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); + if (!HeapTupleIsValid(tuple)) + return; /* concurrently dropped, so nothing to do */ + classform = (Form_pg_class) GETSTRUCT(tuple); + + /* + * PG 11 sends relkind as partitioned index for an index + * on partitioned table. It is handled the same + * as regular index as far as we are concerned here. + * + * See tablecmds.c:RangeVarCallbackForDropRelation() + */ + expected_relkind = classform->relkind; + +#if PG_VERSION_NUM >= 110000 + if (expected_relkind == RELKIND_PARTITIONED_INDEX) + expected_relkind = RELKIND_INDEX; +#endif + + if (expected_relkind != relkind) + ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is not an index", rel->relname))); + + /* Allow DROP to either table owner or schema owner */ + if (!pg_class_ownercheck(relOid, GetUserId()) && + !pg_namespace_ownercheck(classform->relnamespace, GetUserId())) + { + aclcheck_error(ACLCHECK_NOT_OWNER, ACLCHECK_OBJECT_INDEX, rel->relname); + } + + if (!allowSystemTableMods && IsSystemClass(relOid, classform)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied: \"%s\" is a system catalog", + rel->relname))); + + ReleaseSysCache(tuple); + + /* + * In DROP INDEX, attempt to acquire lock on the parent table before + * locking the index. index_drop() will need this anyway, and since + * regular queries lock tables before their indexes, we risk deadlock if + * we do it the other way around. No error if we don't find a pg_index + * entry, though --- the relation may have been dropped. + */ + if (relkind == RELKIND_INDEX && relOid != oldRelOid) + { + state->heapOid = IndexGetRelation(relOid, true); + if (OidIsValid(state->heapOid)) + LockRelationOid(state->heapOid, heap_lockmode); + } + /* *INDENT-ON* */ +} + + +/* + * ErrorIfUnsupportedIndexStmt checks if the corresponding index statement is + * supported for distributed tables and errors out if it is not. + */ +static void +ErrorIfUnsupportedIndexStmt(IndexStmt *createIndexStatement) +{ + char *indexRelationName = createIndexStatement->idxname; + if (indexRelationName == NULL) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("creating index without a name on a distributed table is " + "currently unsupported"))); + } + + if (createIndexStatement->tableSpace != NULL) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("specifying tablespaces with CREATE INDEX statements is " + "currently unsupported"))); + } + + if (createIndexStatement->unique) + { + RangeVar *relation = createIndexStatement->relation; + bool missingOk = false; + + /* caller uses ShareLock for non-concurrent indexes, use the same lock here */ + LOCKMODE lockMode = ShareLock; + Oid relationId = RangeVarGetRelid(relation, lockMode, missingOk); + Var *partitionKey = DistPartitionKey(relationId); + char partitionMethod = PartitionMethod(relationId); + List *indexParameterList = NIL; + ListCell *indexParameterCell = NULL; + bool indexContainsPartitionColumn = false; + + /* + * Reference tables do not have partition key, and unique constraints + * are allowed for them. Thus, we added a short-circuit for reference tables. + */ + if (partitionMethod == DISTRIBUTE_BY_NONE) + { + return; + } + + if (partitionMethod == DISTRIBUTE_BY_APPEND) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("creating unique indexes on append-partitioned tables " + "is currently unsupported"))); + } + + indexParameterList = createIndexStatement->indexParams; + foreach(indexParameterCell, indexParameterList) + { + IndexElem *indexElement = (IndexElem *) lfirst(indexParameterCell); + char *columnName = indexElement->name; + AttrNumber attributeNumber = InvalidAttrNumber; + + /* column name is null for index expressions, skip it */ + if (columnName == NULL) + { + continue; + } + + attributeNumber = get_attnum(relationId, columnName); + if (attributeNumber == partitionKey->varattno) + { + indexContainsPartitionColumn = true; + } + } + + if (!indexContainsPartitionColumn) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("creating unique indexes on non-partition " + "columns is currently unsupported"))); + } + } +} + + +/* + * ErrorIfUnsupportedDropIndexStmt checks if the corresponding drop index statement is + * supported for distributed tables and errors out if it is not. + */ +static void +ErrorIfUnsupportedDropIndexStmt(DropStmt *dropIndexStatement) +{ + Assert(dropIndexStatement->removeType == OBJECT_INDEX); + + if (list_length(dropIndexStatement->objects) > 1) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot drop multiple distributed objects in a " + "single command"), + errhint("Try dropping each object in a separate DROP " + "command."))); + } +} + + +/* + * DropIndexTaskList builds a list of tasks to execute a DROP INDEX command + * against a specified distributed table. + */ +static List * +DropIndexTaskList(Oid relationId, Oid indexId, DropStmt *dropStmt) +{ + List *taskList = NIL; + List *shardIntervalList = LoadShardIntervalList(relationId); + ListCell *shardIntervalCell = NULL; + char *indexName = get_rel_name(indexId); + Oid schemaId = get_rel_namespace(indexId); + char *schemaName = get_namespace_name(schemaId); + StringInfoData ddlString; + uint64 jobId = INVALID_JOB_ID; + int taskId = 1; + + initStringInfo(&ddlString); + + /* lock metadata before getting placement lists */ + LockShardListMetadata(shardIntervalList, ShareLock); + + foreach(shardIntervalCell, shardIntervalList) + { + ShardInterval *shardInterval = (ShardInterval *) lfirst(shardIntervalCell); + uint64 shardId = shardInterval->shardId; + char *shardIndexName = pstrdup(indexName); + Task *task = NULL; + + AppendShardIdToName(&shardIndexName, shardId); + + /* deparse shard-specific DROP INDEX command */ + appendStringInfo(&ddlString, "DROP INDEX %s %s %s %s", + (dropStmt->concurrent ? "CONCURRENTLY" : ""), + (dropStmt->missing_ok ? "IF EXISTS" : ""), + quote_qualified_identifier(schemaName, shardIndexName), + (dropStmt->behavior == DROP_RESTRICT ? "RESTRICT" : "CASCADE")); + + task = CitusMakeNode(Task); + task->jobId = jobId; + task->taskId = taskId++; + task->taskType = DDL_TASK; + task->queryString = pstrdup(ddlString.data); + task->replicationModel = REPLICATION_MODEL_INVALID; + task->dependedTaskList = NULL; + task->anchorShardId = shardId; + task->taskPlacementList = FinalizedShardPlacementList(shardId); + + taskList = lappend(taskList, task); + + resetStringInfo(&ddlString); + } + + return taskList; +} diff --git a/src/backend/distributed/commands/multi_copy.c b/src/backend/distributed/commands/multi_copy.c index c91db46f521..e50441f7d77 100644 --- a/src/backend/distributed/commands/multi_copy.c +++ b/src/backend/distributed/commands/multi_copy.c @@ -55,13 +55,17 @@ #include "access/htup_details.h" #include "access/htup.h" #include "access/sdir.h" +#include "access/sysattr.h" +#include "access/xact.h" #include "catalog/namespace.h" #include "catalog/pg_type.h" #include "commands/copy.h" #include "commands/defrem.h" +#include "distributed/commands/multi_copy.h" +#include "distributed/commands/utility_hook.h" +#include "distributed/intermediate_results.h" #include "distributed/master_protocol.h" #include "distributed/metadata_cache.h" -#include "distributed/multi_copy.h" #include "distributed/multi_partitioning_utils.h" #include "distributed/multi_physical_planner.h" #include "distributed/multi_router_planner.h" @@ -73,7 +77,9 @@ #include "distributed/resource_lock.h" #include "distributed/shard_pruning.h" #include "distributed/version_compat.h" +#include "distributed/worker_protocol.h" #include "executor/executor.h" +#include "foreign/foreign.h" #include "libpq/pqformat.h" #include "nodes/makefuncs.h" #include "tsearch/ts_locale.h" @@ -133,6 +139,13 @@ static CopyCoercionData * ColumnCoercionPaths(TupleDesc destTupleDescriptor, static FmgrInfo * TypeOutputFunctions(uint32 columnCount, Oid *typeIdArray, bool binaryFormat); static Datum CoerceColumnValue(Datum inputValue, CopyCoercionData *coercionPath); +static void CreateLocalTable(RangeVar *relation, char *nodeName, int32 nodePort); +static void CheckCopyPermissions(CopyStmt *copyStatement); +static List * CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist); +static bool IsCopyResultStmt(CopyStmt *copyStatement); +static bool IsCopyFromWorker(CopyStmt *copyStatement); +static NodeAddress * MasterNodeAddress(CopyStmt *copyStatement); +static void CitusCopyFrom(CopyStmt *copyStatement, char *completionTag); /* Private functions copied and adapted from copy.c in PostgreSQL */ static void CopySendData(CopyOutState outputState, const void *databuf, int datasize); @@ -161,7 +174,7 @@ PG_FUNCTION_INFO_V1(citus_text_send_as_jsonb); * statement to related subfunctions based on where the copy command is run * and the partition method of the distributed table. */ -void +static void CitusCopyFrom(CopyStmt *copyStatement, char *completionTag) { bool isCopyFromWorker = false; @@ -200,7 +213,7 @@ CitusCopyFrom(CopyStmt *copyStatement, char *completionTag) Oid relationId = RangeVarGetRelid(copyStatement->relation, NoLock, false); char partitionMethod = PartitionMethod(relationId); - /* disallow modifications to a partition table which have rep. factpr > 1 */ + /* disallow modifications to a partition table which have rep. factor > 1 */ EnsurePartitionTableNotReplicated(relationId); if (partitionMethod == DISTRIBUTE_BY_HASH || partitionMethod == @@ -226,7 +239,7 @@ CitusCopyFrom(CopyStmt *copyStatement, char *completionTag) /* * IsCopyFromWorker checks if the given copy statement has the master host option. */ -bool +static bool IsCopyFromWorker(CopyStmt *copyStatement) { ListCell *optionCell = NULL; @@ -680,7 +693,7 @@ CopyToNewShards(CopyStmt *copyStatement, char *completionTag, Oid relationId) * it. Note that if the master_port is not provided, we use 5432 as the default * port. */ -NodeAddress * +static NodeAddress * MasterNodeAddress(CopyStmt *copyStatement) { NodeAddress *masterNodeAddress = (NodeAddress *) palloc0(sizeof(NodeAddress)); @@ -2423,3 +2436,414 @@ CitusCopyDestReceiverDestroy(DestReceiver *destReceiver) pfree(copyDest); } + + +/* + * IsCopyResultStmt determines whether the given copy statement is a + * COPY "resultkey" FROM STDIN WITH (format result) statement, which is used + * to copy query results from the coordinator into workers. + */ +static bool +IsCopyResultStmt(CopyStmt *copyStatement) +{ + ListCell *optionCell = NULL; + bool hasFormatReceive = false; + + /* extract WITH (...) options from the COPY statement */ + foreach(optionCell, copyStatement->options) + { + DefElem *defel = (DefElem *) lfirst(optionCell); + + if (strncmp(defel->defname, "format", NAMEDATALEN) == 0 && + strncmp(defGetString(defel), "result", NAMEDATALEN) == 0) + { + hasFormatReceive = true; + break; + } + } + + return hasFormatReceive; +} + + +/* + * ProcessCopyStmt handles Citus specific concerns for COPY like supporting + * COPYing from distributed tables and preventing unsupported actions. The + * function returns a modified COPY statement to be executed, or NULL if no + * further processing is needed. + * + * commandMustRunAsOwner is an output parameter used to communicate to the caller whether + * the copy statement should be executed using elevated privileges. If + * ProcessCopyStmt that is required, a call to CheckCopyPermissions will take + * care of verifying the current user's permissions before ProcessCopyStmt + * returns. + */ +Node * +ProcessCopyStmt(CopyStmt *copyStatement, char *completionTag, bool *commandMustRunAsOwner) +{ + *commandMustRunAsOwner = false; /* make sure variable is initialized */ + + /* + * Handle special COPY "resultid" FROM STDIN WITH (format result) commands + * for sending intermediate results to workers. + */ + if (IsCopyResultStmt(copyStatement)) + { + const char *resultId = copyStatement->relation->relname; + + ReceiveQueryResultViaCopy(resultId); + + return NULL; + } + + /* + * We check whether a distributed relation is affected. For that, we need to open the + * relation. To prevent race conditions with later lookups, lock the table, and modify + * the rangevar to include the schema. + */ + if (copyStatement->relation != NULL) + { + bool isDistributedRelation = false; + bool isCopyFromWorker = IsCopyFromWorker(copyStatement); + + if (isCopyFromWorker) + { + RangeVar *relation = copyStatement->relation; + NodeAddress *masterNodeAddress = MasterNodeAddress(copyStatement); + char *nodeName = masterNodeAddress->nodeName; + int32 nodePort = masterNodeAddress->nodePort; + + CreateLocalTable(relation, nodeName, nodePort); + + /* + * We expect copy from worker to be on a distributed table; otherwise, + * it fails in CitusCopyFrom() while checking the partition method. + */ + isDistributedRelation = true; + } + else + { + bool isFrom = copyStatement->is_from; + Relation copiedRelation = NULL; + char *schemaName = NULL; + MemoryContext relationContext = NULL; + + /* consider using RangeVarGetRelidExtended to check perms before locking */ + copiedRelation = heap_openrv(copyStatement->relation, + isFrom ? RowExclusiveLock : AccessShareLock); + + isDistributedRelation = IsDistributedTable(RelationGetRelid(copiedRelation)); + + /* ensure future lookups hit the same relation */ + schemaName = get_namespace_name(RelationGetNamespace(copiedRelation)); + + /* ensure we copy string into proper context */ + relationContext = GetMemoryChunkContext(copyStatement->relation); + schemaName = MemoryContextStrdup(relationContext, schemaName); + copyStatement->relation->schemaname = schemaName; + + heap_close(copiedRelation, NoLock); + } + + if (isDistributedRelation) + { + if (copyStatement->is_from) + { + /* check permissions, we're bypassing postgres' normal checks */ + if (!isCopyFromWorker) + { + CheckCopyPermissions(copyStatement); + } + + CitusCopyFrom(copyStatement, completionTag); + return NULL; + } + else if (!copyStatement->is_from) + { + /* + * The copy code only handles SELECTs in COPY ... TO on master tables, + * as that can be done non-invasively. To handle COPY master_rel TO + * the copy statement is replaced by a generated select statement. + */ + ColumnRef *allColumns = makeNode(ColumnRef); + SelectStmt *selectStmt = makeNode(SelectStmt); + ResTarget *selectTarget = makeNode(ResTarget); + + allColumns->fields = list_make1(makeNode(A_Star)); + allColumns->location = -1; + + selectTarget->name = NULL; + selectTarget->indirection = NIL; + selectTarget->val = (Node *) allColumns; + selectTarget->location = -1; + + selectStmt->targetList = list_make1(selectTarget); + selectStmt->fromClause = list_make1(copyObject(copyStatement->relation)); + + /* replace original statement */ + copyStatement = copyObject(copyStatement); + copyStatement->relation = NULL; + copyStatement->query = (Node *) selectStmt; + } + } + } + + + if (copyStatement->filename != NULL && !copyStatement->is_program) + { + const char *filename = copyStatement->filename; + + if (CacheDirectoryElement(filename)) + { + /* + * Only superusers are allowed to copy from a file, so we have to + * become superuser to execute copies to/from files used by citus' + * query execution. + * + * XXX: This is a decidedly suboptimal solution, as that means + * that triggers, input functions, etc. run with elevated + * privileges. But this is better than not being able to run + * queries as normal user. + */ + *commandMustRunAsOwner = true; + + /* + * Have to manually check permissions here as the COPY is will be + * run as a superuser. + */ + if (copyStatement->relation != NULL) + { + CheckCopyPermissions(copyStatement); + } + + /* + * Check if we have a "COPY (query) TO filename". If we do, copy + * doesn't accept relative file paths. However, SQL tasks that get + * assigned to worker nodes have relative paths. We therefore + * convert relative paths to absolute ones here. + */ + if (copyStatement->relation == NULL && + !copyStatement->is_from && + !is_absolute_path(filename)) + { + copyStatement->filename = make_absolute_path(filename); + } + } + } + + + return (Node *) copyStatement; +} + + +/* + * CreateLocalTable gets DDL commands from the remote node for the given + * relation. Then, it creates the local relation as temporary and on commit drop. + */ +static void +CreateLocalTable(RangeVar *relation, char *nodeName, int32 nodePort) +{ + List *ddlCommandList = NIL; + ListCell *ddlCommandCell = NULL; + + char *relationName = relation->relname; + char *schemaName = relation->schemaname; + char *qualifiedRelationName = quote_qualified_identifier(schemaName, relationName); + + /* + * The warning message created in TableDDLCommandList() is descriptive + * enough; therefore, we just throw an error which says that we could not + * run the copy operation. + */ + ddlCommandList = TableDDLCommandList(nodeName, nodePort, qualifiedRelationName); + if (ddlCommandList == NIL) + { + ereport(ERROR, (errmsg("could not run copy from the worker node"))); + } + + /* apply DDL commands against the local database */ + foreach(ddlCommandCell, ddlCommandList) + { + StringInfo ddlCommand = (StringInfo) lfirst(ddlCommandCell); + Node *ddlCommandNode = ParseTreeNode(ddlCommand->data); + bool applyDDLCommand = false; + + if (IsA(ddlCommandNode, CreateStmt) || + IsA(ddlCommandNode, CreateForeignTableStmt)) + { + CreateStmt *createStatement = (CreateStmt *) ddlCommandNode; + + /* create the local relation as temporary and on commit drop */ + createStatement->relation->relpersistence = RELPERSISTENCE_TEMP; + createStatement->oncommit = ONCOMMIT_DROP; + + /* temporarily strip schema name */ + createStatement->relation->schemaname = NULL; + + applyDDLCommand = true; + } + else if (IsA(ddlCommandNode, CreateForeignServerStmt)) + { + CreateForeignServerStmt *createServerStmt = + (CreateForeignServerStmt *) ddlCommandNode; + if (GetForeignServerByName(createServerStmt->servername, true) == NULL) + { + /* create server if not exists */ + applyDDLCommand = true; + } + } + else if ((IsA(ddlCommandNode, CreateExtensionStmt))) + { + applyDDLCommand = true; + } + else if ((IsA(ddlCommandNode, CreateSeqStmt))) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot copy to table with serial column from worker"), + errhint("Connect to the master node to COPY to tables which " + "use serial column types."))); + } + + /* run only a selected set of DDL commands */ + if (applyDDLCommand) + { + CitusProcessUtility(ddlCommandNode, CreateCommandTag(ddlCommandNode), + PROCESS_UTILITY_TOPLEVEL, NULL, None_Receiver, NULL); + + CommandCounterIncrement(); + } + } +} + + +/* + * Check whether the current user has the permission to execute a COPY + * statement, raise ERROR if not. In some cases we have to do this separately + * from postgres' copy.c, because we have to execute the copy with elevated + * privileges. + * + * Copied from postgres, where it's part of DoCopy(). + */ +static void +CheckCopyPermissions(CopyStmt *copyStatement) +{ + /* *INDENT-OFF* */ + bool is_from = copyStatement->is_from; + Relation rel; + Oid relid; + List *range_table = NIL; + TupleDesc tupDesc; + AclMode required_access = (is_from ? ACL_INSERT : ACL_SELECT); + List *attnums; + ListCell *cur; + RangeTblEntry *rte; + + rel = heap_openrv(copyStatement->relation, + is_from ? RowExclusiveLock : AccessShareLock); + + relid = RelationGetRelid(rel); + + rte = makeNode(RangeTblEntry); + rte->rtekind = RTE_RELATION; + rte->relid = relid; + rte->relkind = rel->rd_rel->relkind; + rte->requiredPerms = required_access; + range_table = list_make1(rte); + + tupDesc = RelationGetDescr(rel); + + attnums = CopyGetAttnums(tupDesc, rel, copyStatement->attlist); + foreach(cur, attnums) + { + int attno = lfirst_int(cur) - FirstLowInvalidHeapAttributeNumber; + + if (is_from) + { + rte->insertedCols = bms_add_member(rte->insertedCols, attno); + } + else + { + rte->selectedCols = bms_add_member(rte->selectedCols, attno); + } + } + + ExecCheckRTPerms(range_table, true); + + /* TODO: Perform RLS checks once supported */ + + heap_close(rel, NoLock); + /* *INDENT-ON* */ +} + + +/* Helper for CheckCopyPermissions(), copied from postgres */ +static List * +CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist) +{ + /* *INDENT-OFF* */ + List *attnums = NIL; + + if (attnamelist == NIL) + { + /* Generate default column list */ + int attr_count = tupDesc->natts; + int i; + + for (i = 0; i < attr_count; i++) + { + if (TupleDescAttr(tupDesc, i)->attisdropped) + continue; + attnums = lappend_int(attnums, i + 1); + } + } + else + { + /* Validate the user-supplied list and extract attnums */ + ListCell *l; + + foreach(l, attnamelist) + { + char *name = strVal(lfirst(l)); + int attnum; + int i; + + /* Lookup column name */ + attnum = InvalidAttrNumber; + for (i = 0; i < tupDesc->natts; i++) + { + Form_pg_attribute att = TupleDescAttr(tupDesc, i); + + if (att->attisdropped) + continue; + if (namestrcmp(&(att->attname), name) == 0) + { + attnum = att->attnum; + break; + } + } + if (attnum == InvalidAttrNumber) + { + if (rel != NULL) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" of relation \"%s\" does not exist", + name, RelationGetRelationName(rel)))); + else + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_COLUMN), + errmsg("column \"%s\" does not exist", + name))); + } + /* Check for duplicates */ + if (list_member_int(attnums, attnum)) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_COLUMN), + errmsg("column \"%s\" specified more than once", + name))); + attnums = lappend_int(attnums, attnum); + } + } + + return attnums; + /* *INDENT-ON* */ +} diff --git a/src/backend/distributed/ddl/policy.c b/src/backend/distributed/commands/policy.c similarity index 98% rename from src/backend/distributed/ddl/policy.c rename to src/backend/distributed/commands/policy.c index 3d523aeebf5..acc40aff6c6 100644 --- a/src/backend/distributed/ddl/policy.c +++ b/src/backend/distributed/commands/policy.c @@ -12,8 +12,8 @@ #include "catalog/namespace.h" #include "commands/policy.h" +#include "distributed/commands.h" #include "distributed/metadata_cache.h" -#include "distributed/policy.h" #include "utils/builtins.h" diff --git a/src/backend/distributed/commands/rename.c b/src/backend/distributed/commands/rename.c new file mode 100644 index 00000000000..042e080379d --- /dev/null +++ b/src/backend/distributed/commands/rename.c @@ -0,0 +1,139 @@ +/*------------------------------------------------------------------------- + * + * rename.c + * Commands for renaming objects related to distributed tables + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/index.h" +#include "catalog/namespace.h" +#include "distributed/commands.h" +#include "distributed/commands/utility_hook.h" +#include "distributed/metadata_cache.h" +#include "nodes/parsenodes.h" + + +/* + * PlanRenameStmt first determines whether a given rename statement involves + * a distributed table. If so (and if it is supported, i.e. renames a column), + * it creates a DDLJob to encapsulate information needed during the worker node + * portion of DDL execution before returning that DDLJob in a List. If no dis- + * tributed table is involved, this function returns NIL. + */ +List * +PlanRenameStmt(RenameStmt *renameStmt, const char *renameCommand) +{ + Oid objectRelationId = InvalidOid; /* SQL Object OID */ + Oid tableRelationId = InvalidOid; /* Relation OID, maybe not the same. */ + bool isDistributedRelation = false; + DDLJob *ddlJob = NULL; + + /* + * We only support some of the PostgreSQL supported RENAME statements, and + * our list include only renaming table and index (related) objects. + */ + if (!IsAlterTableRenameStmt(renameStmt) && + !IsIndexRenameStmt(renameStmt) && + !IsPolicyRenameStmt(renameStmt)) + { + return NIL; + } + + /* + * The lock levels here should be same as the ones taken in + * RenameRelation(), renameatt() and RenameConstraint(). However, since all + * three statements have identical lock levels, we just use a single statement. + */ + objectRelationId = RangeVarGetRelid(renameStmt->relation, + AccessExclusiveLock, + renameStmt->missing_ok); + + /* + * If the table does not exist, don't do anything here to allow PostgreSQL + * to throw the appropriate error or notice message later. + */ + if (!OidIsValid(objectRelationId)) + { + return NIL; + } + + /* we have no planning to do unless the table is distributed */ + switch (renameStmt->renameType) + { + case OBJECT_TABLE: + case OBJECT_COLUMN: + case OBJECT_TABCONSTRAINT: + case OBJECT_POLICY: + { + /* the target object is our tableRelationId. */ + tableRelationId = objectRelationId; + break; + } + + case OBJECT_INDEX: + { + /* + * here, objRelationId points to the index relation entry, and we + * are interested into the entry of the table on which the index is + * defined. + */ + tableRelationId = IndexGetRelation(objectRelationId, false); + break; + } + + default: + + /* + * Nodes that are not supported by Citus: we pass-through to the + * main PostgreSQL executor. Any Citus-supported RenameStmt + * renameType must appear above in the switch, explicitly. + */ + return NIL; + } + + isDistributedRelation = IsDistributedTable(tableRelationId); + if (!isDistributedRelation) + { + return NIL; + } + + /* + * We might ERROR out on some commands, but only for Citus tables where + * isDistributedRelation is true. That's why this test comes this late in + * the function. + */ + ErrorIfUnsupportedRenameStmt(renameStmt); + + ddlJob = palloc0(sizeof(DDLJob)); + ddlJob->targetRelationId = tableRelationId; + ddlJob->concurrentIndexCmd = false; + ddlJob->commandString = renameCommand; + ddlJob->taskList = DDLTaskList(tableRelationId, renameCommand); + + return list_make1(ddlJob); +} + + +/* + * ErrorIfDistributedRenameStmt errors out if the corresponding rename statement + * operates on any part of a distributed table other than a column. + * + * Note: This function handles RenameStmt applied to relations handed by Citus. + * At the moment of writing this comment, it could be either tables or indexes. + */ +void +ErrorIfUnsupportedRenameStmt(RenameStmt *renameStmt) +{ + if (IsAlterTableRenameStmt(renameStmt) && + renameStmt->renameType == OBJECT_TABCONSTRAINT) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("renaming constraints belonging to distributed tables is " + "currently unsupported"))); + } +} diff --git a/src/backend/distributed/commands/schema.c b/src/backend/distributed/commands/schema.c new file mode 100644 index 00000000000..d85d6afc1b8 --- /dev/null +++ b/src/backend/distributed/commands/schema.c @@ -0,0 +1,159 @@ +/*------------------------------------------------------------------------- + * + * schema.c + * Commands for creating and altering schemas for distributed tables. + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + + +#include "postgres.h" + +#include "access/genam.h" +#include "access/heapam.h" +#include "access/htup.h" +#include "access/htup_details.h" +#include "catalog/namespace.h" +#include "catalog/pg_class.h" +#include "distributed/commands.h" +#include "distributed/commands/utility_hook.h" +#include "distributed/metadata_cache.h" +#include "nodes/parsenodes.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" +#include "utils/relcache.h" + + +/* Local functions forward declarations for helper functions */ +static char * GetSchemaNameFromDropObject(ListCell *dropSchemaCell); + +/* + * ProcessDropSchemaStmt invalidates the foreign key cache if any table created + * under dropped schema involved in any foreign key relationship. + */ +void +ProcessDropSchemaStmt(DropStmt *dropStatement) +{ + Relation pgClass = NULL; + HeapTuple heapTuple = NULL; + SysScanDesc scanDescriptor = NULL; + ScanKeyData scanKey[1]; + int scanKeyCount = 1; + Oid scanIndexId = InvalidOid; + bool useIndex = false; + ListCell *dropSchemaCell; + + if (dropStatement->behavior != DROP_CASCADE) + { + return; + } + + foreach(dropSchemaCell, dropStatement->objects) + { + char *schemaString = GetSchemaNameFromDropObject(dropSchemaCell); + Oid namespaceOid = get_namespace_oid(schemaString, true); + + if (namespaceOid == InvalidOid) + { + continue; + } + + pgClass = heap_open(RelationRelationId, AccessShareLock); + + ScanKeyInit(&scanKey[0], Anum_pg_class_relnamespace, BTEqualStrategyNumber, + F_OIDEQ, namespaceOid); + scanDescriptor = systable_beginscan(pgClass, scanIndexId, useIndex, NULL, + scanKeyCount, scanKey); + + heapTuple = systable_getnext(scanDescriptor); + while (HeapTupleIsValid(heapTuple)) + { + Form_pg_class relationForm = (Form_pg_class) GETSTRUCT(heapTuple); + char *relationName = NameStr(relationForm->relname); + Oid relationId = get_relname_relid(relationName, namespaceOid); + + /* we're not interested in non-valid, non-distributed relations */ + if (relationId == InvalidOid || !IsDistributedTable(relationId)) + { + heapTuple = systable_getnext(scanDescriptor); + continue; + } + + /* invalidate foreign key cache if the table involved in any foreign key */ + if (TableReferenced(relationId) || TableReferencing(relationId)) + { + MarkInvalidateForeignKeyGraph(); + + systable_endscan(scanDescriptor); + heap_close(pgClass, NoLock); + return; + } + + heapTuple = systable_getnext(scanDescriptor); + } + + systable_endscan(scanDescriptor); + heap_close(pgClass, NoLock); + } +} + + +/* + * PlanAlterObjectSchemaStmt determines whether a given ALTER ... SET SCHEMA + * statement involves a distributed table and issues a warning if so. Because + * we do not support distributed ALTER ... SET SCHEMA, this function always + * returns NIL. + */ +List * +PlanAlterObjectSchemaStmt(AlterObjectSchemaStmt *alterObjectSchemaStmt, + const char *alterObjectSchemaCommand) +{ + Oid relationId = InvalidOid; + + if (alterObjectSchemaStmt->relation == NULL) + { + return NIL; + } + + relationId = RangeVarGetRelid(alterObjectSchemaStmt->relation, + AccessExclusiveLock, + alterObjectSchemaStmt->missing_ok); + + /* first check whether a distributed relation is affected */ + if (!OidIsValid(relationId) || !IsDistributedTable(relationId)) + { + return NIL; + } + + /* emit a warning if a distributed relation is affected */ + ereport(WARNING, (errmsg("not propagating ALTER ... SET SCHEMA commands to " + "worker nodes"), + errhint("Connect to worker nodes directly to manually " + "change schemas of affected objects."))); + + return NIL; +} + + +/* + * GetSchemaNameFromDropObject gets the name of the drop schema from given + * list cell. This function is defined due to API change between PG 9.6 and + * PG 10. + */ +static char * +GetSchemaNameFromDropObject(ListCell *dropSchemaCell) +{ + char *schemaString = NULL; + +#if (PG_VERSION_NUM >= 100000) + Value *schemaValue = (Value *) lfirst(dropSchemaCell); + schemaString = strVal(schemaValue); +#else + List *schemaNameList = (List *) lfirst(dropSchemaCell); + schemaString = NameListToString(schemaNameList); +#endif + + return schemaString; +} diff --git a/src/backend/distributed/commands/sequence.c b/src/backend/distributed/commands/sequence.c new file mode 100644 index 00000000000..2b5a2a0cae4 --- /dev/null +++ b/src/backend/distributed/commands/sequence.c @@ -0,0 +1,155 @@ +/*------------------------------------------------------------------------- + * + * sequence.c + * This file contains implementation of CREATE and ALTER SEQUENCE + * statement functions to run in a distributed setting + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/dependency.h" +#include "catalog/namespace.h" +#include "commands/defrem.h" +#include "distributed/commands.h" +#include "distributed/metadata_cache.h" +#include "nodes/parsenodes.h" + +/* Local functions forward declarations for helper functions */ +static bool OptionsSpecifyOwnedBy(List *optionList, Oid *ownedByTableId); + + +/* + * ErrorIfUnsupportedSeqStmt errors out if the provided create sequence + * statement specifies a distributed table in its OWNED BY clause. + */ +void +ErrorIfUnsupportedSeqStmt(CreateSeqStmt *createSeqStmt) +{ + Oid ownedByTableId = InvalidOid; + + /* create is easy: just prohibit any distributed OWNED BY */ + if (OptionsSpecifyOwnedBy(createSeqStmt->options, &ownedByTableId)) + { + if (IsDistributedTable(ownedByTableId)) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot create sequences that specify a distributed " + "table in their OWNED BY option"), + errhint("Use a sequence in a distributed table by specifying " + "a serial column type before creating any shards."))); + } + } +} + + +/* + * ErrorIfDistributedAlterSeqOwnedBy errors out if the provided alter sequence + * statement attempts to change the owned by property of a distributed sequence + * or attempt to change a local sequence to be owned by a distributed table. + */ +void +ErrorIfDistributedAlterSeqOwnedBy(AlterSeqStmt *alterSeqStmt) +{ + Oid sequenceId = RangeVarGetRelid(alterSeqStmt->sequence, AccessShareLock, + alterSeqStmt->missing_ok); + bool sequenceOwned = false; + Oid ownedByTableId = InvalidOid; + Oid newOwnedByTableId = InvalidOid; + int32 ownedByColumnId = 0; + bool hasDistributedOwner = false; + + /* alter statement referenced nonexistent sequence; return */ + if (sequenceId == InvalidOid) + { + return; + } + +#if (PG_VERSION_NUM >= 100000) + sequenceOwned = sequenceIsOwned(sequenceId, DEPENDENCY_AUTO, &ownedByTableId, + &ownedByColumnId); + if (!sequenceOwned) + { + sequenceOwned = sequenceIsOwned(sequenceId, DEPENDENCY_INTERNAL, &ownedByTableId, + &ownedByColumnId); + } +#else + sequenceOwned = sequenceIsOwned(sequenceId, &ownedByTableId, &ownedByColumnId); +#endif + + /* see whether the sequence is already owned by a distributed table */ + if (sequenceOwned) + { + hasDistributedOwner = IsDistributedTable(ownedByTableId); + } + + if (OptionsSpecifyOwnedBy(alterSeqStmt->options, &newOwnedByTableId)) + { + /* if a distributed sequence tries to change owner, error */ + if (hasDistributedOwner && ownedByTableId != newOwnedByTableId) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot alter OWNED BY option of a sequence " + "already owned by a distributed table"))); + } + else if (!hasDistributedOwner && IsDistributedTable(newOwnedByTableId)) + { + /* and don't let local sequences get a distributed OWNED BY */ + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot associate an existing sequence with a " + "distributed table"), + errhint("Use a sequence in a distributed table by specifying " + "a serial column type before creating any shards."))); + } + } +} + + +/* + * OptionsSpecifyOwnedBy processes the options list of either a CREATE or ALTER + * SEQUENCE command, extracting the first OWNED BY option it encounters. The + * identifier for the specified table is placed in the Oid out parameter before + * returning true. Returns false if no such option is found. Still returns true + * for OWNED BY NONE, but leaves the out paramter set to InvalidOid. + */ +static bool +OptionsSpecifyOwnedBy(List *optionList, Oid *ownedByTableId) +{ + ListCell *optionCell = NULL; + + foreach(optionCell, optionList) + { + DefElem *defElem = (DefElem *) lfirst(optionCell); + if (strcmp(defElem->defname, "owned_by") == 0) + { + List *ownedByNames = defGetQualifiedName(defElem); + int nameCount = list_length(ownedByNames); + + /* if only one name is present, this is OWNED BY NONE */ + if (nameCount == 1) + { + *ownedByTableId = InvalidOid; + return true; + } + else + { + /* + * Otherwise, we have a list of schema, table, column, which we + * need to truncate to simply the schema and table to determine + * the relevant relation identifier. + */ + List *relNameList = list_truncate(list_copy(ownedByNames), nameCount - 1); + RangeVar *rangeVar = makeRangeVarFromNameList(relNameList); + bool failOK = true; + + *ownedByTableId = RangeVarGetRelid(rangeVar, NoLock, failOK); + return true; + } + } + } + + return false; +} diff --git a/src/backend/distributed/commands/subscription.c b/src/backend/distributed/commands/subscription.c new file mode 100644 index 00000000000..3b5495e3ab0 --- /dev/null +++ b/src/backend/distributed/commands/subscription.c @@ -0,0 +1,27 @@ +/*------------------------------------------------------------------------- + * + * subscription.c + * Commands for creating subscriptions + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#if (PG_VERSION_NUM >= 100000) + +#include "distributed/commands.h" +#include "nodes/parsenodes.h" + + +/* placeholder for ProcessCreateSubscriptionStmt */ +Node * +ProcessCreateSubscriptionStmt(CreateSubscriptionStmt *createSubStmt) +{ + return (Node *) createSubStmt; +} + + +#endif /* PG_VERSION_NUM >= 100000 */ diff --git a/src/backend/distributed/commands/table.c b/src/backend/distributed/commands/table.c new file mode 100644 index 00000000000..ddb4e5ace95 --- /dev/null +++ b/src/backend/distributed/commands/table.c @@ -0,0 +1,1380 @@ +/*------------------------------------------------------------------------- + * + * table.c + * Commands for creating and altering distributed tables. + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "access/htup_details.h" +#include "access/xact.h" +#include "catalog/index.h" +#include "catalog/pg_class.h" +#include "commands/tablecmds.h" +#include "distributed/citus_ruleutils.h" +#include "distributed/colocation_utils.h" +#include "distributed/commands.h" +#include "distributed/commands/utility_hook.h" +#include "distributed/master_protocol.h" +#include "distributed/metadata_sync.h" +#include "distributed/multi_executor.h" +#include "distributed/multi_partitioning_utils.h" +#include "distributed/relation_access_tracking.h" +#include "distributed/resource_lock.h" +#include "distributed/version_compat.h" +#include "lib/stringinfo.h" +#include "nodes/parsenodes.h" +#include "storage/lmgr.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + + +/* Local functions forward declarations for unsupported command checks */ +static void ErrorIfUnsupportedAlterTableStmt(AlterTableStmt *alterTableStatement); +static List * InterShardDDLTaskList(Oid leftRelationId, Oid rightRelationId, + const char *commandString); +static bool AlterInvolvesPartitionColumn(AlterTableStmt *alterTableStatement, + AlterTableCmd *command); +static void ErrorIfUnsupportedAlterAddConstraintStmt(AlterTableStmt *alterTableStatement); + +/* + * We need to run some of the commands sequentially if there is a foreign constraint + * from/to reference table. + */ +static bool SetupExecutionModeForAlterTable(Oid relationId, AlterTableCmd *command); + +/* + * ProcessDropTableStmt processes DROP TABLE commands for partitioned tables. + * If we are trying to DROP partitioned tables, we first need to go to MX nodes + * and DETACH partitions from their parents. Otherwise, we process DROP command + * multiple times in MX workers. For shards, we send DROP commands with IF EXISTS + * parameter which solves problem of processing same command multiple times. + * However, for distributed table itself, we directly remove related table from + * Postgres catalogs via performDeletion function, thus we need to be cautious + * about not processing same DROP command twice. + */ +void +ProcessDropTableStmt(DropStmt *dropTableStatement) +{ + ListCell *dropTableCell = NULL; + + Assert(dropTableStatement->removeType == OBJECT_TABLE); + + foreach(dropTableCell, dropTableStatement->objects) + { + List *tableNameList = (List *) lfirst(dropTableCell); + RangeVar *tableRangeVar = makeRangeVarFromNameList(tableNameList); + bool missingOK = true; + List *partitionList = NIL; + ListCell *partitionCell = NULL; + + Oid relationId = RangeVarGetRelid(tableRangeVar, AccessShareLock, missingOK); + + /* we're not interested in non-valid, non-distributed relations */ + if (relationId == InvalidOid || !IsDistributedTable(relationId)) + { + continue; + } + + /* invalidate foreign key cache if the table involved in any foreign key */ + if ((TableReferenced(relationId) || TableReferencing(relationId))) + { + MarkInvalidateForeignKeyGraph(); + } + + /* we're only interested in partitioned and mx tables */ + if (!ShouldSyncTableMetadata(relationId) || !PartitionedTable(relationId)) + { + continue; + } + + EnsureCoordinator(); + + partitionList = PartitionList(relationId); + if (list_length(partitionList) == 0) + { + continue; + } + + SendCommandToWorkers(WORKERS_WITH_METADATA, DISABLE_DDL_PROPAGATION); + + foreach(partitionCell, partitionList) + { + Oid partitionRelationId = lfirst_oid(partitionCell); + char *detachPartitionCommand = + GenerateDetachPartitionCommand(partitionRelationId); + + SendCommandToWorkers(WORKERS_WITH_METADATA, detachPartitionCommand); + } + } +} + + +/* + * ProcessCreateTableStmtPartitionOf takes CreateStmt object as a parameter but + * it only processes CREATE TABLE ... PARTITION OF statements and it checks if + * user creates the table as a partition of a distributed table. In that case, + * it distributes partition as well. Since the table itself is a partition, + * CreateDistributedTable will attach it to its parent table automatically after + * distributing it. + * + * This function does nothing if PostgreSQL's version is less then 10 and given + * CreateStmt is not a CREATE TABLE ... PARTITION OF command. + */ +void +ProcessCreateTableStmtPartitionOf(CreateStmt *createStatement) +{ +#if (PG_VERSION_NUM >= 100000) + if (createStatement->inhRelations != NIL && createStatement->partbound != NULL) + { + RangeVar *parentRelation = linitial(createStatement->inhRelations); + bool parentMissingOk = false; + Oid parentRelationId = RangeVarGetRelid(parentRelation, NoLock, + parentMissingOk); + + /* a partition can only inherit from single parent table */ + Assert(list_length(createStatement->inhRelations) == 1); + + Assert(parentRelationId != InvalidOid); + + /* + * If a partition is being created and if its parent is a distributed + * table, we will distribute this table as well. + */ + if (IsDistributedTable(parentRelationId)) + { + bool missingOk = false; + Oid relationId = RangeVarGetRelid(createStatement->relation, NoLock, + missingOk); + Var *parentDistributionColumn = DistPartitionKey(parentRelationId); + char parentDistributionMethod = DISTRIBUTE_BY_HASH; + char *parentRelationName = generate_qualified_relation_name(parentRelationId); + bool viaDeprecatedAPI = false; + + CreateDistributedTable(relationId, parentDistributionColumn, + parentDistributionMethod, parentRelationName, + viaDeprecatedAPI); + } + } +#endif +} + + +/* + * ProcessAlterTableStmtAttachPartition takes AlterTableStmt object as parameter + * but it only processes into ALTER TABLE ... ATTACH PARTITION commands and + * distributes the partition if necessary. There are four cases to consider; + * + * Parent is not distributed, partition is not distributed: We do not need to + * do anything in this case. + * + * Parent is not distributed, partition is distributed: This can happen if + * user first distributes a table and tries to attach it to a non-distributed + * table. Non-distributed tables cannot have distributed partitions, thus we + * simply error out in this case. + * + * Parent is distributed, partition is not distributed: We should distribute + * the table and attach it to its parent in workers. CreateDistributedTable + * perform both of these operations. Thus, we will not propagate ALTER TABLE + * ... ATTACH PARTITION command to workers. + * + * Parent is distributed, partition is distributed: Partition is already + * distributed, we only need to attach it to its parent in workers. Attaching + * operation will be performed via propagating this ALTER TABLE ... ATTACH + * PARTITION command to workers. + * + * This function does nothing if PostgreSQL's version is less then 10 and given + * CreateStmt is not a ALTER TABLE ... ATTACH PARTITION OF command. + */ +void +ProcessAlterTableStmtAttachPartition(AlterTableStmt *alterTableStatement) +{ +#if (PG_VERSION_NUM >= 100000) + List *commandList = alterTableStatement->cmds; + ListCell *commandCell = NULL; + + foreach(commandCell, commandList) + { + AlterTableCmd *alterTableCommand = (AlterTableCmd *) lfirst(commandCell); + + if (alterTableCommand->subtype == AT_AttachPartition) + { + Oid relationId = AlterTableLookupRelation(alterTableStatement, NoLock); + PartitionCmd *partitionCommand = (PartitionCmd *) alterTableCommand->def; + bool partitionMissingOk = false; + Oid partitionRelationId = RangeVarGetRelid(partitionCommand->name, NoLock, + partitionMissingOk); + + /* + * If user first distributes the table then tries to attach it to non + * distributed table, we error out. + */ + if (!IsDistributedTable(relationId) && + IsDistributedTable(partitionRelationId)) + { + char *parentRelationName = get_rel_name(partitionRelationId); + + ereport(ERROR, (errmsg("non-distributed tables cannot have " + "distributed partitions"), + errhint("Distribute the partitioned table \"%s\" " + "instead", parentRelationName))); + } + + /* if parent of this table is distributed, distribute this table too */ + if (IsDistributedTable(relationId) && + !IsDistributedTable(partitionRelationId)) + { + Var *distributionColumn = DistPartitionKey(relationId); + char distributionMethod = DISTRIBUTE_BY_HASH; + char *parentRelationName = generate_qualified_relation_name(relationId); + bool viaDeprecatedAPI = false; + + CreateDistributedTable(partitionRelationId, distributionColumn, + distributionMethod, parentRelationName, + viaDeprecatedAPI); + } + } + } +#endif +} + + +/* + * PlanAlterTableStmt determines whether a given ALTER TABLE statement involves + * a distributed table. If so (and if the statement does not use unsupported + * options), it modifies the input statement to ensure proper execution against + * the master node table and creates a DDLJob to encapsulate information needed + * during the worker node portion of DDL execution before returning that DDLJob + * in a List. If no distributed table is involved, this function returns NIL. + */ +List * +PlanAlterTableStmt(AlterTableStmt *alterTableStatement, const char *alterTableCommand) +{ + List *ddlJobs = NIL; + DDLJob *ddlJob = NULL; + LOCKMODE lockmode = 0; + Oid leftRelationId = InvalidOid; + Oid rightRelationId = InvalidOid; + char leftRelationKind; + bool isDistributedRelation = false; + List *commandList = NIL; + ListCell *commandCell = NULL; + bool executeSequentially = false; + + /* first check whether a distributed relation is affected */ + if (alterTableStatement->relation == NULL) + { + return NIL; + } + + lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); + leftRelationId = AlterTableLookupRelation(alterTableStatement, lockmode); + if (!OidIsValid(leftRelationId)) + { + return NIL; + } + + /* + * AlterTableStmt applies also to INDEX relations, and we have support for + * SET/SET storage parameters in Citus, so we might have to check for + * another relation here. + */ + leftRelationKind = get_rel_relkind(leftRelationId); + if (leftRelationKind == RELKIND_INDEX) + { + leftRelationId = IndexGetRelation(leftRelationId, false); + } + + isDistributedRelation = IsDistributedTable(leftRelationId); + if (!isDistributedRelation) + { + return NIL; + } + + /* + * The PostgreSQL parser dispatches several commands into the node type + * AlterTableStmt, from ALTER INDEX to ALTER SEQUENCE or ALTER VIEW. Here + * we have a special implementation for ALTER INDEX, and a specific error + * message in case of unsupported sub-command. + */ + if (leftRelationKind == RELKIND_INDEX) + { + ErrorIfUnsupportedAlterIndexStmt(alterTableStatement); + } + else + { + /* this function also accepts more than just RELKIND_RELATION... */ + ErrorIfUnsupportedAlterTableStmt(alterTableStatement); + } + + /* + * We check if there is a ADD/DROP FOREIGN CONSTRAINT command in sub commands list. + * If there is we assign referenced relation id to rightRelationId and we also + * set skip_validation to true to prevent PostgreSQL to verify validity of the + * foreign constraint in master. Validity will be checked in workers anyway. + */ + commandList = alterTableStatement->cmds; + + foreach(commandCell, commandList) + { + AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); + AlterTableType alterTableType = command->subtype; + + if (alterTableType == AT_AddConstraint) + { + Constraint *constraint = (Constraint *) command->def; + if (constraint->contype == CONSTR_FOREIGN) + { + /* + * We only support ALTER TABLE ADD CONSTRAINT ... FOREIGN KEY, if it is + * only subcommand of ALTER TABLE. It was already checked in + * ErrorIfUnsupportedAlterTableStmt. + */ + Assert(list_length(commandList) <= 1); + + rightRelationId = RangeVarGetRelid(constraint->pktable, lockmode, + alterTableStatement->missing_ok); + + /* + * Foreign constraint validations will be done in workers. If we do not + * set this flag, PostgreSQL tries to do additional checking when we drop + * to standard_ProcessUtility. standard_ProcessUtility tries to open new + * connections to workers to verify foreign constraints while original + * transaction is in process, which causes deadlock. + */ + constraint->skip_validation = true; + } + } + else if (alterTableType == AT_AddColumn) + { + /* + * TODO: This code path is nothing beneficial since we do not + * support ALTER TABLE %s ADD COLUMN %s [constraint] for foreign keys. + * However, the code is kept in case we fix the constraint + * creation without a name and allow foreign key creation with the mentioned + * command. + */ + ColumnDef *columnDefinition = (ColumnDef *) command->def; + List *columnConstraints = columnDefinition->constraints; + + ListCell *columnConstraint = NULL; + foreach(columnConstraint, columnConstraints) + { + Constraint *constraint = (Constraint *) lfirst(columnConstraint); + if (constraint->contype == CONSTR_FOREIGN) + { + rightRelationId = RangeVarGetRelid(constraint->pktable, lockmode, + alterTableStatement->missing_ok); + + /* + * Foreign constraint validations will be done in workers. If we do not + * set this flag, PostgreSQL tries to do additional checking when we drop + * to standard_ProcessUtility. standard_ProcessUtility tries to open new + * connections to workers to verify foreign constraints while original + * transaction is in process, which causes deadlock. + */ + constraint->skip_validation = true; + break; + } + } + } +#if (PG_VERSION_NUM >= 100000) + else if (alterTableType == AT_AttachPartition) + { + PartitionCmd *partitionCommand = (PartitionCmd *) command->def; + + /* + * We only support ALTER TABLE ATTACH PARTITION, if it is only subcommand of + * ALTER TABLE. It was already checked in ErrorIfUnsupportedAlterTableStmt. + */ + Assert(list_length(commandList) <= 1); + + rightRelationId = RangeVarGetRelid(partitionCommand->name, NoLock, false); + + /* + * Do not generate tasks if relation is distributed and the partition + * is not distributed. Because, we'll manually convert the partition into + * distributed table and co-locate with its parent. + */ + if (!IsDistributedTable(rightRelationId)) + { + return NIL; + } + } + else if (alterTableType == AT_DetachPartition) + { + PartitionCmd *partitionCommand = (PartitionCmd *) command->def; + + /* + * We only support ALTER TABLE DETACH PARTITION, if it is only subcommand of + * ALTER TABLE. It was already checked in ErrorIfUnsupportedAlterTableStmt. + */ + Assert(list_length(commandList) <= 1); + + rightRelationId = RangeVarGetRelid(partitionCommand->name, NoLock, false); + } +#endif + executeSequentially |= SetupExecutionModeForAlterTable(leftRelationId, + command); + } + + ddlJob = palloc0(sizeof(DDLJob)); + ddlJob->targetRelationId = leftRelationId; + ddlJob->concurrentIndexCmd = false; + ddlJob->commandString = alterTableCommand; + ddlJob->executeSequentially = executeSequentially; + + if (rightRelationId) + { + if (!IsDistributedTable(rightRelationId)) + { + ddlJob->taskList = NIL; + } + else + { + /* if foreign key related, use specialized task list function ... */ + ddlJob->taskList = InterShardDDLTaskList(leftRelationId, rightRelationId, + alterTableCommand); + } + } + else + { + /* ... otherwise use standard DDL task list function */ + ddlJob->taskList = DDLTaskList(leftRelationId, alterTableCommand); + } + + ddlJobs = list_make1(ddlJob); + + return ddlJobs; +} + + +/* + * WorkerProcessAlterTableStmt checks and processes the alter table statement to be + * worked on the distributed table of the worker node. Currently, it only processes + * ALTER TABLE ... ADD FOREIGN KEY command to skip the validation step. + */ +Node * +WorkerProcessAlterTableStmt(AlterTableStmt *alterTableStatement, + const char *alterTableCommand) +{ + LOCKMODE lockmode = 0; + Oid leftRelationId = InvalidOid; + bool isDistributedRelation = false; + List *commandList = NIL; + ListCell *commandCell = NULL; + + /* first check whether a distributed relation is affected */ + if (alterTableStatement->relation == NULL) + { + return (Node *) alterTableStatement; + } + + lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); + leftRelationId = AlterTableLookupRelation(alterTableStatement, lockmode); + if (!OidIsValid(leftRelationId)) + { + return (Node *) alterTableStatement; + } + + isDistributedRelation = IsDistributedTable(leftRelationId); + if (!isDistributedRelation) + { + return (Node *) alterTableStatement; + } + + /* + * We check if there is a ADD FOREIGN CONSTRAINT command in sub commands list. + * If there is we assign referenced releation id to rightRelationId and we also + * set skip_validation to true to prevent PostgreSQL to verify validity of the + * foreign constraint in master. Validity will be checked in workers anyway. + */ + commandList = alterTableStatement->cmds; + + foreach(commandCell, commandList) + { + AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); + AlterTableType alterTableType = command->subtype; + + if (alterTableType == AT_AddConstraint) + { + Constraint *constraint = (Constraint *) command->def; + if (constraint->contype == CONSTR_FOREIGN) + { + /* foreign constraint validations will be done in shards. */ + constraint->skip_validation = true; + } + } + } + + return (Node *) alterTableStatement; +} + + +/* + * IsAlterTableRenameStmt returns whether the passed-in RenameStmt is one of + * the following forms: + * + * - ALTER TABLE RENAME + * - ALTER TABLE RENAME COLUMN + * - ALTER TABLE RENAME CONSTRAINT + */ +bool +IsAlterTableRenameStmt(RenameStmt *renameStmt) +{ + bool isAlterTableRenameStmt = false; + + if (renameStmt->renameType == OBJECT_TABLE) + { + isAlterTableRenameStmt = true; + } + else if (renameStmt->renameType == OBJECT_COLUMN && + renameStmt->relationType == OBJECT_TABLE) + { + isAlterTableRenameStmt = true; + } + else if (renameStmt->renameType == OBJECT_TABCONSTRAINT) + { + isAlterTableRenameStmt = true; + } + + return isAlterTableRenameStmt; +} + + +/* + * ErrorIfDropPartitionColumn checks if any subcommands of the given alter table + * command is a DROP COLUMN command which drops the partition column of a distributed + * table. If there is such a subcommand, this function errors out. + */ +void +ErrorIfAlterDropsPartitionColumn(AlterTableStmt *alterTableStatement) +{ + LOCKMODE lockmode = 0; + Oid leftRelationId = InvalidOid; + bool isDistributedRelation = false; + List *commandList = alterTableStatement->cmds; + ListCell *commandCell = NULL; + + /* first check whether a distributed relation is affected */ + if (alterTableStatement->relation == NULL) + { + return; + } + + lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); + leftRelationId = AlterTableLookupRelation(alterTableStatement, lockmode); + if (!OidIsValid(leftRelationId)) + { + return; + } + + isDistributedRelation = IsDistributedTable(leftRelationId); + if (!isDistributedRelation) + { + return; + } + + /* then check if any of subcommands drop partition column.*/ + foreach(commandCell, commandList) + { + AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); + AlterTableType alterTableType = command->subtype; + if (alterTableType == AT_DropColumn) + { + if (AlterInvolvesPartitionColumn(alterTableStatement, command)) + { + ereport(ERROR, (errmsg("cannot execute ALTER TABLE command " + "dropping partition column"))); + } + } + } +} + + +/* + * PostProcessAlterTableStmt runs after the ALTER TABLE command has already run on the + * master, so we are checking constraints over the table with constraints already defined + * (to make the constraint check process same for ALTER TABLE and CREATE TABLE). If + * constraints do not fulfill the rules we defined, they will be removed and the table + * will return back to the state before the ALTER TABLE command. + */ +void +PostProcessAlterTableStmt(AlterTableStmt *alterTableStatement) +{ + List *commandList = alterTableStatement->cmds; + ListCell *commandCell = NULL; + + foreach(commandCell, commandList) + { + AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); + AlterTableType alterTableType = command->subtype; + + if (alterTableType == AT_AddConstraint) + { + LOCKMODE lockmode = NoLock; + Oid relationId = InvalidOid; + Constraint *constraint = NULL; + + Assert(list_length(commandList) == 1); + + ErrorIfUnsupportedAlterAddConstraintStmt(alterTableStatement); + + lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); + relationId = AlterTableLookupRelation(alterTableStatement, lockmode); + + if (!OidIsValid(relationId)) + { + continue; + } + + constraint = (Constraint *) command->def; + if (constraint->contype == CONSTR_FOREIGN) + { + InvalidateForeignKeyGraph(); + } + } + else if (alterTableType == AT_AddColumn) + { + List *columnConstraints = NIL; + ListCell *columnConstraint = NULL; + Oid relationId = InvalidOid; + LOCKMODE lockmode = NoLock; + + ColumnDef *columnDefinition = (ColumnDef *) command->def; + columnConstraints = columnDefinition->constraints; + if (columnConstraints) + { + ErrorIfUnsupportedAlterAddConstraintStmt(alterTableStatement); + } + + lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); + relationId = AlterTableLookupRelation(alterTableStatement, lockmode); + if (!OidIsValid(relationId)) + { + continue; + } + + foreach(columnConstraint, columnConstraints) + { + Constraint *constraint = (Constraint *) lfirst(columnConstraint); + + if (constraint->conname == NULL && + (constraint->contype == CONSTR_PRIMARY || + constraint->contype == CONSTR_UNIQUE || + constraint->contype == CONSTR_FOREIGN || + constraint->contype == CONSTR_CHECK)) + { + ErrorUnsupportedAlterTableAddColumn(relationId, command, + constraint); + } + } + } + } +} + + +void +ErrorUnsupportedAlterTableAddColumn(Oid relationId, AlterTableCmd *command, + Constraint *constraint) +{ + ColumnDef *columnDefinition = (ColumnDef *) command->def; + char *colName = columnDefinition->colname; + char *errMsg = + "cannot execute ADD COLUMN command with PRIMARY KEY, UNIQUE, FOREIGN and CHECK constraints"; + StringInfo errHint = makeStringInfo(); + appendStringInfo(errHint, "You can issue each command separately such as "); + appendStringInfo(errHint, + "ALTER TABLE %s ADD COLUMN %s data_type; ALTER TABLE %s ADD CONSTRAINT constraint_name ", + get_rel_name(relationId), + colName, get_rel_name(relationId)); + + if (constraint->contype == CONSTR_UNIQUE) + { + appendStringInfo(errHint, "UNIQUE (%s)", colName); + } + else if (constraint->contype == CONSTR_PRIMARY) + { + appendStringInfo(errHint, "PRIMARY KEY (%s)", colName); + } + else if (constraint->contype == CONSTR_CHECK) + { + appendStringInfo(errHint, "CHECK (check_expression)"); + } + else if (constraint->contype == CONSTR_FOREIGN) + { + RangeVar *referencedTable = constraint->pktable; + char *referencedColumn = strVal(lfirst(list_head(constraint->pk_attrs))); + Oid referencedRelationId = RangeVarGetRelid(referencedTable, NoLock, false); + + appendStringInfo(errHint, "FOREIGN KEY (%s) REFERENCES %s(%s)", colName, + get_rel_name(referencedRelationId), referencedColumn); + + if (constraint->fk_del_action == FKCONSTR_ACTION_SETNULL) + { + appendStringInfo(errHint, " %s", "ON DELETE SET NULL"); + } + else if (constraint->fk_del_action == FKCONSTR_ACTION_CASCADE) + { + appendStringInfo(errHint, " %s", "ON DELETE CASCADE"); + } + else if (constraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT) + { + appendStringInfo(errHint, " %s", "ON DELETE SET DEFAULT"); + } + else if (constraint->fk_del_action == FKCONSTR_ACTION_RESTRICT) + { + appendStringInfo(errHint, " %s", "ON DELETE RESTRICT"); + } + + if (constraint->fk_upd_action == FKCONSTR_ACTION_SETNULL) + { + appendStringInfo(errHint, " %s", "ON UPDATE SET NULL"); + } + else if (constraint->fk_upd_action == FKCONSTR_ACTION_CASCADE) + { + appendStringInfo(errHint, " %s", "ON UPDATE CASCADE"); + } + else if (constraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT) + { + appendStringInfo(errHint, " %s", "ON UPDATE SET DEFAULT"); + } + else if (constraint->fk_upd_action == FKCONSTR_ACTION_RESTRICT) + { + appendStringInfo(errHint, " %s", "ON UPDATE RESTRICT"); + } + } + + appendStringInfo(errHint, "%s", ";"); + + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("%s", errMsg), + errhint("%s", errHint->data), + errdetail("Adding a column with a constraint in " + "one command is not supported because " + "all constraints in Citus must have " + "explicit names"))); +} + + +/*ErrorIfUnsupportedConstraint + * run checks related to unique index / exclude + * constraints. + * + * The function skips the uniqeness checks for reference tables (i.e., distribution + * method is 'none'). + * + * Forbid UNIQUE, PRIMARY KEY, or EXCLUDE constraints on append partitioned + * tables, since currently there is no way of enforcing uniqueness for + * overlapping shards. + * + * Similarly, do not allow such constraints if they do not include partition + * column. This check is important for two reasons: + * i. First, currently Citus does not enforce uniqueness constraint on multiple + * shards. + * ii. Second, INSERT INTO .. ON CONFLICT (i.e., UPSERT) queries can be executed + * with no further check for constraints. + */ +void +ErrorIfUnsupportedConstraint(Relation relation, char distributionMethod, + Var *distributionColumn, uint32 colocationId) +{ + char *relationName = NULL; + List *indexOidList = NULL; + ListCell *indexOidCell = NULL; + + /* + * We first perform check for foreign constraints. It is important to do this check + * before next check, because other types of constraints are allowed on reference + * tables and we return early for those constraints thanks to next check. Therefore, + * for reference tables, we first check for foreing constraints and if they are OK, + * we do not error out for other types of constraints. + */ + ErrorIfUnsupportedForeignConstraint(relation, distributionMethod, distributionColumn, + colocationId); + + /* + * Citus supports any kind of uniqueness constraints for reference tables + * given that they only consist of a single shard and we can simply rely on + * Postgres. + */ + if (distributionMethod == DISTRIBUTE_BY_NONE) + { + return; + } + + relationName = RelationGetRelationName(relation); + indexOidList = RelationGetIndexList(relation); + + foreach(indexOidCell, indexOidList) + { + Oid indexOid = lfirst_oid(indexOidCell); + Relation indexDesc = index_open(indexOid, RowExclusiveLock); + IndexInfo *indexInfo = NULL; + AttrNumber *attributeNumberArray = NULL; + bool hasDistributionColumn = false; + int attributeCount = 0; + int attributeIndex = 0; + + /* extract index key information from the index's pg_index info */ + indexInfo = BuildIndexInfo(indexDesc); + + /* only check unique indexes and exclusion constraints. */ + if (indexInfo->ii_Unique == false && indexInfo->ii_ExclusionOps == NULL) + { + index_close(indexDesc, NoLock); + continue; + } + + /* + * Citus cannot enforce uniqueness/exclusion constraints with overlapping shards. + * Thus, emit a warning for unique indexes and exclusion constraints on + * append partitioned tables. + */ + if (distributionMethod == DISTRIBUTE_BY_APPEND) + { + ereport(WARNING, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("table \"%s\" has a UNIQUE or EXCLUDE constraint", + relationName), + errdetail("UNIQUE constraints, EXCLUDE constraints, " + "and PRIMARY KEYs on " + "append-partitioned tables cannot be enforced."), + errhint("Consider using hash partitioning."))); + } + + attributeCount = indexInfo->ii_NumIndexAttrs; + attributeNumberArray = IndexInfoAttributeNumberArray(indexInfo); + + for (attributeIndex = 0; attributeIndex < attributeCount; attributeIndex++) + { + AttrNumber attributeNumber = attributeNumberArray[attributeIndex]; + bool uniqueConstraint = false; + bool exclusionConstraintWithEquality = false; + + if (distributionColumn->varattno != attributeNumber) + { + continue; + } + + uniqueConstraint = indexInfo->ii_Unique; + exclusionConstraintWithEquality = (indexInfo->ii_ExclusionOps != NULL && + OperatorImplementsEquality( + indexInfo->ii_ExclusionOps[ + attributeIndex])); + + if (uniqueConstraint || exclusionConstraintWithEquality) + { + hasDistributionColumn = true; + break; + } + } + + if (!hasDistributionColumn) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot create constraint on \"%s\"", + relationName), + errdetail("Distributed relations cannot have UNIQUE, " + "EXCLUDE, or PRIMARY KEY constraints that do not " + "include the partition column (with an equality " + "operator if EXCLUDE)."))); + } + + index_close(indexDesc, NoLock); + } +} + + +/* + * ErrorIfUnsupportedAlterTableStmt checks if the corresponding alter table + * statement is supported for distributed tables and errors out if it is not. + * Currently, only the following commands are supported. + * + * ALTER TABLE ADD|DROP COLUMN + * ALTER TABLE ALTER COLUMN SET DATA TYPE + * ALTER TABLE SET|DROP NOT NULL + * ALTER TABLE SET|DROP DEFAULT + * ALTER TABLE ADD|DROP CONSTRAINT + * ALTER TABLE REPLICA IDENTITY + * ALTER TABLE SET () + * ALTER TABLE RESET () + */ +static void +ErrorIfUnsupportedAlterTableStmt(AlterTableStmt *alterTableStatement) +{ + List *commandList = alterTableStatement->cmds; + ListCell *commandCell = NULL; + + /* error out if any of the subcommands are unsupported */ + foreach(commandCell, commandList) + { + AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); + AlterTableType alterTableType = command->subtype; + + switch (alterTableType) + { + case AT_AddColumn: + { + if (IsA(command->def, ColumnDef)) + { + ColumnDef *column = (ColumnDef *) command->def; + + /* + * Check for SERIAL pseudo-types. The structure of this + * check is copied from transformColumnDefinition. + */ + if (column->typeName && list_length(column->typeName->names) == 1 && + !column->typeName->pct_type) + { + char *typeName = strVal(linitial(column->typeName->names)); + + if (strcmp(typeName, "smallserial") == 0 || + strcmp(typeName, "serial2") == 0 || + strcmp(typeName, "serial") == 0 || + strcmp(typeName, "serial4") == 0 || + strcmp(typeName, "bigserial") == 0 || + strcmp(typeName, "serial8") == 0) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot execute ADD COLUMN commands " + "involving serial pseudotypes"))); + } + } + } + + break; + } + + case AT_DropColumn: + case AT_ColumnDefault: + case AT_AlterColumnType: + case AT_DropNotNull: + { + if (AlterInvolvesPartitionColumn(alterTableStatement, command)) + { + ereport(ERROR, (errmsg("cannot execute ALTER TABLE command " + "involving partition column"))); + } + break; + } + + case AT_AddConstraint: + { + Constraint *constraint = (Constraint *) command->def; + + /* we only allow constraints if they are only subcommand */ + if (commandList->length > 1) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot execute ADD CONSTRAINT command with " + "other subcommands"), + errhint("You can issue each subcommand separately"))); + } + + /* + * We will use constraint name in each placement by extending it at + * workers. Therefore we require it to be exist. + */ + if (constraint->conname == NULL) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot create constraint without a name on a " + "distributed table"))); + } + + break; + } + +#if (PG_VERSION_NUM >= 100000) + case AT_AttachPartition: + { + Oid relationId = AlterTableLookupRelation(alterTableStatement, + NoLock); + PartitionCmd *partitionCommand = (PartitionCmd *) command->def; + bool missingOK = false; + Oid partitionRelationId = RangeVarGetRelid(partitionCommand->name, + NoLock, missingOK); + + /* we only allow partitioning commands if they are only subcommand */ + if (commandList->length > 1) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot execute ATTACH PARTITION command " + "with other subcommands"), + errhint("You can issue each subcommand " + "separately."))); + } + + if (IsDistributedTable(partitionRelationId) && + !TablesColocated(relationId, partitionRelationId)) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("distributed tables cannot have " + "non-colocated distributed tables as a " + "partition "))); + } + + break; + } + + case AT_DetachPartition: + { + /* we only allow partitioning commands if they are only subcommand */ + if (commandList->length > 1) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("cannot execute DETACH PARTITION command " + "with other subcommands"), + errhint("You can issue each subcommand " + "separately."))); + } + + break; + } + +#endif + case AT_DropConstraint: + { + LOCKMODE lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); + Oid relationId = AlterTableLookupRelation(alterTableStatement, lockmode); + + if (!OidIsValid(relationId)) + { + return; + } + + if (ConstraintIsAForeignKey(command->name, relationId)) + { + MarkInvalidateForeignKeyGraph(); + } + + break; + } + + case AT_SetNotNull: + case AT_EnableTrigAll: + case AT_DisableTrigAll: + case AT_ReplicaIdentity: + { + /* + * We will not perform any special check for ALTER TABLE DROP CONSTRAINT + * , ALTER TABLE .. ALTER COLUMN .. SET NOT NULL and ALTER TABLE ENABLE/ + * DISABLE TRIGGER ALL, ALTER TABLE .. REPLICA IDENTITY .. + */ + break; + } + + case AT_SetRelOptions: /* SET (...) */ + case AT_ResetRelOptions: /* RESET (...) */ + case AT_ReplaceRelOptions: /* replace entire option list */ + { + /* this command is supported by Citus */ + break; + } + + default: + { + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("alter table command is currently unsupported"), + errdetail("Only ADD|DROP COLUMN, SET|DROP NOT NULL, " + "SET|DROP DEFAULT, ADD|DROP CONSTRAINT, " + "SET (), RESET (), " + "ATTACH|DETACH PARTITION and TYPE subcommands " + "are supported."))); + } + } + } +} + + +/* + * SetupExecutionModeForAlterTable is the function that is responsible + * for two things for practial purpose for not doing the same checks + * twice: + * (a) For any command, decide and return whether we should + * run the command in sequntial mode + * (b) For commands in a transaction block, set the transaction local + * multi-shard modify mode to sequential when necessary + * + * The commands that operate on the same reference table shard in parallel + * is in the interest of (a), where the return value indicates the executor + * to run the command sequentially to prevent self-deadlocks. + * + * The commands that both operate on the same reference table shard in parallel + * and cascades to run any parallel operation is in the interest of (b). By + * setting the multi-shard mode, we ensure that the cascading parallel commands + * are executed sequentially to prevent self-deadlocks. + * + * One final note on the function is that if the function decides to execute + * the command in sequential mode, and a parallel command has already been + * executed in the same transaction, the function errors out. See the comment + * in the function for the rationale. + */ +static bool +SetupExecutionModeForAlterTable(Oid relationId, AlterTableCmd *command) +{ + bool executeSequentially = false; + AlterTableType alterTableType = command->subtype; + if (alterTableType == AT_DropConstraint) + { + char *constraintName = command->name; + if (ConstraintIsAForeignKeyToReferenceTable(constraintName, relationId)) + { + executeSequentially = true; + } + } + else if (alterTableType == AT_AddColumn) + { + /* + * TODO: This code path will never be executed since we do not + * support foreign constraint creation via + * ALTER TABLE %s ADD COLUMN %s [constraint]. However, the code + * is kept in case we fix the constraint creation without a name + * and allow foreign key creation with the mentioned command. + */ + ColumnDef *columnDefinition = (ColumnDef *) command->def; + List *columnConstraints = columnDefinition->constraints; + + ListCell *columnConstraint = NULL; + foreach(columnConstraint, columnConstraints) + { + Constraint *constraint = (Constraint *) lfirst(columnConstraint); + if (constraint->contype == CONSTR_FOREIGN) + { + Oid rightRelationId = RangeVarGetRelid(constraint->pktable, NoLock, + false); + if (IsDistributedTable(rightRelationId) && + PartitionMethod(rightRelationId) == DISTRIBUTE_BY_NONE) + { + executeSequentially = true; + } + } + } + } + else if (alterTableType == AT_DropColumn || alterTableType == AT_AlterColumnType) + { + char *affectedColumnName = command->name; + + if (ColumnAppearsInForeignKeyToReferenceTable(affectedColumnName, + relationId)) + { + if (IsTransactionBlock() && alterTableType == AT_AlterColumnType) + { + SetLocalMultiShardModifyModeToSequential(); + } + + executeSequentially = true; + } + } + else if (alterTableType == AT_AddConstraint) + { + /* + * We need to execute the ddls working with reference tables on the + * right side sequentially, because parallel ddl operations + * relating to one and only shard of a reference table on a worker + * may cause self-deadlocks. + */ + Constraint *constraint = (Constraint *) command->def; + if (constraint->contype == CONSTR_FOREIGN) + { + Oid rightRelationId = RangeVarGetRelid(constraint->pktable, NoLock, + false); + if (IsDistributedTable(rightRelationId) && + PartitionMethod(rightRelationId) == DISTRIBUTE_BY_NONE) + { + executeSequentially = true; + } + } + } + + /* + * If there has already been a parallel query executed, the sequential mode + * would still use the already opened parallel connections to the workers for + * the distributed tables, thus contradicting our purpose of using + * sequential mode. + */ + if (executeSequentially && IsDistributedTable(relationId) && + PartitionMethod(relationId) != DISTRIBUTE_BY_NONE && + ParallelQueryExecutedInTransaction()) + { + char *relationName = get_rel_name(relationId); + + ereport(ERROR, (errmsg("cannot modify table \"%s\" because there " + "was a parallel operation on a distributed table " + "in the transaction", relationName), + errdetail("When there is a foreign key to a reference " + "table, Citus needs to perform all operations " + "over a single connection per node to ensure " + "consistency."), + errhint("Try re-running the transaction with " + "\"SET LOCAL citus.multi_shard_modify_mode TO " + "\'sequential\';\""))); + } + + return executeSequentially; +} + + +/* + * InterShardDDLTaskList builds a list of tasks to execute a inter shard DDL command on a + * shards of given list of distributed table. At the moment this function is used to run + * foreign key and partitioning command on worker node. + * + * leftRelationId is the relation id of actual distributed table which given command is + * applied. rightRelationId is the relation id of distributed table which given command + * refers to. + */ +static List * +InterShardDDLTaskList(Oid leftRelationId, Oid rightRelationId, + const char *commandString) +{ + List *taskList = NIL; + + List *leftShardList = LoadShardIntervalList(leftRelationId); + ListCell *leftShardCell = NULL; + Oid leftSchemaId = get_rel_namespace(leftRelationId); + char *leftSchemaName = get_namespace_name(leftSchemaId); + char *escapedLeftSchemaName = quote_literal_cstr(leftSchemaName); + + char rightPartitionMethod = PartitionMethod(rightRelationId); + List *rightShardList = LoadShardIntervalList(rightRelationId); + ListCell *rightShardCell = NULL; + Oid rightSchemaId = get_rel_namespace(rightRelationId); + char *rightSchemaName = get_namespace_name(rightSchemaId); + char *escapedRightSchemaName = quote_literal_cstr(rightSchemaName); + + char *escapedCommandString = quote_literal_cstr(commandString); + uint64 jobId = INVALID_JOB_ID; + int taskId = 1; + + /* + * If the rightPartitionMethod is a reference table, we need to make sure + * that the tasks are created in a way that the right shard stays the same + * since we only have one placement per worker. This hack is first implemented + * for foreign constraint support from distributed tables to reference tables. + */ + if (rightPartitionMethod == DISTRIBUTE_BY_NONE) + { + ShardInterval *rightShardInterval = NULL; + int rightShardCount = list_length(rightShardList); + int leftShardCount = list_length(leftShardList); + int shardCounter = 0; + + Assert(rightShardCount == 1); + + rightShardInterval = (ShardInterval *) linitial(rightShardList); + for (shardCounter = rightShardCount; shardCounter < leftShardCount; + shardCounter++) + { + rightShardList = lappend(rightShardList, rightShardInterval); + } + } + + /* lock metadata before getting placement lists */ + LockShardListMetadata(leftShardList, ShareLock); + + forboth(leftShardCell, leftShardList, rightShardCell, rightShardList) + { + ShardInterval *leftShardInterval = (ShardInterval *) lfirst(leftShardCell); + uint64 leftShardId = leftShardInterval->shardId; + StringInfo applyCommand = makeStringInfo(); + Task *task = NULL; + RelationShard *leftRelationShard = CitusMakeNode(RelationShard); + RelationShard *rightRelationShard = CitusMakeNode(RelationShard); + + ShardInterval *rightShardInterval = (ShardInterval *) lfirst(rightShardCell); + uint64 rightShardId = rightShardInterval->shardId; + + leftRelationShard->relationId = leftRelationId; + leftRelationShard->shardId = leftShardId; + + rightRelationShard->relationId = rightRelationId; + rightRelationShard->shardId = rightShardId; + + appendStringInfo(applyCommand, WORKER_APPLY_INTER_SHARD_DDL_COMMAND, + leftShardId, escapedLeftSchemaName, rightShardId, + escapedRightSchemaName, escapedCommandString); + + task = CitusMakeNode(Task); + task->jobId = jobId; + task->taskId = taskId++; + task->taskType = DDL_TASK; + task->queryString = applyCommand->data; + task->dependedTaskList = NULL; + task->replicationModel = REPLICATION_MODEL_INVALID; + task->anchorShardId = leftShardId; + task->taskPlacementList = FinalizedShardPlacementList(leftShardId); + task->relationShardList = list_make2(leftRelationShard, rightRelationShard); + + taskList = lappend(taskList, task); + } + + return taskList; +} + + +/* + * AlterInvolvesPartitionColumn checks if the given alter table command + * involves relation's partition column. + */ +static bool +AlterInvolvesPartitionColumn(AlterTableStmt *alterTableStatement, + AlterTableCmd *command) +{ + bool involvesPartitionColumn = false; + Var *partitionColumn = NULL; + HeapTuple tuple = NULL; + char *alterColumnName = command->name; + + LOCKMODE lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); + Oid relationId = AlterTableLookupRelation(alterTableStatement, lockmode); + if (!OidIsValid(relationId)) + { + return false; + } + + partitionColumn = DistPartitionKey(relationId); + + tuple = SearchSysCacheAttName(relationId, alterColumnName); + if (HeapTupleIsValid(tuple)) + { + Form_pg_attribute targetAttr = (Form_pg_attribute) GETSTRUCT(tuple); + + /* reference tables do not have partition column, so allow them */ + if (partitionColumn != NULL && + targetAttr->attnum == partitionColumn->varattno) + { + involvesPartitionColumn = true; + } + + ReleaseSysCache(tuple); + } + + return involvesPartitionColumn; +} + + +/* + * ErrorIfUnsopprtedAlterAddConstraintStmt runs the constraint checks on distributed + * table using the same logic with create_distributed_table. + */ +static void +ErrorIfUnsupportedAlterAddConstraintStmt(AlterTableStmt *alterTableStatement) +{ + LOCKMODE lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); + Oid relationId = AlterTableLookupRelation(alterTableStatement, lockmode); + char distributionMethod = PartitionMethod(relationId); + Var *distributionColumn = DistPartitionKey(relationId); + uint32 colocationId = TableColocationId(relationId); + Relation relation = relation_open(relationId, ExclusiveLock); + + ErrorIfUnsupportedConstraint(relation, distributionMethod, distributionColumn, + colocationId); + relation_close(relation, NoLock); +} diff --git a/src/backend/distributed/commands/transmit.c b/src/backend/distributed/commands/transmit.c index c5dcb667b83..95c90bba9da 100644 --- a/src/backend/distributed/commands/transmit.c +++ b/src/backend/distributed/commands/transmit.c @@ -15,6 +15,7 @@ #include #include +#include "commands/defrem.h" #include "distributed/relay_utility.h" #include "distributed/transmit.h" #include "distributed/worker_protocol.h" @@ -330,3 +331,83 @@ ReceiveCopyData(StringInfo copyData) return copyDone; } + + +/* Is the passed in statement a transmit statement? */ +bool +IsTransmitStmt(Node *parsetree) +{ + if (IsA(parsetree, CopyStmt)) + { + CopyStmt *copyStatement = (CopyStmt *) parsetree; + ListCell *optionCell = NULL; + + /* Extract options from the statement node tree */ + foreach(optionCell, copyStatement->options) + { + DefElem *defel = (DefElem *) lfirst(optionCell); + + if (strncmp(defel->defname, "format", NAMEDATALEN) == 0 && + strncmp(defGetString(defel), "transmit", NAMEDATALEN) == 0) + { + return true; + } + } + } + + return false; +} + + +/* + * VerifyTransmitStmt checks that the passed in command is a valid transmit + * statement. Raise ERROR if not. + * + * Note that only 'toplevel' options in the CopyStmt struct are checked, and + * that verification of the target files existance is not done here. + */ +void +VerifyTransmitStmt(CopyStmt *copyStatement) +{ + char *fileName = NULL; + + EnsureSuperUser(); + + /* do some minimal option verification */ + if (copyStatement->relation == NULL || + copyStatement->relation->relname == NULL) + { + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("FORMAT 'transmit' requires a target file"))); + } + + fileName = copyStatement->relation->relname; + + if (is_absolute_path(fileName)) + { + ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + (errmsg("absolute path not allowed")))); + } + else if (!path_is_relative_and_below_cwd(fileName)) + { + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + (errmsg("path must be in or below the current directory")))); + } + + if (copyStatement->filename != NULL) + { + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("FORMAT 'transmit' only accepts STDIN/STDOUT" + " as input/output"))); + } + + if (copyStatement->query != NULL || + copyStatement->attlist != NULL || + copyStatement->is_program) + { + ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("FORMAT 'transmit' does not accept query, attribute list" + " or PROGRAM parameters "))); + } +} diff --git a/src/backend/distributed/commands/truncate.c b/src/backend/distributed/commands/truncate.c new file mode 100644 index 00000000000..558688598c3 --- /dev/null +++ b/src/backend/distributed/commands/truncate.c @@ -0,0 +1,287 @@ +/*------------------------------------------------------------------------- + * + * truncate.c + * Commands for truncating distributed tables. + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" + +#include "catalog/namespace.h" +#include "catalog/pg_class.h" +#include "distributed/citus_ruleutils.h" +#include "distributed/commands.h" +#include "distributed/distributed_planner.h" +#include "distributed/listutils.h" +#include "distributed/metadata_cache.h" +#include "distributed/metadata_sync.h" +#include "distributed/multi_executor.h" +#include "distributed/multi_join_order.h" +#include "distributed/reference_table_utils.h" +#include "distributed/resource_lock.h" +#include "distributed/transaction_management.h" +#include "distributed/worker_transaction.h" +#include "storage/lmgr.h" +#include "utils/lsyscache.h" +#include "utils/rel.h" + + +#define LOCK_RELATION_IF_EXISTS "SELECT lock_relation_if_exists('%s', '%s');" + + +/* Local functions forward declarations for unsupported command checks */ +static void ErrorIfUnsupportedTruncateStmt(TruncateStmt *truncateStatement); +static void ExecuteTruncateStmtSequentialIfNecessary(TruncateStmt *command); +static void EnsurePartitionTableNotReplicatedForTruncate(TruncateStmt *truncateStatement); +static void LockTruncatedRelationMetadataInWorkers(TruncateStmt *truncateStatement); +static void AcquireDistributedLockOnRelations(List *relationIdList, LOCKMODE lockMode); + + +/* + * ProcessTruncateStatement handles few things that should be + * done before standard process utility is called for truncate + * command. + */ +void +ProcessTruncateStatement(TruncateStmt *truncateStatement) +{ + ErrorIfUnsupportedTruncateStmt(truncateStatement); + EnsurePartitionTableNotReplicatedForTruncate(truncateStatement); + ExecuteTruncateStmtSequentialIfNecessary(truncateStatement); + LockTruncatedRelationMetadataInWorkers(truncateStatement); +} + + +/* + * ErrorIfUnsupportedTruncateStmt errors out if the command attempts to + * truncate a distributed foreign table. + */ +static void +ErrorIfUnsupportedTruncateStmt(TruncateStmt *truncateStatement) +{ + List *relationList = truncateStatement->relations; + ListCell *relationCell = NULL; + foreach(relationCell, relationList) + { + RangeVar *rangeVar = (RangeVar *) lfirst(relationCell); + Oid relationId = RangeVarGetRelid(rangeVar, NoLock, true); + char relationKind = get_rel_relkind(relationId); + if (IsDistributedTable(relationId) && + relationKind == RELKIND_FOREIGN_TABLE) + { + ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("truncating distributed foreign tables is " + "currently unsupported"), + errhint("Use master_drop_all_shards to remove " + "foreign table's shards."))); + } + } +} + + +/* + * EnsurePartitionTableNotReplicatedForTruncate a simple wrapper around + * EnsurePartitionTableNotReplicated for TRUNCATE command. + */ +static void +EnsurePartitionTableNotReplicatedForTruncate(TruncateStmt *truncateStatement) +{ + ListCell *relationCell = NULL; + + foreach(relationCell, truncateStatement->relations) + { + RangeVar *relationRV = (RangeVar *) lfirst(relationCell); + Relation relation = heap_openrv(relationRV, NoLock); + Oid relationId = RelationGetRelid(relation); + + if (!IsDistributedTable(relationId)) + { + heap_close(relation, NoLock); + continue; + } + + EnsurePartitionTableNotReplicated(relationId); + + heap_close(relation, NoLock); + } +} + + +/* + * ExecuteTruncateStmtSequentialIfNecessary decides if the TRUNCATE stmt needs + * to run sequential. If so, it calls SetLocalMultiShardModifyModeToSequential(). + * + * If a reference table which has a foreign key from a distributed table is truncated + * we need to execute the command sequentially to avoid self-deadlock. + */ +static void +ExecuteTruncateStmtSequentialIfNecessary(TruncateStmt *command) +{ + List *relationList = command->relations; + ListCell *relationCell = NULL; + bool failOK = false; + + foreach(relationCell, relationList) + { + RangeVar *rangeVar = (RangeVar *) lfirst(relationCell); + Oid relationId = RangeVarGetRelid(rangeVar, NoLock, failOK); + + if (IsDistributedTable(relationId) && + PartitionMethod(relationId) == DISTRIBUTE_BY_NONE && + TableReferenced(relationId)) + { + char *relationName = get_rel_name(relationId); + + ereport(DEBUG1, (errmsg("switching to sequential query execution mode"), + errdetail( + "Reference relation \"%s\" is modified, which might lead " + "to data inconsistencies or distributed deadlocks via " + "parallel accesses to hash distributed relations due to " + "foreign keys. Any parallel modification to " + "those hash distributed relations in the same " + "transaction can only be executed in sequential query " + "execution mode", relationName))); + + SetLocalMultiShardModifyModeToSequential(); + + /* nothing to do more */ + return; + } + } +} + + +/* + * LockTruncatedRelationMetadataInWorkers determines if distributed + * lock is necessary for truncated relations, and acquire locks. + * + * LockTruncatedRelationMetadataInWorkers handles distributed locking + * of truncated tables before standard utility takes over. + * + * Actual distributed truncation occurs inside truncate trigger. + * + * This is only for distributed serialization of truncate commands. + * The function assumes that there is no foreign key relation between + * non-distributed and distributed relations. + */ +static void +LockTruncatedRelationMetadataInWorkers(TruncateStmt *truncateStatement) +{ + List *distributedRelationList = NIL; + ListCell *relationCell = NULL; + + /* nothing to do if there is no metadata at worker nodes */ + if (!ClusterHasKnownMetadataWorkers()) + { + return; + } + + foreach(relationCell, truncateStatement->relations) + { + RangeVar *relationRV = (RangeVar *) lfirst(relationCell); + Relation relation = heap_openrv(relationRV, NoLock); + Oid relationId = RelationGetRelid(relation); + DistTableCacheEntry *cacheEntry = NULL; + List *referencingTableList = NIL; + ListCell *referencingTableCell = NULL; + + if (!IsDistributedTable(relationId)) + { + heap_close(relation, NoLock); + continue; + } + + if (list_member_oid(distributedRelationList, relationId)) + { + heap_close(relation, NoLock); + continue; + } + + distributedRelationList = lappend_oid(distributedRelationList, relationId); + + cacheEntry = DistributedTableCacheEntry(relationId); + Assert(cacheEntry != NULL); + + referencingTableList = cacheEntry->referencingRelationsViaForeignKey; + foreach(referencingTableCell, referencingTableList) + { + Oid referencingRelationId = lfirst_oid(referencingTableCell); + distributedRelationList = list_append_unique_oid(distributedRelationList, + referencingRelationId); + } + + heap_close(relation, NoLock); + } + + if (distributedRelationList != NIL) + { + AcquireDistributedLockOnRelations(distributedRelationList, AccessExclusiveLock); + } +} + + +/* + * AcquireDistributedLockOnRelations acquire a distributed lock on worker nodes + * for given list of relations ids. Relation id list and worker node list + * sorted so that the lock is acquired in the same order regardless of which + * node it was run on. Notice that no lock is acquired on coordinator node. + * + * Notice that the locking functions is sent to all workers regardless of if + * it has metadata or not. This is because a worker node only knows itself + * and previous workers that has metadata sync turned on. The node does not + * know about other nodes that have metadata sync turned on afterwards. + */ +static void +AcquireDistributedLockOnRelations(List *relationIdList, LOCKMODE lockMode) +{ + ListCell *relationIdCell = NULL; + List *workerNodeList = ActivePrimaryNodeList(); + const char *lockModeText = LockModeToLockModeText(lockMode); + + /* + * We want to acquire locks in the same order accross the nodes. + * Although relation ids may change, their ordering will not. + */ + relationIdList = SortList(relationIdList, CompareOids); + workerNodeList = SortList(workerNodeList, CompareWorkerNodes); + + BeginOrContinueCoordinatedTransaction(); + + foreach(relationIdCell, relationIdList) + { + Oid relationId = lfirst_oid(relationIdCell); + + /* + * We only acquire distributed lock on relation if + * the relation is sync'ed between mx nodes. + */ + if (ShouldSyncTableMetadata(relationId)) + { + char *qualifiedRelationName = generate_qualified_relation_name(relationId); + StringInfo lockRelationCommand = makeStringInfo(); + ListCell *workerNodeCell = NULL; + + appendStringInfo(lockRelationCommand, LOCK_RELATION_IF_EXISTS, + qualifiedRelationName, lockModeText); + + foreach(workerNodeCell, workerNodeList) + { + WorkerNode *workerNode = (WorkerNode *) lfirst(workerNodeCell); + char *nodeName = workerNode->workerName; + int nodePort = workerNode->workerPort; + + /* if local node is one of the targets, acquire the lock locally */ + if (workerNode->groupId == GetLocalGroupId()) + { + LockRelationOid(relationId, lockMode); + continue; + } + + SendCommandToWorker(nodeName, nodePort, lockRelationCommand->data); + } + } + } +} diff --git a/src/backend/distributed/commands/utility_hook.c b/src/backend/distributed/commands/utility_hook.c new file mode 100644 index 00000000000..b0d52383a98 --- /dev/null +++ b/src/backend/distributed/commands/utility_hook.c @@ -0,0 +1,801 @@ +/*------------------------------------------------------------------------- + * utility_hook.c + * Citus utility hook and related functionality. + * + * The utility hook is called by PostgreSQL when processing any command + * that is not SELECT, UPDATE, DELETE, INSERT, in place of the regular + * ProcessUtility function. We use this primarily to implement (or in + * some cases prevent) DDL commands and COPY on distributed tables. + * + * For DDL commands that affect distributed tables, we check whether + * they are valid (and implemented) for the distributed table and then + * propagate the command to all shards and, in case of MX, to distributed + * tables on other nodes. We still call the original ProcessUtility + * function to apply catalog changes on the coordinator. + * + * For COPY into a distributed table, we provide an alternative + * implementation in ProcessCopyStmt that sends rows to shards based + * on their distribution column value instead of writing it to the local + * table on the coordinator. For COPY from a distributed table, we + * replace the table with a SELECT * FROM table and pass it back to + * ProcessUtility, which will plan the query via the distributed planner + * hook. + * + * Copyright (c) 2012-2018, Citus Data, Inc. + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "miscadmin.h" + +#include "access/attnum.h" +#include "access/heapam.h" +#include "access/htup_details.h" +#include "catalog/catalog.h" +#include "catalog/dependency.h" +#include "commands/dbcommands.h" +#include "commands/tablecmds.h" +#include "distributed/colocation_utils.h" +#include "distributed/commands.h" +#include "distributed/commands/multi_copy.h" +#include "distributed/commands/utility_hook.h" /* IWYU pragma: keep */ +#include "distributed/maintenanced.h" +#include "distributed/master_protocol.h" +#include "distributed/metadata_cache.h" +#include "distributed/metadata_sync.h" +#include "distributed/multi_router_executor.h" +#include "distributed/resource_lock.h" +#include "distributed/transmit.h" +#include "distributed/version_compat.h" +#include "distributed/worker_transaction.h" +#include "lib/stringinfo.h" +#include "tcop/utility.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" +#include "utils/syscache.h" + + +bool EnableDDLPropagation = true; /* ddl propagation is enabled */ +static bool shouldInvalidateForeignKeyGraph = false; + + +/* Local functions forward declarations for helper functions */ +static void ExecuteDistributedDDLJob(DDLJob *ddlJob); +static char * SetSearchPathToCurrentSearchPathCommand(void); +static char * CurrentSearchPath(void); +static void PostProcessUtility(Node *parsetree); + + +/* + * multi_ProcessUtility9x is the 9.x-compatible wrapper for Citus' main utility + * hook. It simply adapts the old-style hook to call into the new-style (10+) + * hook, which is what now houses all actual logic. + */ +void +multi_ProcessUtility9x(Node *parsetree, + const char *queryString, + ProcessUtilityContext context, + ParamListInfo params, + DestReceiver *dest, + char *completionTag) +{ + PlannedStmt *plannedStmt = makeNode(PlannedStmt); + plannedStmt->commandType = CMD_UTILITY; + plannedStmt->utilityStmt = parsetree; + + multi_ProcessUtility(plannedStmt, queryString, context, params, NULL, dest, + completionTag); +} + + +/* + * CitusProcessUtility is a version-aware wrapper of ProcessUtility to account + * for argument differences between the 9.x and 10+ PostgreSQL versions. + */ +void +CitusProcessUtility(Node *node, const char *queryString, ProcessUtilityContext context, + ParamListInfo params, DestReceiver *dest, char *completionTag) +{ +#if (PG_VERSION_NUM >= 100000) + PlannedStmt *plannedStmt = makeNode(PlannedStmt); + plannedStmt->commandType = CMD_UTILITY; + plannedStmt->utilityStmt = node; + + ProcessUtility(plannedStmt, queryString, context, params, NULL, dest, + completionTag); +#else + ProcessUtility(node, queryString, context, params, dest, completionTag); +#endif +} + + +/* + * multi_ProcessUtility is the main entry hook for implementing Citus-specific + * utility behavior. Its primary responsibilities are intercepting COPY and DDL + * commands and augmenting the coordinator's command with corresponding tasks + * to be run on worker nodes, after suitably ensuring said commands' options + * are fully supported by Citus. Much of the DDL behavior is toggled by Citus' + * enable_ddl_propagation GUC. In addition to DDL and COPY, utilities such as + * TRUNCATE and VACUUM are also supported. + */ +void +multi_ProcessUtility(PlannedStmt *pstmt, + const char *queryString, + ProcessUtilityContext context, + ParamListInfo params, + struct QueryEnvironment *queryEnv, + DestReceiver *dest, + char *completionTag) +{ + Node *parsetree = pstmt->utilityStmt; + bool commandMustRunAsOwner = false; + Oid savedUserId = InvalidOid; + int savedSecurityContext = 0; + List *ddlJobs = NIL; + bool checkExtensionVersion = false; + + if (IsA(parsetree, TransactionStmt)) + { + /* + * Transaction statements (e.g. ABORT, COMMIT) can be run in aborted + * transactions in which case a lot of checks cannot be done safely in + * that state. Since we never need to intercept transaction statements, + * skip our checks and immediately fall into standard_ProcessUtility. + */ +#if (PG_VERSION_NUM >= 100000) + standard_ProcessUtility(pstmt, queryString, context, + params, queryEnv, dest, completionTag); +#else + standard_ProcessUtility(parsetree, queryString, context, + params, dest, completionTag); +#endif + + return; + } + + checkExtensionVersion = IsCitusExtensionStmt(parsetree); + if (EnableVersionChecks && checkExtensionVersion) + { + ErrorIfUnstableCreateOrAlterExtensionStmt(parsetree); + } + + + if (!CitusHasBeenLoaded()) + { + /* + * Ensure that utility commands do not behave any differently until CREATE + * EXTENSION is invoked. + */ +#if (PG_VERSION_NUM >= 100000) + standard_ProcessUtility(pstmt, queryString, context, + params, queryEnv, dest, completionTag); +#else + standard_ProcessUtility(parsetree, queryString, context, + params, dest, completionTag); +#endif + + return; + } + +#if (PG_VERSION_NUM >= 100000) + if (IsA(parsetree, CreateSubscriptionStmt)) + { + CreateSubscriptionStmt *createSubStmt = (CreateSubscriptionStmt *) parsetree; + + parsetree = ProcessCreateSubscriptionStmt(createSubStmt); + } +#endif + +#if (PG_VERSION_NUM >= 110000) + if (IsA(parsetree, CallStmt)) + { + /* + * Stored procedures are a bit strange in the sense that some statements + * are not in a transaction block, but can be rolled back. We need to + * make sure we send all statements in a transaction block. The + * StoredProcedureLevel variable signals this to the router executor + * and indicates how deep in the call stack we are in case of nested + * stored procedures. + */ + StoredProcedureLevel += 1; + + PG_TRY(); + { + standard_ProcessUtility(pstmt, queryString, context, + params, queryEnv, dest, completionTag); + + StoredProcedureLevel -= 1; + } + PG_CATCH(); + { + StoredProcedureLevel -= 1; + PG_RE_THROW(); + } + PG_END_TRY(); + + return; + } +#endif + + /* + * TRANSMIT used to be separate command, but to avoid patching the grammar + * it's no overlaid onto COPY, but with FORMAT = 'transmit' instead of the + * normal FORMAT options. + */ + if (IsTransmitStmt(parsetree)) + { + CopyStmt *copyStatement = (CopyStmt *) parsetree; + + VerifyTransmitStmt(copyStatement); + + /* ->relation->relname is the target file in our overloaded COPY */ + if (copyStatement->is_from) + { + RedirectCopyDataToRegularFile(copyStatement->relation->relname); + } + else + { + SendRegularFile(copyStatement->relation->relname); + } + + /* Don't execute the faux copy statement */ + return; + } + + if (IsA(parsetree, CopyStmt)) + { + MemoryContext planContext = GetMemoryChunkContext(parsetree); + MemoryContext previousContext; + + parsetree = copyObject(parsetree); + parsetree = ProcessCopyStmt((CopyStmt *) parsetree, completionTag, + &commandMustRunAsOwner); + + previousContext = MemoryContextSwitchTo(planContext); + parsetree = copyObject(parsetree); + MemoryContextSwitchTo(previousContext); + + if (parsetree == NULL) + { + return; + } + } + + /* we're mostly in DDL (and VACUUM/TRUNCATE) territory at this point... */ + + if (IsA(parsetree, CreateSeqStmt)) + { + ErrorIfUnsupportedSeqStmt((CreateSeqStmt *) parsetree); + } + + if (IsA(parsetree, AlterSeqStmt)) + { + ErrorIfDistributedAlterSeqOwnedBy((AlterSeqStmt *) parsetree); + } + + if (IsA(parsetree, TruncateStmt)) + { + ProcessTruncateStatement((TruncateStmt *) parsetree); + } + + /* only generate worker DDLJobs if propagation is enabled */ + if (EnableDDLPropagation) + { + if (IsA(parsetree, IndexStmt)) + { + MemoryContext oldContext = MemoryContextSwitchTo(GetMemoryChunkContext( + parsetree)); + + /* copy parse tree since we might scribble on it to fix the schema name */ + parsetree = copyObject(parsetree); + + MemoryContextSwitchTo(oldContext); + + ddlJobs = PlanIndexStmt((IndexStmt *) parsetree, queryString); + } + + if (IsA(parsetree, DropStmt)) + { + DropStmt *dropStatement = (DropStmt *) parsetree; + if (dropStatement->removeType == OBJECT_INDEX) + { + ddlJobs = PlanDropIndexStmt(dropStatement, queryString); + } + + if (dropStatement->removeType == OBJECT_TABLE) + { + ProcessDropTableStmt(dropStatement); + } + + if (dropStatement->removeType == OBJECT_SCHEMA) + { + ProcessDropSchemaStmt(dropStatement); + } + + if (dropStatement->removeType == OBJECT_POLICY) + { + ddlJobs = PlanDropPolicyStmt(dropStatement, queryString); + } + } + + if (IsA(parsetree, AlterTableStmt)) + { + AlterTableStmt *alterTableStmt = (AlterTableStmt *) parsetree; + if (alterTableStmt->relkind == OBJECT_TABLE || + alterTableStmt->relkind == OBJECT_INDEX) + { + ddlJobs = PlanAlterTableStmt(alterTableStmt, queryString); + } + } + + /* + * ALTER TABLE ... RENAME statements have their node type as RenameStmt and + * not AlterTableStmt. So, we intercept RenameStmt to tackle these commands. + */ + if (IsA(parsetree, RenameStmt)) + { + ddlJobs = PlanRenameStmt((RenameStmt *) parsetree, queryString); + } + + /* handle distributed CLUSTER statements */ + if (IsA(parsetree, ClusterStmt)) + { + ddlJobs = PlanClusterStmt((ClusterStmt *) parsetree, queryString); + } + + /* + * ALTER ... SET SCHEMA statements have their node type as AlterObjectSchemaStmt. + * So, we intercept AlterObjectSchemaStmt to tackle these commands. + */ + if (IsA(parsetree, AlterObjectSchemaStmt)) + { + AlterObjectSchemaStmt *setSchemaStmt = (AlterObjectSchemaStmt *) parsetree; + ddlJobs = PlanAlterObjectSchemaStmt(setSchemaStmt, queryString); + } + + if (IsA(parsetree, GrantStmt)) + { + ddlJobs = PlanGrantStmt((GrantStmt *) parsetree); + } + + if (IsA(parsetree, CreatePolicyStmt)) + { + ddlJobs = PlanCreatePolicyStmt((CreatePolicyStmt *) parsetree); + } + + if (IsA(parsetree, AlterPolicyStmt)) + { + ddlJobs = PlanAlterPolicyStmt((AlterPolicyStmt *) parsetree); + } + + /* + * ALTER TABLE ALL IN TABLESPACE statements have their node type as + * AlterTableMoveAllStmt. At the moment we do not support this functionality in + * the distributed environment. We warn out here. + */ + if (IsA(parsetree, AlterTableMoveAllStmt)) + { + ereport(WARNING, (errmsg("not propagating ALTER TABLE ALL IN TABLESPACE " + "commands to worker nodes"), + errhint("Connect to worker nodes directly to manually " + "move all tables."))); + } + } + else + { + /* + * citus.enable_ddl_propagation is disabled, which means that PostgreSQL + * should handle the DDL command on a distributed table directly, without + * Citus intervening. The only exception is partition column drop, in + * which case we error out. Advanced Citus users use this to implement their + * own DDL propagation. We also use it to avoid re-propagating DDL commands + * when changing MX tables on workers. Below, we also make sure that DDL + * commands don't run queries that might get intercepted by Citus and error + * out, specifically we skip validation in foreign keys. + */ + + if (IsA(parsetree, AlterTableStmt)) + { + AlterTableStmt *alterTableStmt = (AlterTableStmt *) parsetree; + if (alterTableStmt->relkind == OBJECT_TABLE) + { + ErrorIfAlterDropsPartitionColumn(alterTableStmt); + + /* + * When issuing an ALTER TABLE ... ADD FOREIGN KEY command, the + * the validation step should be skipped on the distributed table. + * Therefore, we check whether the given ALTER TABLE statement is a + * FOREIGN KEY constraint and if so disable the validation step. + * Note that validation is done on the shard level when DDL + * propagation is enabled. Unlike the preceeding Plan* calls, the + * following eagerly executes some tasks on workers. + */ + parsetree = WorkerProcessAlterTableStmt(alterTableStmt, queryString); + } + } + } + + /* inform the user about potential caveats */ + if (IsA(parsetree, CreatedbStmt)) + { + ereport(NOTICE, (errmsg("Citus partially supports CREATE DATABASE for " + "distributed databases"), + errdetail("Citus does not propagate CREATE DATABASE " + "command to workers"), + errhint("You can manually create a database and its " + "extensions on workers."))); + } + else if (IsA(parsetree, CreateRoleStmt)) + { + ereport(NOTICE, (errmsg("not propagating CREATE ROLE/USER commands to worker" + " nodes"), + errhint("Connect to worker nodes directly to manually create all" + " necessary users and roles."))); + } + + /* + * Make sure that on DROP DATABASE we terminate the background daemon + * associated with it. + */ + if (IsA(parsetree, DropdbStmt)) + { + const bool missingOK = true; + DropdbStmt *dropDbStatement = (DropdbStmt *) parsetree; + char *dbname = dropDbStatement->dbname; + Oid databaseOid = get_database_oid(dbname, missingOK); + + if (OidIsValid(databaseOid)) + { + StopMaintenanceDaemon(databaseOid); + } + } + + /* set user if needed and go ahead and run local utility using standard hook */ + if (commandMustRunAsOwner) + { + GetUserIdAndSecContext(&savedUserId, &savedSecurityContext); + SetUserIdAndSecContext(CitusExtensionOwner(), SECURITY_LOCAL_USERID_CHANGE); + } + +#if (PG_VERSION_NUM >= 100000) + pstmt->utilityStmt = parsetree; + standard_ProcessUtility(pstmt, queryString, context, + params, queryEnv, dest, completionTag); +#else + standard_ProcessUtility(parsetree, queryString, context, + params, dest, completionTag); +#endif + + + /* + * We only process CREATE TABLE ... PARTITION OF commands in the function below + * to handle the case when user creates a table as a partition of distributed table. + */ + if (IsA(parsetree, CreateStmt)) + { + CreateStmt *createStatement = (CreateStmt *) parsetree; + + ProcessCreateTableStmtPartitionOf(createStatement); + } + + /* + * We only process ALTER TABLE ... ATTACH PARTITION commands in the function below + * and distribute the partition if necessary. + */ + if (IsA(parsetree, AlterTableStmt)) + { + AlterTableStmt *alterTableStatement = (AlterTableStmt *) parsetree; + + ProcessAlterTableStmtAttachPartition(alterTableStatement); + } + + /* don't run post-process code for local commands */ + if (ddlJobs != NIL) + { + PostProcessUtility(parsetree); + } + + if (commandMustRunAsOwner) + { + SetUserIdAndSecContext(savedUserId, savedSecurityContext); + } + + /* + * Re-forming the foreign key graph relies on the command being executed + * on the local table first. However, in order to decide whether the + * command leads to an invalidation, we need to check before the command + * is being executed since we read pg_constraint table. Thus, we maintain a + * local flag and do the invalidation after multi_ProcessUtility, + * before ExecuteDistributedDDLJob(). + */ + InvalidateForeignKeyGraphForDDL(); + + /* after local command has completed, finish by executing worker DDLJobs, if any */ + if (ddlJobs != NIL) + { + ListCell *ddlJobCell = NULL; + + if (IsA(parsetree, AlterTableStmt)) + { + PostProcessAlterTableStmt(castNode(AlterTableStmt, parsetree)); + } + + foreach(ddlJobCell, ddlJobs) + { + DDLJob *ddlJob = (DDLJob *) lfirst(ddlJobCell); + + ExecuteDistributedDDLJob(ddlJob); + } + } + + /* TODO: fold VACUUM's processing into the above block */ + if (IsA(parsetree, VacuumStmt)) + { + VacuumStmt *vacuumStmt = (VacuumStmt *) parsetree; + + ProcessVacuumStmt(vacuumStmt, queryString); + } + + /* + * Ensure value is valid, we can't do some checks during CREATE + * EXTENSION. This is important to register some invalidation callbacks. + */ + CitusHasBeenLoaded(); +} + + +/* + * ExecuteDistributedDDLJob simply executes a provided DDLJob in a distributed trans- + * action, including metadata sync if needed. If the multi shard commit protocol is + * in its default value of '1pc', then a notice message indicating that '2pc' might be + * used for extra safety. In the commit protocol, a BEGIN is sent after connection to + * each shard placement and COMMIT/ROLLBACK is handled by + * CoordinatedTransactionCallback function. + * + * The function errors out if the node is not the coordinator or if the DDL is on + * a partitioned table which has replication factor > 1. + * + */ +static void +ExecuteDistributedDDLJob(DDLJob *ddlJob) +{ + bool shouldSyncMetadata = ShouldSyncTableMetadata(ddlJob->targetRelationId); + + EnsureCoordinator(); + EnsurePartitionTableNotReplicated(ddlJob->targetRelationId); + + if (!ddlJob->concurrentIndexCmd) + { + if (shouldSyncMetadata) + { + char *setSearchPathCommand = SetSearchPathToCurrentSearchPathCommand(); + + SendCommandToWorkers(WORKERS_WITH_METADATA, DISABLE_DDL_PROPAGATION); + + /* + * Given that we're relaying the query to the worker nodes directly, + * we should set the search path exactly the same when necessary. + */ + if (setSearchPathCommand != NULL) + { + SendCommandToWorkers(WORKERS_WITH_METADATA, setSearchPathCommand); + } + + SendCommandToWorkers(WORKERS_WITH_METADATA, (char *) ddlJob->commandString); + } + + if (MultiShardConnectionType == SEQUENTIAL_CONNECTION || + ddlJob->executeSequentially) + { + ExecuteModifyTasksSequentiallyWithoutResults(ddlJob->taskList, CMD_UTILITY); + } + else + { + ExecuteModifyTasksWithoutResults(ddlJob->taskList); + } + } + else + { + /* save old commit protocol to restore at xact end */ + Assert(SavedMultiShardCommitProtocol == COMMIT_PROTOCOL_BARE); + SavedMultiShardCommitProtocol = MultiShardCommitProtocol; + MultiShardCommitProtocol = COMMIT_PROTOCOL_BARE; + + PG_TRY(); + { + ExecuteModifyTasksSequentiallyWithoutResults(ddlJob->taskList, CMD_UTILITY); + + if (shouldSyncMetadata) + { + List *commandList = list_make1(DISABLE_DDL_PROPAGATION); + char *setSearchPathCommand = SetSearchPathToCurrentSearchPathCommand(); + + /* + * Given that we're relaying the query to the worker nodes directly, + * we should set the search path exactly the same when necessary. + */ + if (setSearchPathCommand != NULL) + { + commandList = lappend(commandList, setSearchPathCommand); + } + + commandList = lappend(commandList, (char *) ddlJob->commandString); + + SendBareCommandListToWorkers(WORKERS_WITH_METADATA, commandList); + } + } + PG_CATCH(); + { + ereport(ERROR, + (errmsg("CONCURRENTLY-enabled index command failed"), + errdetail("CONCURRENTLY-enabled index commands can fail partially, " + "leaving behind an INVALID index."), + errhint("Use DROP INDEX CONCURRENTLY IF EXISTS to remove the " + "invalid index, then retry the original command."))); + } + PG_END_TRY(); + } +} + + +/* + * SetSearchPathToCurrentSearchPathCommand generates a command which can + * set the search path to the exact same search path that the issueing node + * has. + * + * If the current search path is null (or doesn't have any valid schemas), + * the function returns NULL. + */ +static char * +SetSearchPathToCurrentSearchPathCommand(void) +{ + StringInfo setCommand = NULL; + char *currentSearchPath = CurrentSearchPath(); + + if (currentSearchPath == NULL) + { + return NULL; + } + + setCommand = makeStringInfo(); + appendStringInfo(setCommand, "SET search_path TO %s;", currentSearchPath); + + return setCommand->data; +} + + +/* + * CurrentSearchPath is a C interface for calling current_schemas(bool) that + * PostgreSQL exports. + * + * CurrentSchemas returns all the schemas in the seach_path that are seperated + * with comma (,) sign. The returned string can be used to set the search_path. + * + * The function omits implicit schemas. + * + * The function returns NULL if there are no valid schemas in the search_path, + * mimicing current_schemas(false) function. + */ +static char * +CurrentSearchPath(void) +{ + StringInfo currentSearchPath = makeStringInfo(); + List *searchPathList = fetch_search_path(false); + ListCell *searchPathCell; + bool schemaAdded = false; + + foreach(searchPathCell, searchPathList) + { + char *schemaName = get_namespace_name(lfirst_oid(searchPathCell)); + + /* watch out for deleted namespace */ + if (schemaName) + { + if (schemaAdded) + { + appendStringInfoString(currentSearchPath, ","); + schemaAdded = false; + } + + appendStringInfoString(currentSearchPath, quote_identifier(schemaName)); + schemaAdded = true; + } + } + + /* fetch_search_path() returns a palloc'd list that we should free now */ + list_free(searchPathList); + + return (currentSearchPath->len > 0 ? currentSearchPath->data : NULL); +} + + +/* + * PostProcessUtility performs additional tasks after a utility's local portion + * has been completed. + */ +static void +PostProcessUtility(Node *parsetree) +{ + if (IsA(parsetree, IndexStmt)) + { + PostProcessIndexStmt(castNode(IndexStmt, parsetree)); + } +} + + +/* + * MarkInvalidateForeignKeyGraph marks whether the foreign key graph should be + * invalidated due to a DDL. + */ +void +MarkInvalidateForeignKeyGraph() +{ + shouldInvalidateForeignKeyGraph = true; +} + + +/* + * InvalidateForeignKeyGraphForDDL simply keeps track of whether + * the foreign key graph should be invalidated due to a DDL. + */ +void +InvalidateForeignKeyGraphForDDL(void) +{ + if (shouldInvalidateForeignKeyGraph) + { + InvalidateForeignKeyGraph(); + + shouldInvalidateForeignKeyGraph = false; + } +} + + +/* + * DDLTaskList builds a list of tasks to execute a DDL command on a + * given list of shards. + */ +List * +DDLTaskList(Oid relationId, const char *commandString) +{ + List *taskList = NIL; + List *shardIntervalList = LoadShardIntervalList(relationId); + ListCell *shardIntervalCell = NULL; + Oid schemaId = get_rel_namespace(relationId); + char *schemaName = get_namespace_name(schemaId); + char *escapedSchemaName = quote_literal_cstr(schemaName); + char *escapedCommandString = quote_literal_cstr(commandString); + uint64 jobId = INVALID_JOB_ID; + int taskId = 1; + + /* lock metadata before getting placement lists */ + LockShardListMetadata(shardIntervalList, ShareLock); + + foreach(shardIntervalCell, shardIntervalList) + { + ShardInterval *shardInterval = (ShardInterval *) lfirst(shardIntervalCell); + uint64 shardId = shardInterval->shardId; + StringInfo applyCommand = makeStringInfo(); + Task *task = NULL; + + /* + * If rightRelationId is not InvalidOid, instead of worker_apply_shard_ddl_command + * we use worker_apply_inter_shard_ddl_command. + */ + appendStringInfo(applyCommand, WORKER_APPLY_SHARD_DDL_COMMAND, shardId, + escapedSchemaName, escapedCommandString); + + task = CitusMakeNode(Task); + task->jobId = jobId; + task->taskId = taskId++; + task->taskType = DDL_TASK; + task->queryString = applyCommand->data; + task->replicationModel = REPLICATION_MODEL_INVALID; + task->dependedTaskList = NULL; + task->anchorShardId = shardId; + task->taskPlacementList = FinalizedShardPlacementList(shardId); + + taskList = lappend(taskList, task); + } + + return taskList; +} diff --git a/src/backend/distributed/commands/vacuum.c b/src/backend/distributed/commands/vacuum.c new file mode 100644 index 00000000000..9288fcc6d66 --- /dev/null +++ b/src/backend/distributed/commands/vacuum.c @@ -0,0 +1,341 @@ +/*------------------------------------------------------------------------- + * + * vacuum.c + * Commands for vacuuming distributed tables. + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "c.h" + +#include "distributed/commands.h" +#include "distributed/commands/utility_hook.h" +#include "distributed/metadata_cache.h" +#include "distributed/multi_router_executor.h" +#include "distributed/resource_lock.h" +#include "distributed/transaction_management.h" +#include "distributed/version_compat.h" +#include "storage/lmgr.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + + +/* Local functions forward declarations for processing distributed table commands */ +static bool IsDistributedVacuumStmt(VacuumStmt *vacuumStmt, List *vacuumRelationIdList); +static List * VacuumTaskList(Oid relationId, int vacuumOptions, List *vacuumColumnList); +static StringInfo DeparseVacuumStmtPrefix(int vacuumFlags); +static char * DeparseVacuumColumnNames(List *columnNameList); + + +/* + * ProcessVacuumStmt processes vacuum statements that may need propagation to + * distributed tables. If a VACUUM or ANALYZE command references a distributed + * table, it is propagated to all involved nodes; otherwise, this function will + * immediately exit after some error checking. + * + * Unlike most other Process functions within this file, this function does not + * return a modified parse node, as it is expected that the local VACUUM or + * ANALYZE has already been processed. + */ +void +ProcessVacuumStmt(VacuumStmt *vacuumStmt, const char *vacuumCommand) +{ + int relationIndex = 0; + bool distributedVacuumStmt = false; + List *vacuumRelationList = ExtractVacuumTargetRels(vacuumStmt); + ListCell *vacuumRelationCell = NULL; + List *relationIdList = NIL; + ListCell *relationIdCell = NULL; + LOCKMODE lockMode = (vacuumStmt->options & VACOPT_FULL) ? AccessExclusiveLock : + ShareUpdateExclusiveLock; + int executedVacuumCount = 0; + + foreach(vacuumRelationCell, vacuumRelationList) + { + RangeVar *vacuumRelation = (RangeVar *) lfirst(vacuumRelationCell); + Oid relationId = RangeVarGetRelid(vacuumRelation, lockMode, false); + relationIdList = lappend_oid(relationIdList, relationId); + } + + distributedVacuumStmt = IsDistributedVacuumStmt(vacuumStmt, relationIdList); + if (!distributedVacuumStmt) + { + return; + } + + /* execute vacuum on distributed tables */ + foreach(relationIdCell, relationIdList) + { + Oid relationId = lfirst_oid(relationIdCell); + if (IsDistributedTable(relationId)) + { + List *vacuumColumnList = NIL; + List *taskList = NIL; + + /* + * VACUUM commands cannot run inside a transaction block, so we use + * the "bare" commit protocol without BEGIN/COMMIT. However, ANALYZE + * commands can run inside a transaction block. Notice that we do this + * once even if there are multiple distributed tables to be vacuumed. + */ + if (executedVacuumCount == 0 && (vacuumStmt->options & VACOPT_VACUUM) != 0) + { + /* save old commit protocol to restore at xact end */ + Assert(SavedMultiShardCommitProtocol == COMMIT_PROTOCOL_BARE); + SavedMultiShardCommitProtocol = MultiShardCommitProtocol; + MultiShardCommitProtocol = COMMIT_PROTOCOL_BARE; + } + + vacuumColumnList = VacuumColumnList(vacuumStmt, relationIndex); + taskList = VacuumTaskList(relationId, vacuumStmt->options, vacuumColumnList); + + ExecuteModifyTasksWithoutResults(taskList); + + executedVacuumCount++; + } + relationIndex++; + } +} + + +/* + * IsSupportedDistributedVacuumStmt returns whether distributed execution of a + * given VacuumStmt is supported. The provided relationId list represents + * the list of tables targeted by the provided statement. + * + * Returns true if the statement requires distributed execution and returns + * false otherwise. + */ +static bool +IsDistributedVacuumStmt(VacuumStmt *vacuumStmt, List *vacuumRelationIdList) +{ + const char *stmtName = (vacuumStmt->options & VACOPT_VACUUM) ? "VACUUM" : "ANALYZE"; + bool distributeStmt = false; + ListCell *relationIdCell = NULL; + int distributedRelationCount = 0; + int vacuumedRelationCount = 0; + + /* + * No table in the vacuum statement means vacuuming all relations + * which is not supported by citus. + */ + vacuumedRelationCount = list_length(vacuumRelationIdList); + if (vacuumedRelationCount == 0) + { + /* WARN for unqualified VACUUM commands */ + ereport(WARNING, (errmsg("not propagating %s command to worker nodes", stmtName), + errhint("Provide a specific table in order to %s " + "distributed tables.", stmtName))); + } + + foreach(relationIdCell, vacuumRelationIdList) + { + Oid relationId = lfirst_oid(relationIdCell); + if (OidIsValid(relationId) && IsDistributedTable(relationId)) + { + distributedRelationCount++; + } + } + + if (distributedRelationCount == 0) + { + /* nothing to do here */ + } + else if (!EnableDDLPropagation) + { + /* WARN if DDL propagation is not enabled */ + ereport(WARNING, (errmsg("not propagating %s command to worker nodes", stmtName), + errhint("Set citus.enable_ddl_propagation to true in order to " + "send targeted %s commands to worker nodes.", + stmtName))); + } + else + { + distributeStmt = true; + } + + return distributeStmt; +} + + +/* + * VacuumTaskList returns a list of tasks to be executed as part of processing + * a VacuumStmt which targets a distributed relation. + */ +static List * +VacuumTaskList(Oid relationId, int vacuumOptions, List *vacuumColumnList) +{ + List *taskList = NIL; + List *shardIntervalList = NIL; + ListCell *shardIntervalCell = NULL; + uint64 jobId = INVALID_JOB_ID; + int taskId = 1; + StringInfo vacuumString = DeparseVacuumStmtPrefix(vacuumOptions); + const char *columnNames = NULL; + const int vacuumPrefixLen = vacuumString->len; + Oid schemaId = get_rel_namespace(relationId); + char *schemaName = get_namespace_name(schemaId); + char *tableName = get_rel_name(relationId); + + columnNames = DeparseVacuumColumnNames(vacuumColumnList); + + /* + * We obtain ShareUpdateExclusiveLock here to not conflict with INSERT's + * RowExclusiveLock. However if VACUUM FULL is used, we already obtain + * AccessExclusiveLock before reaching to that point and INSERT's will be + * blocked anyway. This is inline with PostgreSQL's own behaviour. + */ + LockRelationOid(relationId, ShareUpdateExclusiveLock); + + shardIntervalList = LoadShardIntervalList(relationId); + + /* grab shard lock before getting placement list */ + LockShardListMetadata(shardIntervalList, ShareLock); + + foreach(shardIntervalCell, shardIntervalList) + { + ShardInterval *shardInterval = (ShardInterval *) lfirst(shardIntervalCell); + uint64 shardId = shardInterval->shardId; + Task *task = NULL; + + char *shardName = pstrdup(tableName); + AppendShardIdToName(&shardName, shardInterval->shardId); + shardName = quote_qualified_identifier(schemaName, shardName); + + vacuumString->len = vacuumPrefixLen; + appendStringInfoString(vacuumString, shardName); + appendStringInfoString(vacuumString, columnNames); + + task = CitusMakeNode(Task); + task->jobId = jobId; + task->taskId = taskId++; + task->taskType = VACUUM_ANALYZE_TASK; + task->queryString = pstrdup(vacuumString->data); + task->dependedTaskList = NULL; + task->replicationModel = REPLICATION_MODEL_INVALID; + task->anchorShardId = shardId; + task->taskPlacementList = FinalizedShardPlacementList(shardId); + + taskList = lappend(taskList, task); + } + + return taskList; +} + + +/* + * DeparseVacuumStmtPrefix returns a StringInfo appropriate for use as a prefix + * during distributed execution of a VACUUM or ANALYZE statement. Callers may + * reuse this prefix within a loop to generate shard-specific VACUUM or ANALYZE + * statements. + */ +static StringInfo +DeparseVacuumStmtPrefix(int vacuumFlags) +{ + StringInfo vacuumPrefix = makeStringInfo(); + const int unsupportedFlags PG_USED_FOR_ASSERTS_ONLY = ~( + VACOPT_ANALYZE | + VACOPT_DISABLE_PAGE_SKIPPING | + VACOPT_FREEZE | + VACOPT_FULL | + VACOPT_VERBOSE + ); + + /* determine actual command and block out its bit */ + if (vacuumFlags & VACOPT_VACUUM) + { + appendStringInfoString(vacuumPrefix, "VACUUM "); + vacuumFlags &= ~VACOPT_VACUUM; + } + else + { + appendStringInfoString(vacuumPrefix, "ANALYZE "); + vacuumFlags &= ~VACOPT_ANALYZE; + + if (vacuumFlags & VACOPT_VERBOSE) + { + appendStringInfoString(vacuumPrefix, "VERBOSE "); + vacuumFlags &= ~VACOPT_VERBOSE; + } + } + + /* unsupported flags should have already been rejected */ + Assert((vacuumFlags & unsupportedFlags) == 0); + + /* if no flags remain, exit early */ + if (vacuumFlags == 0) + { + return vacuumPrefix; + } + + /* otherwise, handle options */ + appendStringInfoChar(vacuumPrefix, '('); + + if (vacuumFlags & VACOPT_ANALYZE) + { + appendStringInfoString(vacuumPrefix, "ANALYZE,"); + } + + if (vacuumFlags & VACOPT_DISABLE_PAGE_SKIPPING) + { + appendStringInfoString(vacuumPrefix, "DISABLE_PAGE_SKIPPING,"); + } + + if (vacuumFlags & VACOPT_FREEZE) + { + appendStringInfoString(vacuumPrefix, "FREEZE,"); + } + + if (vacuumFlags & VACOPT_FULL) + { + appendStringInfoString(vacuumPrefix, "FULL,"); + } + + if (vacuumFlags & VACOPT_VERBOSE) + { + appendStringInfoString(vacuumPrefix, "VERBOSE,"); + } + + vacuumPrefix->data[vacuumPrefix->len - 1] = ')'; + + appendStringInfoChar(vacuumPrefix, ' '); + + return vacuumPrefix; +} + + +/* + * DeparseVacuumColumnNames joins the list of strings using commas as a + * delimiter. The whole thing is placed in parenthesis and set off with a + * single space in order to facilitate appending it to the end of any VACUUM + * or ANALYZE command which uses explicit column names. If the provided list + * is empty, this function returns an empty string to keep the calling code + * simplest. + */ +static char * +DeparseVacuumColumnNames(List *columnNameList) +{ + StringInfo columnNames = makeStringInfo(); + ListCell *columnNameCell = NULL; + + if (columnNameList == NIL) + { + return columnNames->data; + } + + appendStringInfoString(columnNames, " ("); + + foreach(columnNameCell, columnNameList) + { + char *columnName = strVal(lfirst(columnNameCell)); + + appendStringInfo(columnNames, "%s,", columnName); + } + + columnNames->data[columnNames->len - 1] = ')'; + + return columnNames->data; +} diff --git a/src/backend/distributed/executor/insert_select_executor.c b/src/backend/distributed/executor/insert_select_executor.c index e756cecdce9..d03f70dce68 100644 --- a/src/backend/distributed/executor/insert_select_executor.c +++ b/src/backend/distributed/executor/insert_select_executor.c @@ -10,9 +10,9 @@ #include "postgres.h" +#include "distributed/commands/multi_copy.h" #include "distributed/insert_select_executor.h" #include "distributed/insert_select_planner.h" -#include "distributed/multi_copy.h" #include "distributed/multi_executor.h" #include "distributed/multi_partitioning_utils.h" #include "distributed/multi_physical_planner.h" diff --git a/src/backend/distributed/executor/intermediate_results.c b/src/backend/distributed/executor/intermediate_results.c index a324b7a64c6..2198f8fb561 100644 --- a/src/backend/distributed/executor/intermediate_results.c +++ b/src/backend/distributed/executor/intermediate_results.c @@ -18,11 +18,11 @@ #include "catalog/pg_enum.h" #include "commands/copy.h" +#include "distributed/commands/multi_copy.h" #include "distributed/connection_management.h" #include "distributed/intermediate_results.h" #include "distributed/master_metadata_utility.h" #include "distributed/metadata_cache.h" -#include "distributed/multi_copy.h" #include "distributed/multi_executor.h" #include "distributed/remote_commands.h" #include "distributed/transmit.h" diff --git a/src/backend/distributed/executor/multi_executor.c b/src/backend/distributed/executor/multi_executor.c index 5735e44227a..73d2d88e3ae 100644 --- a/src/backend/distributed/executor/multi_executor.c +++ b/src/backend/distributed/executor/multi_executor.c @@ -16,9 +16,9 @@ #include "catalog/dependency.h" #include "catalog/namespace.h" #include "distributed/citus_custom_scan.h" +#include "distributed/commands/multi_copy.h" #include "distributed/insert_select_executor.h" #include "distributed/insert_select_planner.h" -#include "distributed/multi_copy.h" #include "distributed/multi_executor.h" #include "distributed/multi_master_planner.h" #include "distributed/distributed_planner.h" @@ -26,7 +26,6 @@ #include "distributed/multi_router_planner.h" #include "distributed/multi_resowner.h" #include "distributed/multi_server_executor.h" -#include "distributed/multi_utility.h" #include "distributed/resource_lock.h" #include "distributed/worker_protocol.h" #include "executor/execdebug.h" diff --git a/src/backend/distributed/executor/multi_utility.c b/src/backend/distributed/executor/multi_utility.c deleted file mode 100644 index 8b895b551c4..00000000000 --- a/src/backend/distributed/executor/multi_utility.c +++ /dev/null @@ -1,4664 +0,0 @@ -/*------------------------------------------------------------------------- - * multi_utility.c - * Citus utility hook and related functionality. - * - * Copyright (c) 2012-2016, Citus Data, Inc. - *------------------------------------------------------------------------- - */ - -#include "postgres.h" -#include "c.h" -#include "libpq-fe.h" -#include "miscadmin.h" -#include "port.h" - -#include - -#include "access/attnum.h" -#include "access/heapam.h" -#include "access/htup.h" -#include "access/htup_details.h" -#include "access/sysattr.h" -#include "access/tupdesc.h" -#include "access/xact.h" -#include "catalog/catalog.h" -#include "catalog/dependency.h" -#include "catalog/index.h" -#include "catalog/indexing.h" -#include "catalog/pg_attribute.h" -#include "catalog/pg_class.h" -#include "citus_version.h" -#include "commands/dbcommands.h" -#include "commands/defrem.h" -#include "commands/tablecmds.h" -#include "commands/prepare.h" -#include "distributed/citus_ruleutils.h" -#include "distributed/colocation_utils.h" -#include "distributed/foreign_constraint.h" -#include "distributed/intermediate_results.h" -#include "distributed/listutils.h" -#include "distributed/maintenanced.h" -#include "distributed/master_metadata_utility.h" -#include "distributed/master_protocol.h" -#include "distributed/metadata_cache.h" -#include "distributed/metadata_sync.h" -#include "distributed/multi_copy.h" -#include "distributed/multi_partitioning_utils.h" -#include "distributed/distributed_planner.h" -#include "distributed/multi_router_executor.h" -#include "distributed/multi_router_planner.h" -#include "distributed/multi_shard_transaction.h" -#include "distributed/multi_utility.h" /* IWYU pragma: keep */ -#include "distributed/pg_dist_partition.h" -#include "distributed/policy.h" -#include "distributed/reference_table_utils.h" -#include "distributed/relation_access_tracking.h" -#include "distributed/resource_lock.h" -#include "distributed/transaction_management.h" -#include "distributed/transmit.h" -#include "distributed/worker_protocol.h" -#include "distributed/worker_transaction.h" -#include "distributed/version_compat.h" -#include "executor/executor.h" -#include "foreign/foreign.h" -#include "lib/stringinfo.h" -#include "nodes/bitmapset.h" -#include "nodes/memnodes.h" -#include "nodes/nodes.h" -#include "nodes/params.h" -#include "nodes/parsenodes.h" -#include "nodes/pg_list.h" -#include "nodes/primnodes.h" -#include "nodes/value.h" -#include "parser/analyze.h" -#include "storage/lmgr.h" -#include "storage/lock.h" -#include "tcop/dest.h" -#include "tcop/utility.h" -#include "utils/acl.h" -#include "utils/builtins.h" -#include "utils/elog.h" -#include "utils/errcodes.h" -#include "utils/fmgroids.h" -#include "utils/guc.h" -#include "utils/hsearch.h" -#include "utils/inval.h" -#include "utils/lsyscache.h" -#include "utils/memutils.h" -#include "utils/palloc.h" -#include "utils/syscache.h" - - -#define LOCK_RELATION_IF_EXISTS "SELECT lock_relation_if_exists('%s', '%s');" - - -bool EnableDDLPropagation = true; /* ddl propagation is enabled */ - - -static bool shouldInvalidateForeignKeyGraph = false; - - -/* - * This struct defines the state for the callback for drop statements. - * It is copied as it is from commands/tablecmds.c in Postgres source. - */ -struct DropRelationCallbackState -{ - char relkind; - Oid heapOid; - bool concurrent; -}; - - -/* Local functions forward declarations for deciding when to perform processing/checks */ -static bool IsCitusExtensionStmt(Node *parsetree); - -/* Local functions forward declarations for Transmit statement */ -static bool IsTransmitStmt(Node *parsetree); -static void VerifyTransmitStmt(CopyStmt *copyStatement); -static bool IsCopyResultStmt(CopyStmt *copyStatement); - -/* Local functions forward declarations for processing distributed table commands */ -static Node * ProcessCopyStmt(CopyStmt *copyStatement, char *completionTag, - bool *commandMustRunAsOwner); -static void ProcessCreateTableStmtPartitionOf(CreateStmt *createStatement); -static void ProcessAlterTableStmtAttachPartition(AlterTableStmt *alterTableStatement); -static List * PlanIndexStmt(IndexStmt *createIndexStatement, - const char *createIndexCommand); -static List * PlanDropIndexStmt(DropStmt *dropIndexStatement, - const char *dropIndexCommand); -static List * PlanAlterTableStmt(AlterTableStmt *alterTableStatement, - const char *alterTableCommand); -static List * PlanRenameStmt(RenameStmt *renameStmt, const char *renameCommand); -static Node * WorkerProcessAlterTableStmt(AlterTableStmt *alterTableStatement, - const char *alterTableCommand); -static List * PlanAlterObjectSchemaStmt(AlterObjectSchemaStmt *alterObjectSchemaStmt, - const char *alterObjectSchemaCommand); -static void ProcessVacuumStmt(VacuumStmt *vacuumStmt, const char *vacuumCommand); -static bool IsDistributedVacuumStmt(VacuumStmt *vacuumStmt, List *vacuumRelationIdList); -static List * VacuumTaskList(Oid relationId, int vacuumOptions, List *vacuumColumnList); -static StringInfo DeparseVacuumStmtPrefix(int vacuumFlags); -static char * DeparseVacuumColumnNames(List *columnNameList); - - -/* Local functions forward declarations for unsupported command checks */ -static void ErrorIfUnstableCreateOrAlterExtensionStmt(Node *parsetree); -static void ErrorIfUnsupportedIndexStmt(IndexStmt *createIndexStatement); -static void ErrorIfUnsupportedDropIndexStmt(DropStmt *dropIndexStatement); -static void ErrorIfUnsupportedAlterTableStmt(AlterTableStmt *alterTableStatement); -static void ErrorIfUnsupportedAlterIndexStmt(AlterTableStmt *alterTableStatement); -static void ErrorIfAlterDropsPartitionColumn(AlterTableStmt *alterTableStatement); -static void ErrorIfUnsupportedSeqStmt(CreateSeqStmt *createSeqStmt); -static void ErrorIfDistributedAlterSeqOwnedBy(AlterSeqStmt *alterSeqStmt); -static void ErrorIfUnsupportedTruncateStmt(TruncateStmt *truncateStatement); -static void ProcessTruncateStatement(TruncateStmt *truncateStatement); -static void ExecuteTruncateStmtSequentialIfNecessary(TruncateStmt *command); -static void EnsurePartitionTableNotReplicatedForTruncate(TruncateStmt *truncateStatement); -static void LockTruncatedRelationMetadataInWorkers(TruncateStmt *truncateStatement); -static void AcquireDistributedLockOnRelations(List *relationIdList, LOCKMODE lockMode); -static bool OptionsSpecifyOwnedBy(List *optionList, Oid *ownedByTableId); -static void ErrorIfUnsupportedRenameStmt(RenameStmt *renameStmt); -static void ErrorIfUnsupportedAlterAddConstraintStmt(AlterTableStmt *alterTableStatement); - -/* Local functions forward declarations for helper functions */ -static char * ExtractNewExtensionVersion(Node *parsetree); -static void CreateLocalTable(RangeVar *relation, char *nodeName, int32 nodePort); -static bool IsAlterTableRenameStmt(RenameStmt *renameStmt); -static bool IsIndexRenameStmt(RenameStmt *renameStmt); -static bool AlterInvolvesPartitionColumn(AlterTableStmt *alterTableStatement, - AlterTableCmd *command); -static void ExecuteDistributedDDLJob(DDLJob *ddlJob); -static char * SetSearchPathToCurrentSearchPathCommand(void); -static char * CurrentSearchPath(void); -static List * CreateIndexTaskList(Oid relationId, IndexStmt *indexStmt); -static List * DropIndexTaskList(Oid relationId, Oid indexId, DropStmt *dropStmt); -static List * InterShardDDLTaskList(Oid leftRelationId, Oid rightRelationId, - const char *commandString); -static void RangeVarCallbackForDropIndex(const RangeVar *rel, Oid relOid, Oid oldRelOid, - void *arg); -static void CheckCopyPermissions(CopyStmt *copyStatement); -static List * CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist); -static void PostProcessUtility(Node *parsetree); -static List * CollectGrantTableIdList(GrantStmt *grantStmt); -static char * GetSchemaNameFromDropObject(ListCell *dropSchemaCell); -static void ProcessDropTableStmt(DropStmt *dropTableStatement); -static void ProcessDropSchemaStmt(DropStmt *dropSchemaStatement); -static void InvalidateForeignKeyGraphForDDL(void); - -static void ErrorUnsupportedAlterTableAddColumn(Oid relationId, AlterTableCmd *command, - Constraint *constraint); - -/* - * We need to run some of the commands sequentially if there is a foreign constraint - * from/to reference table. - */ -static bool SetupExecutionModeForAlterTable(Oid relationId, AlterTableCmd *command); - - -/* - * multi_ProcessUtility9x is the 9.x-compatible wrapper for Citus' main utility - * hook. It simply adapts the old-style hook to call into the new-style (10+) - * hook, which is what now houses all actual logic. - */ -void -multi_ProcessUtility9x(Node *parsetree, - const char *queryString, - ProcessUtilityContext context, - ParamListInfo params, - DestReceiver *dest, - char *completionTag) -{ - PlannedStmt *plannedStmt = makeNode(PlannedStmt); - plannedStmt->commandType = CMD_UTILITY; - plannedStmt->utilityStmt = parsetree; - - multi_ProcessUtility(plannedStmt, queryString, context, params, NULL, dest, - completionTag); -} - - -/* - * CitusProcessUtility is a version-aware wrapper of ProcessUtility to account - * for argument differences between the 9.x and 10+ PostgreSQL versions. - */ -void -CitusProcessUtility(Node *node, const char *queryString, ProcessUtilityContext context, - ParamListInfo params, DestReceiver *dest, char *completionTag) -{ -#if (PG_VERSION_NUM >= 100000) - PlannedStmt *plannedStmt = makeNode(PlannedStmt); - plannedStmt->commandType = CMD_UTILITY; - plannedStmt->utilityStmt = node; - - ProcessUtility(plannedStmt, queryString, context, params, NULL, dest, - completionTag); -#else - ProcessUtility(node, queryString, context, params, dest, completionTag); -#endif -} - - -/* - * multi_ProcessUtility is the main entry hook for implementing Citus-specific - * utility behavior. Its primary responsibilities are intercepting COPY and DDL - * commands and augmenting the coordinator's command with corresponding tasks - * to be run on worker nodes, after suitably ensuring said commands' options - * are fully supported by Citus. Much of the DDL behavior is toggled by Citus' - * enable_ddl_propagation GUC. In addition to DDL and COPY, utilities such as - * TRUNCATE and VACUUM are also supported. - */ -void -multi_ProcessUtility(PlannedStmt *pstmt, - const char *queryString, - ProcessUtilityContext context, - ParamListInfo params, - struct QueryEnvironment *queryEnv, - DestReceiver *dest, - char *completionTag) -{ - Node *parsetree = pstmt->utilityStmt; - bool commandMustRunAsOwner = false; - Oid savedUserId = InvalidOid; - int savedSecurityContext = 0; - List *ddlJobs = NIL; - bool checkExtensionVersion = false; - - if (IsA(parsetree, TransactionStmt)) - { - /* - * Transaction statements (e.g. ABORT, COMMIT) can be run in aborted - * transactions in which case a lot of checks cannot be done safely in - * that state. Since we never need to intercept transaction statements, - * skip our checks and immediately fall into standard_ProcessUtility. - */ -#if (PG_VERSION_NUM >= 100000) - standard_ProcessUtility(pstmt, queryString, context, - params, queryEnv, dest, completionTag); -#else - standard_ProcessUtility(parsetree, queryString, context, - params, dest, completionTag); -#endif - - return; - } - - checkExtensionVersion = IsCitusExtensionStmt(parsetree); - if (EnableVersionChecks && checkExtensionVersion) - { - ErrorIfUnstableCreateOrAlterExtensionStmt(parsetree); - } - - - if (!CitusHasBeenLoaded()) - { - /* - * Ensure that utility commands do not behave any differently until CREATE - * EXTENSION is invoked. - */ -#if (PG_VERSION_NUM >= 100000) - standard_ProcessUtility(pstmt, queryString, context, - params, queryEnv, dest, completionTag); -#else - standard_ProcessUtility(parsetree, queryString, context, - params, dest, completionTag); -#endif - - return; - } - -#if (PG_VERSION_NUM >= 110000) - if (IsA(parsetree, CallStmt)) - { - /* - * Stored procedures are a bit strange in the sense that some statements - * are not in a transaction block, but can be rolled back. We need to - * make sure we send all statements in a transaction block. The - * StoredProcedureLevel variable signals this to the router executor - * and indicates how deep in the call stack we are in case of nested - * stored procedures. - */ - StoredProcedureLevel += 1; - - PG_TRY(); - { - standard_ProcessUtility(pstmt, queryString, context, - params, queryEnv, dest, completionTag); - - StoredProcedureLevel -= 1; - } - PG_CATCH(); - { - StoredProcedureLevel -= 1; - PG_RE_THROW(); - } - PG_END_TRY(); - - return; - } -#endif - - /* - * TRANSMIT used to be separate command, but to avoid patching the grammar - * it's no overlaid onto COPY, but with FORMAT = 'transmit' instead of the - * normal FORMAT options. - */ - if (IsTransmitStmt(parsetree)) - { - CopyStmt *copyStatement = (CopyStmt *) parsetree; - - VerifyTransmitStmt(copyStatement); - - /* ->relation->relname is the target file in our overloaded COPY */ - if (copyStatement->is_from) - { - RedirectCopyDataToRegularFile(copyStatement->relation->relname); - } - else - { - SendRegularFile(copyStatement->relation->relname); - } - - /* Don't execute the faux copy statement */ - return; - } - - if (IsA(parsetree, CopyStmt)) - { - MemoryContext planContext = GetMemoryChunkContext(parsetree); - MemoryContext previousContext; - - parsetree = copyObject(parsetree); - parsetree = ProcessCopyStmt((CopyStmt *) parsetree, completionTag, - &commandMustRunAsOwner); - - previousContext = MemoryContextSwitchTo(planContext); - parsetree = copyObject(parsetree); - MemoryContextSwitchTo(previousContext); - - if (parsetree == NULL) - { - return; - } - } - - /* we're mostly in DDL (and VACUUM/TRUNCATE) territory at this point... */ - - if (IsA(parsetree, CreateSeqStmt)) - { - ErrorIfUnsupportedSeqStmt((CreateSeqStmt *) parsetree); - } - - if (IsA(parsetree, AlterSeqStmt)) - { - ErrorIfDistributedAlterSeqOwnedBy((AlterSeqStmt *) parsetree); - } - - if (IsA(parsetree, TruncateStmt)) - { - ErrorIfUnsupportedTruncateStmt((TruncateStmt *) parsetree); - ProcessTruncateStatement((TruncateStmt *) parsetree); - } - - /* only generate worker DDLJobs if propagation is enabled */ - if (EnableDDLPropagation) - { - if (IsA(parsetree, IndexStmt)) - { - MemoryContext oldContext = MemoryContextSwitchTo(GetMemoryChunkContext( - parsetree)); - - /* copy parse tree since we might scribble on it to fix the schema name */ - parsetree = copyObject(parsetree); - - MemoryContextSwitchTo(oldContext); - - ddlJobs = PlanIndexStmt((IndexStmt *) parsetree, queryString); - } - - if (IsA(parsetree, DropStmt)) - { - DropStmt *dropStatement = (DropStmt *) parsetree; - if (dropStatement->removeType == OBJECT_INDEX) - { - ddlJobs = PlanDropIndexStmt(dropStatement, queryString); - } - - if (dropStatement->removeType == OBJECT_TABLE) - { - ProcessDropTableStmt(dropStatement); - } - - if (dropStatement->removeType == OBJECT_SCHEMA) - { - ProcessDropSchemaStmt(dropStatement); - } - - if (dropStatement->removeType == OBJECT_POLICY) - { - ddlJobs = PlanDropPolicyStmt(dropStatement, queryString); - } - } - - if (IsA(parsetree, AlterTableStmt)) - { - AlterTableStmt *alterTableStmt = (AlterTableStmt *) parsetree; - if (alterTableStmt->relkind == OBJECT_TABLE || - alterTableStmt->relkind == OBJECT_INDEX) - { - ddlJobs = PlanAlterTableStmt(alterTableStmt, queryString); - } - } - - /* - * ALTER TABLE ... RENAME statements have their node type as RenameStmt and - * not AlterTableStmt. So, we intercept RenameStmt to tackle these commands. - */ - if (IsA(parsetree, RenameStmt)) - { - ddlJobs = PlanRenameStmt((RenameStmt *) parsetree, queryString); - } - - /* - * ALTER ... SET SCHEMA statements have their node type as AlterObjectSchemaStmt. - * So, we intercept AlterObjectSchemaStmt to tackle these commands. - */ - if (IsA(parsetree, AlterObjectSchemaStmt)) - { - AlterObjectSchemaStmt *setSchemaStmt = (AlterObjectSchemaStmt *) parsetree; - ddlJobs = PlanAlterObjectSchemaStmt(setSchemaStmt, queryString); - } - - if (IsA(parsetree, CreatePolicyStmt)) - { - ddlJobs = PlanCreatePolicyStmt((CreatePolicyStmt *) parsetree); - } - - if (IsA(parsetree, AlterPolicyStmt)) - { - ddlJobs = PlanAlterPolicyStmt((AlterPolicyStmt *) parsetree); - } - - /* - * ALTER TABLE ALL IN TABLESPACE statements have their node type as - * AlterTableMoveAllStmt. At the moment we do not support this functionality in - * the distributed environment. We warn out here. - */ - if (IsA(parsetree, AlterTableMoveAllStmt)) - { - ereport(WARNING, (errmsg("not propagating ALTER TABLE ALL IN TABLESPACE " - "commands to worker nodes"), - errhint("Connect to worker nodes directly to manually " - "move all tables."))); - } - } - else - { - /* - * citus.enable_ddl_propagation is disabled, which means that PostgreSQL - * should handle the DDL command on a distributed table directly, without - * Citus intervening. The only exception is partition column drop, in - * which case we error out. Advanced Citus users use this to implement their - * own DDL propagation. We also use it to avoid re-propagating DDL commands - * when changing MX tables on workers. Below, we also make sure that DDL - * commands don't run queries that might get intercepted by Citus and error - * out, specifically we skip validation in foreign keys. - */ - - if (IsA(parsetree, AlterTableStmt)) - { - AlterTableStmt *alterTableStmt = (AlterTableStmt *) parsetree; - if (alterTableStmt->relkind == OBJECT_TABLE) - { - ErrorIfAlterDropsPartitionColumn(alterTableStmt); - - /* - * When issuing an ALTER TABLE ... ADD FOREIGN KEY command, the - * the validation step should be skipped on the distributed table. - * Therefore, we check whether the given ALTER TABLE statement is a - * FOREIGN KEY constraint and if so disable the validation step. - * Note that validation is done on the shard level when DDL - * propagation is enabled. Unlike the preceeding Plan* calls, the - * following eagerly executes some tasks on workers. - */ - parsetree = WorkerProcessAlterTableStmt(alterTableStmt, queryString); - } - } - } - - /* inform the user about potential caveats */ - if (IsA(parsetree, CreatedbStmt)) - { - ereport(NOTICE, (errmsg("Citus partially supports CREATE DATABASE for " - "distributed databases"), - errdetail("Citus does not propagate CREATE DATABASE " - "command to workers"), - errhint("You can manually create a database and its " - "extensions on workers."))); - } - else if (IsA(parsetree, CreateRoleStmt)) - { - ereport(NOTICE, (errmsg("not propagating CREATE ROLE/USER commands to worker" - " nodes"), - errhint("Connect to worker nodes directly to manually create all" - " necessary users and roles."))); - } - - /* - * Make sure that on DROP DATABASE we terminate the background deamon - * associated with it. - */ - if (IsA(parsetree, DropdbStmt)) - { - const bool missingOK = true; - DropdbStmt *dropDbStatement = (DropdbStmt *) parsetree; - char *dbname = dropDbStatement->dbname; - Oid databaseOid = get_database_oid(dbname, missingOK); - - if (OidIsValid(databaseOid)) - { - StopMaintenanceDaemon(databaseOid); - } - } - - /* set user if needed and go ahead and run local utility using standard hook */ - if (commandMustRunAsOwner) - { - GetUserIdAndSecContext(&savedUserId, &savedSecurityContext); - SetUserIdAndSecContext(CitusExtensionOwner(), SECURITY_LOCAL_USERID_CHANGE); - } - -#if (PG_VERSION_NUM >= 100000) - pstmt->utilityStmt = parsetree; - standard_ProcessUtility(pstmt, queryString, context, - params, queryEnv, dest, completionTag); -#else - standard_ProcessUtility(parsetree, queryString, context, - params, dest, completionTag); -#endif - - - /* - * We only process CREATE TABLE ... PARTITION OF commands in the function below - * to handle the case when user creates a table as a partition of distributed table. - */ - if (IsA(parsetree, CreateStmt)) - { - CreateStmt *createStatement = (CreateStmt *) parsetree; - - ProcessCreateTableStmtPartitionOf(createStatement); - } - - /* - * We only process ALTER TABLE ... ATTACH PARTITION commands in the function below - * and distribute the partition if necessary. - */ - if (IsA(parsetree, AlterTableStmt)) - { - AlterTableStmt *alterTableStatement = (AlterTableStmt *) parsetree; - - ProcessAlterTableStmtAttachPartition(alterTableStatement); - } - - /* don't run post-process code for local commands */ - if (ddlJobs != NIL) - { - PostProcessUtility(parsetree); - } - - if (commandMustRunAsOwner) - { - SetUserIdAndSecContext(savedUserId, savedSecurityContext); - } - - /* - * Re-forming the foreign key graph relies on the command being executed - * on the local table first. However, in order to decide whether the - * command leads to an invalidation, we need to check before the command - * is being executed since we read pg_constraint table. Thus, we maintain a - * local flag and do the invalidation after multi_ProcessUtility, - * before ExecuteDistributedDDLJob(). - */ - InvalidateForeignKeyGraphForDDL(); - - /* after local command has completed, finish by executing worker DDLJobs, if any */ - if (ddlJobs != NIL) - { - ListCell *ddlJobCell = NULL; - - /* - * At this point, ALTER TABLE command has already run on the master, so we - * are checking constraints over the table with constraints already defined - * (to make the constraint check process same for ALTER TABLE and CREATE - * TABLE). If constraints do not fulfill the rules we defined, they will - * be removed and the table will return back to the state before the ALTER - * TABLE command. - */ - if (IsA(parsetree, AlterTableStmt)) - { - AlterTableStmt *alterTableStatement = (AlterTableStmt *) parsetree; - List *commandList = alterTableStatement->cmds; - ListCell *commandCell = NULL; - - foreach(commandCell, commandList) - { - AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); - AlterTableType alterTableType = command->subtype; - - if (alterTableType == AT_AddConstraint) - { - LOCKMODE lockmode = NoLock; - Oid relationId = InvalidOid; - Constraint *constraint = NULL; - - Assert(list_length(commandList) == 1); - - ErrorIfUnsupportedAlterAddConstraintStmt(alterTableStatement); - - lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); - relationId = AlterTableLookupRelation(alterTableStatement, lockmode); - - if (!OidIsValid(relationId)) - { - continue; - } - - constraint = (Constraint *) command->def; - if (constraint->contype == CONSTR_FOREIGN) - { - InvalidateForeignKeyGraph(); - } - } - else if (alterTableType == AT_AddColumn) - { - List *columnConstraints = NIL; - ListCell *columnConstraint = NULL; - Oid relationId = InvalidOid; - LOCKMODE lockmode = NoLock; - - ColumnDef *columnDefinition = (ColumnDef *) command->def; - columnConstraints = columnDefinition->constraints; - if (columnConstraints) - { - ErrorIfUnsupportedAlterAddConstraintStmt(alterTableStatement); - } - - lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); - relationId = AlterTableLookupRelation(alterTableStatement, lockmode); - if (!OidIsValid(relationId)) - { - continue; - } - - foreach(columnConstraint, columnConstraints) - { - Constraint *constraint = (Constraint *) lfirst(columnConstraint); - - if (constraint->conname == NULL && - (constraint->contype == CONSTR_PRIMARY || - constraint->contype == CONSTR_UNIQUE || - constraint->contype == CONSTR_FOREIGN || - constraint->contype == CONSTR_CHECK)) - { - ErrorUnsupportedAlterTableAddColumn(relationId, command, - constraint); - } - } - } - } - } - - foreach(ddlJobCell, ddlJobs) - { - DDLJob *ddlJob = (DDLJob *) lfirst(ddlJobCell); - - ExecuteDistributedDDLJob(ddlJob); - } - } - - /* TODO: fold VACUUM's processing into the above block */ - if (IsA(parsetree, VacuumStmt)) - { - VacuumStmt *vacuumStmt = (VacuumStmt *) parsetree; - - ProcessVacuumStmt(vacuumStmt, queryString); - } - - /* warn for CLUSTER command on distributed tables */ - if (IsA(parsetree, ClusterStmt)) - { - ClusterStmt *clusterStmt = (ClusterStmt *) parsetree; - bool showPropagationWarning = false; - - /* CLUSTER all */ - if (clusterStmt->relation == NULL) - { - showPropagationWarning = true; - } - else - { - Oid relationId = InvalidOid; - bool missingOK = false; - - relationId = RangeVarGetRelid(clusterStmt->relation, AccessShareLock, - missingOK); - - if (OidIsValid(relationId)) - { - showPropagationWarning = IsDistributedTable(relationId); - } - } - - if (showPropagationWarning) - { - ereport(WARNING, (errmsg("not propagating CLUSTER command to worker nodes"))); - } - } - - /* - * Ensure value is valid, we can't do some checks during CREATE - * EXTENSION. This is important to register some invalidation callbacks. - */ - CitusHasBeenLoaded(); -} - - -static void -ErrorUnsupportedAlterTableAddColumn(Oid relationId, AlterTableCmd *command, - Constraint *constraint) -{ - ColumnDef *columnDefinition = (ColumnDef *) command->def; - char *colName = columnDefinition->colname; - char *errMsg = - "cannot execute ADD COLUMN command with PRIMARY KEY, UNIQUE, FOREIGN and CHECK constraints"; - StringInfo errHint = makeStringInfo(); - appendStringInfo(errHint, "You can issue each command separately such as "); - appendStringInfo(errHint, - "ALTER TABLE %s ADD COLUMN %s data_type; ALTER TABLE %s ADD CONSTRAINT constraint_name ", - get_rel_name(relationId), - colName, get_rel_name(relationId)); - - if (constraint->contype == CONSTR_UNIQUE) - { - appendStringInfo(errHint, "UNIQUE (%s)", colName); - } - else if (constraint->contype == CONSTR_PRIMARY) - { - appendStringInfo(errHint, "PRIMARY KEY (%s)", colName); - } - else if (constraint->contype == CONSTR_CHECK) - { - appendStringInfo(errHint, "CHECK (check_expression)"); - } - else if (constraint->contype == CONSTR_FOREIGN) - { - RangeVar *referencedTable = constraint->pktable; - char *referencedColumn = strVal(lfirst(list_head(constraint->pk_attrs))); - Oid referencedRelationId = RangeVarGetRelid(referencedTable, NoLock, false); - - appendStringInfo(errHint, "FOREIGN KEY (%s) REFERENCES %s(%s)", colName, - get_rel_name(referencedRelationId), referencedColumn); - - if (constraint->fk_del_action == FKCONSTR_ACTION_SETNULL) - { - appendStringInfo(errHint, " %s", "ON DELETE SET NULL"); - } - else if (constraint->fk_del_action == FKCONSTR_ACTION_CASCADE) - { - appendStringInfo(errHint, " %s", "ON DELETE CASCADE"); - } - else if (constraint->fk_del_action == FKCONSTR_ACTION_SETDEFAULT) - { - appendStringInfo(errHint, " %s", "ON DELETE SET DEFAULT"); - } - else if (constraint->fk_del_action == FKCONSTR_ACTION_RESTRICT) - { - appendStringInfo(errHint, " %s", "ON DELETE RESTRICT"); - } - - if (constraint->fk_upd_action == FKCONSTR_ACTION_SETNULL) - { - appendStringInfo(errHint, " %s", "ON UPDATE SET NULL"); - } - else if (constraint->fk_upd_action == FKCONSTR_ACTION_CASCADE) - { - appendStringInfo(errHint, " %s", "ON UPDATE CASCADE"); - } - else if (constraint->fk_upd_action == FKCONSTR_ACTION_SETDEFAULT) - { - appendStringInfo(errHint, " %s", "ON UPDATE SET DEFAULT"); - } - else if (constraint->fk_upd_action == FKCONSTR_ACTION_RESTRICT) - { - appendStringInfo(errHint, " %s", "ON UPDATE RESTRICT"); - } - } - - appendStringInfo(errHint, "%s", ";"); - - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("%s", errMsg), - errhint("%s", errHint->data), - errdetail("Adding a column with a constraint in " - "one command is not supported because " - "all constraints in Citus must have " - "explicit names"))); -} - - -/* - * InvalidateForeignKeyGraphForDDL simply keeps track of whether - * the foreign key graph should be invalidated due to a DDL. - */ -static void -InvalidateForeignKeyGraphForDDL(void) -{ - if (shouldInvalidateForeignKeyGraph) - { - InvalidateForeignKeyGraph(); - - shouldInvalidateForeignKeyGraph = false; - } -} - - -/* - * IsCitusExtensionStmt returns whether a given utility is a CREATE or ALTER - * EXTENSION statement which references the citus extension. This function - * returns false for all other inputs. - */ -static bool -IsCitusExtensionStmt(Node *parsetree) -{ - char *extensionName = ""; - - if (IsA(parsetree, CreateExtensionStmt)) - { - extensionName = ((CreateExtensionStmt *) parsetree)->extname; - } - else if (IsA(parsetree, AlterExtensionStmt)) - { - extensionName = ((AlterExtensionStmt *) parsetree)->extname; - } - - return (strcmp(extensionName, "citus") == 0); -} - - -/* Is the passed in statement a transmit statement? */ -static bool -IsTransmitStmt(Node *parsetree) -{ - if (IsA(parsetree, CopyStmt)) - { - CopyStmt *copyStatement = (CopyStmt *) parsetree; - ListCell *optionCell = NULL; - - /* Extract options from the statement node tree */ - foreach(optionCell, copyStatement->options) - { - DefElem *defel = (DefElem *) lfirst(optionCell); - - if (strncmp(defel->defname, "format", NAMEDATALEN) == 0 && - strncmp(defGetString(defel), "transmit", NAMEDATALEN) == 0) - { - return true; - } - } - } - - return false; -} - - -/* - * VerifyTransmitStmt checks that the passed in command is a valid transmit - * statement. Raise ERROR if not. - * - * Note that only 'toplevel' options in the CopyStmt struct are checked, and - * that verification of the target files existance is not done here. - */ -static void -VerifyTransmitStmt(CopyStmt *copyStatement) -{ - char *fileName = NULL; - - EnsureSuperUser(); - - /* do some minimal option verification */ - if (copyStatement->relation == NULL || - copyStatement->relation->relname == NULL) - { - ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("FORMAT 'transmit' requires a target file"))); - } - - fileName = copyStatement->relation->relname; - - if (is_absolute_path(fileName)) - { - ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - (errmsg("absolute path not allowed")))); - } - else if (!path_is_relative_and_below_cwd(fileName)) - { - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - (errmsg("path must be in or below the current directory")))); - } - - if (copyStatement->filename != NULL) - { - ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("FORMAT 'transmit' only accepts STDIN/STDOUT" - " as input/output"))); - } - - if (copyStatement->query != NULL || - copyStatement->attlist != NULL || - copyStatement->is_program) - { - ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("FORMAT 'transmit' does not accept query, attribute list" - " or PROGRAM parameters "))); - } -} - - -/* - * IsCopyResultStmt determines whether the given copy statement is a - * COPY "resultkey" FROM STDIN WITH (format result) statement, which is used - * to copy query results from the coordinator into workers. - */ -static bool -IsCopyResultStmt(CopyStmt *copyStatement) -{ - ListCell *optionCell = NULL; - bool hasFormatReceive = false; - - /* extract WITH (...) options from the COPY statement */ - foreach(optionCell, copyStatement->options) - { - DefElem *defel = (DefElem *) lfirst(optionCell); - - if (strncmp(defel->defname, "format", NAMEDATALEN) == 0 && - strncmp(defGetString(defel), "result", NAMEDATALEN) == 0) - { - hasFormatReceive = true; - break; - } - } - - return hasFormatReceive; -} - - -/* - * ProcessCopyStmt handles Citus specific concerns for COPY like supporting - * COPYing from distributed tables and preventing unsupported actions. The - * function returns a modified COPY statement to be executed, or NULL if no - * further processing is needed. - * - * commandMustRunAsOwner is an output parameter used to communicate to the caller whether - * the copy statement should be executed using elevated privileges. If - * ProcessCopyStmt that is required, a call to CheckCopyPermissions will take - * care of verifying the current user's permissions before ProcessCopyStmt - * returns. - */ -static Node * -ProcessCopyStmt(CopyStmt *copyStatement, char *completionTag, bool *commandMustRunAsOwner) -{ - *commandMustRunAsOwner = false; /* make sure variable is initialized */ - - /* - * Handle special COPY "resultid" FROM STDIN WITH (format result) commands - * for sending intermediate results to workers. - */ - if (IsCopyResultStmt(copyStatement)) - { - const char *resultId = copyStatement->relation->relname; - - ReceiveQueryResultViaCopy(resultId); - - return NULL; - } - - /* - * We check whether a distributed relation is affected. For that, we need to open the - * relation. To prevent race conditions with later lookups, lock the table, and modify - * the rangevar to include the schema. - */ - if (copyStatement->relation != NULL) - { - bool isDistributedRelation = false; - bool isCopyFromWorker = IsCopyFromWorker(copyStatement); - - if (isCopyFromWorker) - { - RangeVar *relation = copyStatement->relation; - NodeAddress *masterNodeAddress = MasterNodeAddress(copyStatement); - char *nodeName = masterNodeAddress->nodeName; - int32 nodePort = masterNodeAddress->nodePort; - - CreateLocalTable(relation, nodeName, nodePort); - - /* - * We expect copy from worker to be on a distributed table; otherwise, - * it fails in CitusCopyFrom() while checking the partition method. - */ - isDistributedRelation = true; - } - else - { - bool isFrom = copyStatement->is_from; - Relation copiedRelation = NULL; - char *schemaName = NULL; - MemoryContext relationContext = NULL; - - /* consider using RangeVarGetRelidExtended to check perms before locking */ - copiedRelation = heap_openrv(copyStatement->relation, - isFrom ? RowExclusiveLock : AccessShareLock); - - isDistributedRelation = IsDistributedTable(RelationGetRelid(copiedRelation)); - - /* ensure future lookups hit the same relation */ - schemaName = get_namespace_name(RelationGetNamespace(copiedRelation)); - - /* ensure we copy string into proper context */ - relationContext = GetMemoryChunkContext(copyStatement->relation); - schemaName = MemoryContextStrdup(relationContext, schemaName); - copyStatement->relation->schemaname = schemaName; - - heap_close(copiedRelation, NoLock); - } - - if (isDistributedRelation) - { - if (copyStatement->is_from) - { - /* check permissions, we're bypassing postgres' normal checks */ - if (!isCopyFromWorker) - { - CheckCopyPermissions(copyStatement); - } - - CitusCopyFrom(copyStatement, completionTag); - return NULL; - } - else if (!copyStatement->is_from) - { - /* - * The copy code only handles SELECTs in COPY ... TO on master tables, - * as that can be done non-invasively. To handle COPY master_rel TO - * the copy statement is replaced by a generated select statement. - */ - ColumnRef *allColumns = makeNode(ColumnRef); - SelectStmt *selectStmt = makeNode(SelectStmt); - ResTarget *selectTarget = makeNode(ResTarget); - - allColumns->fields = list_make1(makeNode(A_Star)); - allColumns->location = -1; - - selectTarget->name = NULL; - selectTarget->indirection = NIL; - selectTarget->val = (Node *) allColumns; - selectTarget->location = -1; - - selectStmt->targetList = list_make1(selectTarget); - selectStmt->fromClause = list_make1(copyObject(copyStatement->relation)); - - /* replace original statement */ - copyStatement = copyObject(copyStatement); - copyStatement->relation = NULL; - copyStatement->query = (Node *) selectStmt; - } - } - } - - - if (copyStatement->filename != NULL && !copyStatement->is_program) - { - const char *filename = copyStatement->filename; - - if (CacheDirectoryElement(filename)) - { - /* - * Only superusers are allowed to copy from a file, so we have to - * become superuser to execute copies to/from files used by citus' - * query execution. - * - * XXX: This is a decidedly suboptimal solution, as that means - * that triggers, input functions, etc. run with elevated - * privileges. But this is better than not being able to run - * queries as normal user. - */ - *commandMustRunAsOwner = true; - - /* - * Have to manually check permissions here as the COPY is will be - * run as a superuser. - */ - if (copyStatement->relation != NULL) - { - CheckCopyPermissions(copyStatement); - } - - /* - * Check if we have a "COPY (query) TO filename". If we do, copy - * doesn't accept relative file paths. However, SQL tasks that get - * assigned to worker nodes have relative paths. We therefore - * convert relative paths to absolute ones here. - */ - if (copyStatement->relation == NULL && - !copyStatement->is_from && - !is_absolute_path(filename)) - { - copyStatement->filename = make_absolute_path(filename); - } - } - } - - - return (Node *) copyStatement; -} - - -/* - * ProcessCreateTableStmtPartitionOf takes CreateStmt object as a parameter but - * it only processes CREATE TABLE ... PARTITION OF statements and it checks if - * user creates the table as a partition of a distributed table. In that case, - * it distributes partition as well. Since the table itself is a partition, - * CreateDistributedTable will attach it to its parent table automatically after - * distributing it. - * - * This function does nothing if PostgreSQL's version is less then 10 and given - * CreateStmt is not a CREATE TABLE ... PARTITION OF command. - */ -static void -ProcessCreateTableStmtPartitionOf(CreateStmt *createStatement) -{ -#if (PG_VERSION_NUM >= 100000) - if (createStatement->inhRelations != NIL && createStatement->partbound != NULL) - { - RangeVar *parentRelation = linitial(createStatement->inhRelations); - bool parentMissingOk = false; - Oid parentRelationId = RangeVarGetRelid(parentRelation, NoLock, - parentMissingOk); - - /* a partition can only inherit from single parent table */ - Assert(list_length(createStatement->inhRelations) == 1); - - Assert(parentRelationId != InvalidOid); - - /* - * If a partition is being created and if its parent is a distributed - * table, we will distribute this table as well. - */ - if (IsDistributedTable(parentRelationId)) - { - bool missingOk = false; - Oid relationId = RangeVarGetRelid(createStatement->relation, NoLock, - missingOk); - Var *parentDistributionColumn = DistPartitionKey(parentRelationId); - char parentDistributionMethod = DISTRIBUTE_BY_HASH; - char *parentRelationName = generate_qualified_relation_name(parentRelationId); - bool viaDeprecatedAPI = false; - - CreateDistributedTable(relationId, parentDistributionColumn, - parentDistributionMethod, parentRelationName, - viaDeprecatedAPI); - } - } -#endif -} - - -/* - * ProcessAlterTableStmtAttachPartition takes AlterTableStmt object as parameter - * but it only processes into ALTER TABLE ... ATTACH PARTITION commands and - * distributes the partition if necessary. There are four cases to consider; - * - * Parent is not distributed, partition is not distributed: We do not need to - * do anything in this case. - * - * Parent is not distributed, partition is distributed: This can happen if - * user first distributes a table and tries to attach it to a non-distributed - * table. Non-distributed tables cannot have distributed partitions, thus we - * simply error out in this case. - * - * Parent is distributed, partition is not distributed: We should distribute - * the table and attach it to its parent in workers. CreateDistributedTable - * perform both of these operations. Thus, we will not propagate ALTER TABLE - * ... ATTACH PARTITION command to workers. - * - * Parent is distributed, partition is distributed: Partition is already - * distributed, we only need to attach it to its parent in workers. Attaching - * operation will be performed via propagating this ALTER TABLE ... ATTACH - * PARTITION command to workers. - * - * This function does nothing if PostgreSQL's version is less then 10 and given - * CreateStmt is not a ALTER TABLE ... ATTACH PARTITION OF command. - */ -static void -ProcessAlterTableStmtAttachPartition(AlterTableStmt *alterTableStatement) -{ -#if (PG_VERSION_NUM >= 100000) - List *commandList = alterTableStatement->cmds; - ListCell *commandCell = NULL; - - foreach(commandCell, commandList) - { - AlterTableCmd *alterTableCommand = (AlterTableCmd *) lfirst(commandCell); - - if (alterTableCommand->subtype == AT_AttachPartition) - { - Oid relationId = AlterTableLookupRelation(alterTableStatement, NoLock); - PartitionCmd *partitionCommand = (PartitionCmd *) alterTableCommand->def; - bool partitionMissingOk = false; - Oid partitionRelationId = RangeVarGetRelid(partitionCommand->name, NoLock, - partitionMissingOk); - - /* - * If user first distributes the table then tries to attach it to non - * distributed table, we error out. - */ - if (!IsDistributedTable(relationId) && - IsDistributedTable(partitionRelationId)) - { - char *parentRelationName = get_rel_name(partitionRelationId); - - ereport(ERROR, (errmsg("non-distributed tables cannot have " - "distributed partitions"), - errhint("Distribute the partitioned table \"%s\" " - "instead", parentRelationName))); - } - - /* if parent of this table is distributed, distribute this table too */ - if (IsDistributedTable(relationId) && - !IsDistributedTable(partitionRelationId)) - { - Var *distributionColumn = DistPartitionKey(relationId); - char distributionMethod = DISTRIBUTE_BY_HASH; - char *parentRelationName = generate_qualified_relation_name(relationId); - bool viaDeprecatedAPI = false; - - CreateDistributedTable(partitionRelationId, distributionColumn, - distributionMethod, parentRelationName, - viaDeprecatedAPI); - } - } - } -#endif -} - - -/* - * PlanIndexStmt determines whether a given CREATE INDEX statement involves - * a distributed table. If so (and if the statement does not use unsupported - * options), it modifies the input statement to ensure proper execution against - * the master node table and creates a DDLJob to encapsulate information needed - * during the worker node portion of DDL execution before returning that DDLJob - * in a List. If no distributed table is involved, this function returns NIL. - */ -static List * -PlanIndexStmt(IndexStmt *createIndexStatement, const char *createIndexCommand) -{ - List *ddlJobs = NIL; - - /* - * We first check whether a distributed relation is affected. For that, we need to - * open the relation. To prevent race conditions with later lookups, lock the table, - * and modify the rangevar to include the schema. - */ - if (createIndexStatement->relation != NULL) - { - Relation relation = NULL; - Oid relationId = InvalidOid; - bool isDistributedRelation = false; - char *namespaceName = NULL; - LOCKMODE lockmode = ShareLock; - MemoryContext relationContext = NULL; - - /* - * We don't support concurrently creating indexes for distributed - * tables, but till this point, we don't know if it is a regular or a - * distributed table. - */ - if (createIndexStatement->concurrent) - { - lockmode = ShareUpdateExclusiveLock; - } - - /* - * XXX: Consider using RangeVarGetRelidExtended with a permission - * checking callback. Right now we'll acquire the lock before having - * checked permissions, and will only fail when executing the actual - * index statements. - */ - relation = heap_openrv(createIndexStatement->relation, lockmode); - relationId = RelationGetRelid(relation); - - isDistributedRelation = IsDistributedTable(relationId); - - /* - * Before we do any further processing, fix the schema name to make sure - * that a (distributed) table with the same name does not appear on the - * search path in front of the current schema. We do this even if the - * table is not distributed, since a distributed table may appear on the - * search path by the time postgres starts processing this statement. - */ - namespaceName = get_namespace_name(RelationGetNamespace(relation)); - - /* ensure we copy string into proper context */ - relationContext = GetMemoryChunkContext(createIndexStatement->relation); - namespaceName = MemoryContextStrdup(relationContext, namespaceName); - createIndexStatement->relation->schemaname = namespaceName; - - heap_close(relation, NoLock); - - if (isDistributedRelation) - { - Oid namespaceId = InvalidOid; - Oid indexRelationId = InvalidOid; - char *indexName = createIndexStatement->idxname; - - ErrorIfUnsupportedIndexStmt(createIndexStatement); - - namespaceId = get_namespace_oid(namespaceName, false); - indexRelationId = get_relname_relid(indexName, namespaceId); - - /* if index does not exist, send the command to workers */ - if (!OidIsValid(indexRelationId)) - { - DDLJob *ddlJob = palloc0(sizeof(DDLJob)); - ddlJob->targetRelationId = relationId; - ddlJob->concurrentIndexCmd = createIndexStatement->concurrent; - ddlJob->commandString = createIndexCommand; - ddlJob->taskList = CreateIndexTaskList(relationId, createIndexStatement); - - ddlJobs = list_make1(ddlJob); - } - } - } - - return ddlJobs; -} - - -/* - * PlanDropIndexStmt determines whether a given DROP INDEX statement involves - * a distributed table. If so (and if the statement does not use unsupported - * options), it modifies the input statement to ensure proper execution against - * the master node table and creates a DDLJob to encapsulate information needed - * during the worker node portion of DDL execution before returning that DDLJob - * in a List. If no distributed table is involved, this function returns NIL. - */ -static List * -PlanDropIndexStmt(DropStmt *dropIndexStatement, const char *dropIndexCommand) -{ - List *ddlJobs = NIL; - ListCell *dropObjectCell = NULL; - Oid distributedIndexId = InvalidOid; - Oid distributedRelationId = InvalidOid; - - Assert(dropIndexStatement->removeType == OBJECT_INDEX); - - /* check if any of the indexes being dropped belong to a distributed table */ - foreach(dropObjectCell, dropIndexStatement->objects) - { - Oid indexId = InvalidOid; - Oid relationId = InvalidOid; - bool isDistributedRelation = false; - struct DropRelationCallbackState state; - uint32 rvrFlags = RVR_MISSING_OK; - LOCKMODE lockmode = AccessExclusiveLock; - - List *objectNameList = (List *) lfirst(dropObjectCell); - RangeVar *rangeVar = makeRangeVarFromNameList(objectNameList); - - /* - * We don't support concurrently dropping indexes for distributed - * tables, but till this point, we don't know if it is a regular or a - * distributed table. - */ - if (dropIndexStatement->concurrent) - { - lockmode = ShareUpdateExclusiveLock; - } - - /* - * The next few statements are based on RemoveRelations() in - * commands/tablecmds.c in Postgres source. - */ - AcceptInvalidationMessages(); - - state.relkind = RELKIND_INDEX; - state.heapOid = InvalidOid; - state.concurrent = dropIndexStatement->concurrent; - - indexId = RangeVarGetRelidInternal(rangeVar, lockmode, rvrFlags, - RangeVarCallbackForDropIndex, - (void *) &state); - - /* - * If the index does not exist, we don't do anything here, and allow - * postgres to throw appropriate error or notice message later. - */ - if (!OidIsValid(indexId)) - { - continue; - } - - relationId = IndexGetRelation(indexId, false); - isDistributedRelation = IsDistributedTable(relationId); - if (isDistributedRelation) - { - distributedIndexId = indexId; - distributedRelationId = relationId; - break; - } - } - - if (OidIsValid(distributedIndexId)) - { - DDLJob *ddlJob = palloc0(sizeof(DDLJob)); - - ErrorIfUnsupportedDropIndexStmt(dropIndexStatement); - - ddlJob->targetRelationId = distributedRelationId; - ddlJob->concurrentIndexCmd = dropIndexStatement->concurrent; - ddlJob->commandString = dropIndexCommand; - ddlJob->taskList = DropIndexTaskList(distributedRelationId, distributedIndexId, - dropIndexStatement); - - ddlJobs = list_make1(ddlJob); - } - - return ddlJobs; -} - - -/* - * PlanAlterTableStmt determines whether a given ALTER TABLE statement involves - * a distributed table. If so (and if the statement does not use unsupported - * options), it modifies the input statement to ensure proper execution against - * the master node table and creates a DDLJob to encapsulate information needed - * during the worker node portion of DDL execution before returning that DDLJob - * in a List. If no distributed table is involved, this function returns NIL. - */ -static List * -PlanAlterTableStmt(AlterTableStmt *alterTableStatement, const char *alterTableCommand) -{ - List *ddlJobs = NIL; - DDLJob *ddlJob = NULL; - LOCKMODE lockmode = 0; - Oid leftRelationId = InvalidOid; - Oid rightRelationId = InvalidOid; - char leftRelationKind; - bool isDistributedRelation = false; - List *commandList = NIL; - ListCell *commandCell = NULL; - bool executeSequentially = false; - - /* first check whether a distributed relation is affected */ - if (alterTableStatement->relation == NULL) - { - return NIL; - } - - lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); - leftRelationId = AlterTableLookupRelation(alterTableStatement, lockmode); - if (!OidIsValid(leftRelationId)) - { - return NIL; - } - - /* - * AlterTableStmt applies also to INDEX relations, and we have support for - * SET/SET storage parameters in Citus, so we might have to check for - * another relation here. - */ - leftRelationKind = get_rel_relkind(leftRelationId); - if (leftRelationKind == RELKIND_INDEX) - { - leftRelationId = IndexGetRelation(leftRelationId, false); - } - - isDistributedRelation = IsDistributedTable(leftRelationId); - if (!isDistributedRelation) - { - return NIL; - } - - /* - * The PostgreSQL parser dispatches several commands into the node type - * AlterTableStmt, from ALTER INDEX to ALTER SEQUENCE or ALTER VIEW. Here - * we have a special implementation for ALTER INDEX, and a specific error - * message in case of unsupported sub-command. - */ - if (leftRelationKind == RELKIND_INDEX) - { - ErrorIfUnsupportedAlterIndexStmt(alterTableStatement); - } - else - { - /* this function also accepts more than just RELKIND_RELATION... */ - ErrorIfUnsupportedAlterTableStmt(alterTableStatement); - } - - /* - * We check if there is a ADD/DROP FOREIGN CONSTRAINT command in sub commands list. - * If there is we assign referenced relation id to rightRelationId and we also - * set skip_validation to true to prevent PostgreSQL to verify validity of the - * foreign constraint in master. Validity will be checked in workers anyway. - */ - commandList = alterTableStatement->cmds; - - foreach(commandCell, commandList) - { - AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); - AlterTableType alterTableType = command->subtype; - - if (alterTableType == AT_AddConstraint) - { - Constraint *constraint = (Constraint *) command->def; - if (constraint->contype == CONSTR_FOREIGN) - { - /* - * We only support ALTER TABLE ADD CONSTRAINT ... FOREIGN KEY, if it is - * only subcommand of ALTER TABLE. It was already checked in - * ErrorIfUnsupportedAlterTableStmt. - */ - Assert(list_length(commandList) <= 1); - - rightRelationId = RangeVarGetRelid(constraint->pktable, lockmode, - alterTableStatement->missing_ok); - - /* - * Foreign constraint validations will be done in workers. If we do not - * set this flag, PostgreSQL tries to do additional checking when we drop - * to standard_ProcessUtility. standard_ProcessUtility tries to open new - * connections to workers to verify foreign constraints while original - * transaction is in process, which causes deadlock. - */ - constraint->skip_validation = true; - } - } - else if (alterTableType == AT_AddColumn) - { - /* - * TODO: This code path is nothing beneficial since we do not - * support ALTER TABLE %s ADD COLUMN %s [constraint] for foreign keys. - * However, the code is kept in case we fix the constraint - * creation without a name and allow foreign key creation with the mentioned - * command. - */ - ColumnDef *columnDefinition = (ColumnDef *) command->def; - List *columnConstraints = columnDefinition->constraints; - - ListCell *columnConstraint = NULL; - foreach(columnConstraint, columnConstraints) - { - Constraint *constraint = (Constraint *) lfirst(columnConstraint); - if (constraint->contype == CONSTR_FOREIGN) - { - rightRelationId = RangeVarGetRelid(constraint->pktable, lockmode, - alterTableStatement->missing_ok); - - /* - * Foreign constraint validations will be done in workers. If we do not - * set this flag, PostgreSQL tries to do additional checking when we drop - * to standard_ProcessUtility. standard_ProcessUtility tries to open new - * connections to workers to verify foreign constraints while original - * transaction is in process, which causes deadlock. - */ - constraint->skip_validation = true; - break; - } - } - } -#if (PG_VERSION_NUM >= 100000) - else if (alterTableType == AT_AttachPartition) - { - PartitionCmd *partitionCommand = (PartitionCmd *) command->def; - - /* - * We only support ALTER TABLE ATTACH PARTITION, if it is only subcommand of - * ALTER TABLE. It was already checked in ErrorIfUnsupportedAlterTableStmt. - */ - Assert(list_length(commandList) <= 1); - - rightRelationId = RangeVarGetRelid(partitionCommand->name, NoLock, false); - - /* - * Do not generate tasks if relation is distributed and the partition - * is not distributed. Because, we'll manually convert the partition into - * distributed table and co-locate with its parent. - */ - if (!IsDistributedTable(rightRelationId)) - { - return NIL; - } - } - else if (alterTableType == AT_DetachPartition) - { - PartitionCmd *partitionCommand = (PartitionCmd *) command->def; - - /* - * We only support ALTER TABLE DETACH PARTITION, if it is only subcommand of - * ALTER TABLE. It was already checked in ErrorIfUnsupportedAlterTableStmt. - */ - Assert(list_length(commandList) <= 1); - - rightRelationId = RangeVarGetRelid(partitionCommand->name, NoLock, false); - } -#endif - executeSequentially |= SetupExecutionModeForAlterTable(leftRelationId, - command); - } - - ddlJob = palloc0(sizeof(DDLJob)); - ddlJob->targetRelationId = leftRelationId; - ddlJob->concurrentIndexCmd = false; - ddlJob->commandString = alterTableCommand; - ddlJob->executeSequentially = executeSequentially; - - if (rightRelationId) - { - if (!IsDistributedTable(rightRelationId)) - { - ddlJob->taskList = NIL; - } - else - { - /* if foreign key related, use specialized task list function ... */ - ddlJob->taskList = InterShardDDLTaskList(leftRelationId, rightRelationId, - alterTableCommand); - } - } - else - { - /* ... otherwise use standard DDL task list function */ - ddlJob->taskList = DDLTaskList(leftRelationId, alterTableCommand); - } - - ddlJobs = list_make1(ddlJob); - - return ddlJobs; -} - - -/* - * PlanRenameStmt first determines whether a given rename statement involves - * a distributed table. If so (and if it is supported, i.e. renames a column), - * it creates a DDLJob to encapsulate information needed during the worker node - * portion of DDL execution before returning that DDLJob in a List. If no dis- - * tributed table is involved, this function returns NIL. - */ -static List * -PlanRenameStmt(RenameStmt *renameStmt, const char *renameCommand) -{ - Oid objectRelationId = InvalidOid; /* SQL Object OID */ - Oid tableRelationId = InvalidOid; /* Relation OID, maybe not the same. */ - bool isDistributedRelation = false; - DDLJob *ddlJob = NULL; - - /* - * We only support some of the PostgreSQL supported RENAME statements, and - * our list include only renaming table and index (related) objects. - */ - if (!IsAlterTableRenameStmt(renameStmt) && - !IsIndexRenameStmt(renameStmt) && - !IsPolicyRenameStmt(renameStmt)) - { - return NIL; - } - - /* - * The lock levels here should be same as the ones taken in - * RenameRelation(), renameatt() and RenameConstraint(). However, since all - * three statements have identical lock levels, we just use a single statement. - */ - objectRelationId = RangeVarGetRelid(renameStmt->relation, - AccessExclusiveLock, - renameStmt->missing_ok); - - /* - * If the table does not exist, don't do anything here to allow PostgreSQL - * to throw the appropriate error or notice message later. - */ - if (!OidIsValid(objectRelationId)) - { - return NIL; - } - - /* we have no planning to do unless the table is distributed */ - switch (renameStmt->renameType) - { - case OBJECT_TABLE: - case OBJECT_COLUMN: - case OBJECT_TABCONSTRAINT: - case OBJECT_POLICY: - { - /* the target object is our tableRelationId. */ - tableRelationId = objectRelationId; - break; - } - - case OBJECT_INDEX: - { - /* - * here, objRelationId points to the index relation entry, and we - * are interested into the entry of the table on which the index is - * defined. - */ - tableRelationId = IndexGetRelation(objectRelationId, false); - break; - } - - default: - - /* - * Nodes that are not supported by Citus: we pass-through to the - * main PostgreSQL executor. Any Citus-supported RenameStmt - * renameType must appear above in the switch, explicitly. - */ - return NIL; - } - - isDistributedRelation = IsDistributedTable(tableRelationId); - if (!isDistributedRelation) - { - return NIL; - } - - /* - * We might ERROR out on some commands, but only for Citus tables where - * isDistributedRelation is true. That's why this test comes this late in - * the function. - */ - ErrorIfUnsupportedRenameStmt(renameStmt); - - ddlJob = palloc0(sizeof(DDLJob)); - ddlJob->targetRelationId = tableRelationId; - ddlJob->concurrentIndexCmd = false; - ddlJob->commandString = renameCommand; - ddlJob->taskList = DDLTaskList(tableRelationId, renameCommand); - - return list_make1(ddlJob); -} - - -/* - * WorkerProcessAlterTableStmt checks and processes the alter table statement to be - * worked on the distributed table of the worker node. Currently, it only processes - * ALTER TABLE ... ADD FOREIGN KEY command to skip the validation step. - */ -static Node * -WorkerProcessAlterTableStmt(AlterTableStmt *alterTableStatement, - const char *alterTableCommand) -{ - LOCKMODE lockmode = 0; - Oid leftRelationId = InvalidOid; - bool isDistributedRelation = false; - List *commandList = NIL; - ListCell *commandCell = NULL; - - /* first check whether a distributed relation is affected */ - if (alterTableStatement->relation == NULL) - { - return (Node *) alterTableStatement; - } - - lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); - leftRelationId = AlterTableLookupRelation(alterTableStatement, lockmode); - if (!OidIsValid(leftRelationId)) - { - return (Node *) alterTableStatement; - } - - isDistributedRelation = IsDistributedTable(leftRelationId); - if (!isDistributedRelation) - { - return (Node *) alterTableStatement; - } - - /* - * We check if there is a ADD FOREIGN CONSTRAINT command in sub commands list. - * If there is we assign referenced releation id to rightRelationId and we also - * set skip_validation to true to prevent PostgreSQL to verify validity of the - * foreign constraint in master. Validity will be checked in workers anyway. - */ - commandList = alterTableStatement->cmds; - - foreach(commandCell, commandList) - { - AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); - AlterTableType alterTableType = command->subtype; - - if (alterTableType == AT_AddConstraint) - { - Constraint *constraint = (Constraint *) command->def; - if (constraint->contype == CONSTR_FOREIGN) - { - /* foreign constraint validations will be done in shards. */ - constraint->skip_validation = true; - } - } - } - - return (Node *) alterTableStatement; -} - - -/* - * PlanAlterObjectSchemaStmt determines whether a given ALTER ... SET SCHEMA - * statement involves a distributed table and issues a warning if so. Because - * we do not support distributed ALTER ... SET SCHEMA, this function always - * returns NIL. - */ -static List * -PlanAlterObjectSchemaStmt(AlterObjectSchemaStmt *alterObjectSchemaStmt, - const char *alterObjectSchemaCommand) -{ - Oid relationId = InvalidOid; - - if (alterObjectSchemaStmt->relation == NULL) - { - return NIL; - } - - relationId = RangeVarGetRelid(alterObjectSchemaStmt->relation, - AccessExclusiveLock, - alterObjectSchemaStmt->missing_ok); - - /* first check whether a distributed relation is affected */ - if (!OidIsValid(relationId) || !IsDistributedTable(relationId)) - { - return NIL; - } - - /* emit a warning if a distributed relation is affected */ - ereport(WARNING, (errmsg("not propagating ALTER ... SET SCHEMA commands to " - "worker nodes"), - errhint("Connect to worker nodes directly to manually " - "change schemas of affected objects."))); - - return NIL; -} - - -/* - * ProcessVacuumStmt processes vacuum statements that may need propagation to - * distributed tables. If a VACUUM or ANALYZE command references a distributed - * table, it is propagated to all involved nodes; otherwise, this function will - * immediately exit after some error checking. - * - * Unlike most other Process functions within this file, this function does not - * return a modified parse node, as it is expected that the local VACUUM or - * ANALYZE has already been processed. - */ -static void -ProcessVacuumStmt(VacuumStmt *vacuumStmt, const char *vacuumCommand) -{ - int relationIndex = 0; - bool distributedVacuumStmt = false; - List *vacuumRelationList = ExtractVacuumTargetRels(vacuumStmt); - ListCell *vacuumRelationCell = NULL; - List *relationIdList = NIL; - ListCell *relationIdCell = NULL; - LOCKMODE lockMode = (vacuumStmt->options & VACOPT_FULL) ? AccessExclusiveLock : - ShareUpdateExclusiveLock; - int executedVacuumCount = 0; - - foreach(vacuumRelationCell, vacuumRelationList) - { - RangeVar *vacuumRelation = (RangeVar *) lfirst(vacuumRelationCell); - Oid relationId = RangeVarGetRelid(vacuumRelation, lockMode, false); - relationIdList = lappend_oid(relationIdList, relationId); - } - - distributedVacuumStmt = IsDistributedVacuumStmt(vacuumStmt, relationIdList); - if (!distributedVacuumStmt) - { - return; - } - - /* execute vacuum on distributed tables */ - foreach(relationIdCell, relationIdList) - { - Oid relationId = lfirst_oid(relationIdCell); - if (IsDistributedTable(relationId)) - { - List *vacuumColumnList = NIL; - List *taskList = NIL; - - /* - * VACUUM commands cannot run inside a transaction block, so we use - * the "bare" commit protocol without BEGIN/COMMIT. However, ANALYZE - * commands can run inside a transaction block. Notice that we do this - * once even if there are multiple distributed tables to be vacuumed. - */ - if (executedVacuumCount == 0 && (vacuumStmt->options & VACOPT_VACUUM) != 0) - { - /* save old commit protocol to restore at xact end */ - Assert(SavedMultiShardCommitProtocol == COMMIT_PROTOCOL_BARE); - SavedMultiShardCommitProtocol = MultiShardCommitProtocol; - MultiShardCommitProtocol = COMMIT_PROTOCOL_BARE; - } - - vacuumColumnList = VacuumColumnList(vacuumStmt, relationIndex); - taskList = VacuumTaskList(relationId, vacuumStmt->options, vacuumColumnList); - - ExecuteModifyTasksWithoutResults(taskList); - - executedVacuumCount++; - } - relationIndex++; - } -} - - -/* - * IsSupportedDistributedVacuumStmt returns whether distributed execution of a - * given VacuumStmt is supported. The provided relationId list represents - * the list of tables targeted by the provided statement. - * - * Returns true if the statement requires distributed execution and returns - * false otherwise. - */ -static bool -IsDistributedVacuumStmt(VacuumStmt *vacuumStmt, List *vacuumRelationIdList) -{ - const char *stmtName = (vacuumStmt->options & VACOPT_VACUUM) ? "VACUUM" : "ANALYZE"; - bool distributeStmt = false; - ListCell *relationIdCell = NULL; - int distributedRelationCount = 0; - int vacuumedRelationCount = 0; - - /* - * No table in the vacuum statement means vacuuming all relations - * which is not supported by citus. - */ - vacuumedRelationCount = list_length(vacuumRelationIdList); - if (vacuumedRelationCount == 0) - { - /* WARN for unqualified VACUUM commands */ - ereport(WARNING, (errmsg("not propagating %s command to worker nodes", stmtName), - errhint("Provide a specific table in order to %s " - "distributed tables.", stmtName))); - } - - foreach(relationIdCell, vacuumRelationIdList) - { - Oid relationId = lfirst_oid(relationIdCell); - if (OidIsValid(relationId) && IsDistributedTable(relationId)) - { - distributedRelationCount++; - } - } - - if (distributedRelationCount == 0) - { - /* nothing to do here */ - } - else if (!EnableDDLPropagation) - { - /* WARN if DDL propagation is not enabled */ - ereport(WARNING, (errmsg("not propagating %s command to worker nodes", stmtName), - errhint("Set citus.enable_ddl_propagation to true in order to " - "send targeted %s commands to worker nodes.", - stmtName))); - } - else - { - distributeStmt = true; - } - - return distributeStmt; -} - - -/* - * VacuumTaskList returns a list of tasks to be executed as part of processing - * a VacuumStmt which targets a distributed relation. - */ -static List * -VacuumTaskList(Oid relationId, int vacuumOptions, List *vacuumColumnList) -{ - List *taskList = NIL; - List *shardIntervalList = NIL; - ListCell *shardIntervalCell = NULL; - uint64 jobId = INVALID_JOB_ID; - int taskId = 1; - StringInfo vacuumString = DeparseVacuumStmtPrefix(vacuumOptions); - const char *columnNames = NULL; - const int vacuumPrefixLen = vacuumString->len; - Oid schemaId = get_rel_namespace(relationId); - char *schemaName = get_namespace_name(schemaId); - char *tableName = get_rel_name(relationId); - - columnNames = DeparseVacuumColumnNames(vacuumColumnList); - - /* - * We obtain ShareUpdateExclusiveLock here to not conflict with INSERT's - * RowExclusiveLock. However if VACUUM FULL is used, we already obtain - * AccessExclusiveLock before reaching to that point and INSERT's will be - * blocked anyway. This is inline with PostgreSQL's own behaviour. - */ - LockRelationOid(relationId, ShareUpdateExclusiveLock); - - shardIntervalList = LoadShardIntervalList(relationId); - - /* grab shard lock before getting placement list */ - LockShardListMetadata(shardIntervalList, ShareLock); - - foreach(shardIntervalCell, shardIntervalList) - { - ShardInterval *shardInterval = (ShardInterval *) lfirst(shardIntervalCell); - uint64 shardId = shardInterval->shardId; - Task *task = NULL; - - char *shardName = pstrdup(tableName); - AppendShardIdToName(&shardName, shardInterval->shardId); - shardName = quote_qualified_identifier(schemaName, shardName); - - vacuumString->len = vacuumPrefixLen; - appendStringInfoString(vacuumString, shardName); - appendStringInfoString(vacuumString, columnNames); - - task = CitusMakeNode(Task); - task->jobId = jobId; - task->taskId = taskId++; - task->taskType = VACUUM_ANALYZE_TASK; - task->queryString = pstrdup(vacuumString->data); - task->dependedTaskList = NULL; - task->replicationModel = REPLICATION_MODEL_INVALID; - task->anchorShardId = shardId; - task->taskPlacementList = FinalizedShardPlacementList(shardId); - - taskList = lappend(taskList, task); - } - - return taskList; -} - - -/* - * DeparseVacuumStmtPrefix returns a StringInfo appropriate for use as a prefix - * during distributed execution of a VACUUM or ANALYZE statement. Callers may - * reuse this prefix within a loop to generate shard-specific VACUUM or ANALYZE - * statements. - */ -static StringInfo -DeparseVacuumStmtPrefix(int vacuumFlags) -{ - StringInfo vacuumPrefix = makeStringInfo(); - const int unsupportedFlags PG_USED_FOR_ASSERTS_ONLY = ~( - VACOPT_ANALYZE | - VACOPT_DISABLE_PAGE_SKIPPING | - VACOPT_FREEZE | - VACOPT_FULL | - VACOPT_VERBOSE - ); - - /* determine actual command and block out its bit */ - if (vacuumFlags & VACOPT_VACUUM) - { - appendStringInfoString(vacuumPrefix, "VACUUM "); - vacuumFlags &= ~VACOPT_VACUUM; - } - else - { - appendStringInfoString(vacuumPrefix, "ANALYZE "); - vacuumFlags &= ~VACOPT_ANALYZE; - - if (vacuumFlags & VACOPT_VERBOSE) - { - appendStringInfoString(vacuumPrefix, "VERBOSE "); - vacuumFlags &= ~VACOPT_VERBOSE; - } - } - - /* unsupported flags should have already been rejected */ - Assert((vacuumFlags & unsupportedFlags) == 0); - - /* if no flags remain, exit early */ - if (vacuumFlags == 0) - { - return vacuumPrefix; - } - - /* otherwise, handle options */ - appendStringInfoChar(vacuumPrefix, '('); - - if (vacuumFlags & VACOPT_ANALYZE) - { - appendStringInfoString(vacuumPrefix, "ANALYZE,"); - } - - if (vacuumFlags & VACOPT_DISABLE_PAGE_SKIPPING) - { - appendStringInfoString(vacuumPrefix, "DISABLE_PAGE_SKIPPING,"); - } - - if (vacuumFlags & VACOPT_FREEZE) - { - appendStringInfoString(vacuumPrefix, "FREEZE,"); - } - - if (vacuumFlags & VACOPT_FULL) - { - appendStringInfoString(vacuumPrefix, "FULL,"); - } - - if (vacuumFlags & VACOPT_VERBOSE) - { - appendStringInfoString(vacuumPrefix, "VERBOSE,"); - } - - vacuumPrefix->data[vacuumPrefix->len - 1] = ')'; - - appendStringInfoChar(vacuumPrefix, ' '); - - return vacuumPrefix; -} - - -/* - * DeparseVacuumColumnNames joins the list of strings using commas as a - * delimiter. The whole thing is placed in parenthesis and set off with a - * single space in order to facilitate appending it to the end of any VACUUM - * or ANALYZE command which uses explicit column names. If the provided list - * is empty, this function returns an empty string to keep the calling code - * simplest. - */ -static char * -DeparseVacuumColumnNames(List *columnNameList) -{ - StringInfo columnNames = makeStringInfo(); - ListCell *columnNameCell = NULL; - - if (columnNameList == NIL) - { - return columnNames->data; - } - - appendStringInfoString(columnNames, " ("); - - foreach(columnNameCell, columnNameList) - { - char *columnName = strVal(lfirst(columnNameCell)); - - appendStringInfo(columnNames, "%s,", columnName); - } - - columnNames->data[columnNames->len - 1] = ')'; - - return columnNames->data; -} - - -/* - * ErrorIfUnstableCreateOrAlterExtensionStmt compares CITUS_EXTENSIONVERSION - * and version given CREATE/ALTER EXTENSION statement will create/update to. If - * they are not same in major or minor version numbers, this function errors - * out. It ignores the schema version. - */ -static void -ErrorIfUnstableCreateOrAlterExtensionStmt(Node *parsetree) -{ - char *newExtensionVersion = ExtractNewExtensionVersion(parsetree); - - if (newExtensionVersion != NULL) - { - /* explicit version provided in CREATE or ALTER EXTENSION UPDATE; verify */ - if (!MajorVersionsCompatible(newExtensionVersion, CITUS_EXTENSIONVERSION)) - { - ereport(ERROR, (errmsg("specified version incompatible with loaded " - "Citus library"), - errdetail("Loaded library requires %s, but %s was specified.", - CITUS_MAJORVERSION, newExtensionVersion), - errhint("If a newer library is present, restart the database " - "and try the command again."))); - } - } - else - { - /* - * No version was specified, so PostgreSQL will use the default_version - * from the citus.control file. - */ - CheckAvailableVersion(ERROR); - } -} - - -/* - * ExtractNewExtensionVersion returns the new extension version specified by - * a CREATE or ALTER EXTENSION statement. Other inputs are not permitted. This - * function returns NULL for statements with no explicit version specified. - */ -static char * -ExtractNewExtensionVersion(Node *parsetree) -{ - char *newVersion = NULL; - List *optionsList = NIL; - ListCell *optionsCell = NULL; - - if (IsA(parsetree, CreateExtensionStmt)) - { - optionsList = ((CreateExtensionStmt *) parsetree)->options; - } - else if (IsA(parsetree, AlterExtensionStmt)) - { - optionsList = ((AlterExtensionStmt *) parsetree)->options; - } - else - { - /* input must be one of the two above types */ - Assert(false); - } - - foreach(optionsCell, optionsList) - { - DefElem *defElement = (DefElem *) lfirst(optionsCell); - if (strncmp(defElement->defname, "new_version", NAMEDATALEN) == 0) - { - newVersion = strVal(defElement->arg); - break; - } - } - - return newVersion; -} - - -/* - * ErrorIfUnsupportedIndexStmt checks if the corresponding index statement is - * supported for distributed tables and errors out if it is not. - */ -static void -ErrorIfUnsupportedIndexStmt(IndexStmt *createIndexStatement) -{ - char *indexRelationName = createIndexStatement->idxname; - if (indexRelationName == NULL) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("creating index without a name on a distributed table is " - "currently unsupported"))); - } - - if (createIndexStatement->tableSpace != NULL) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("specifying tablespaces with CREATE INDEX statements is " - "currently unsupported"))); - } - - if (createIndexStatement->unique) - { - RangeVar *relation = createIndexStatement->relation; - bool missingOk = false; - - /* caller uses ShareLock for non-concurrent indexes, use the same lock here */ - LOCKMODE lockMode = ShareLock; - Oid relationId = RangeVarGetRelid(relation, lockMode, missingOk); - Var *partitionKey = DistPartitionKey(relationId); - char partitionMethod = PartitionMethod(relationId); - List *indexParameterList = NIL; - ListCell *indexParameterCell = NULL; - bool indexContainsPartitionColumn = false; - - /* - * Reference tables do not have partition key, and unique constraints - * are allowed for them. Thus, we added a short-circuit for reference tables. - */ - if (partitionMethod == DISTRIBUTE_BY_NONE) - { - return; - } - - if (partitionMethod == DISTRIBUTE_BY_APPEND) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("creating unique indexes on append-partitioned tables " - "is currently unsupported"))); - } - - indexParameterList = createIndexStatement->indexParams; - foreach(indexParameterCell, indexParameterList) - { - IndexElem *indexElement = (IndexElem *) lfirst(indexParameterCell); - char *columnName = indexElement->name; - AttrNumber attributeNumber = InvalidAttrNumber; - - /* column name is null for index expressions, skip it */ - if (columnName == NULL) - { - continue; - } - - attributeNumber = get_attnum(relationId, columnName); - if (attributeNumber == partitionKey->varattno) - { - indexContainsPartitionColumn = true; - } - } - - if (!indexContainsPartitionColumn) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("creating unique indexes on non-partition " - "columns is currently unsupported"))); - } - } -} - - -/* - * ErrorIfUnsupportedDropIndexStmt checks if the corresponding drop index statement is - * supported for distributed tables and errors out if it is not. - */ -static void -ErrorIfUnsupportedDropIndexStmt(DropStmt *dropIndexStatement) -{ - Assert(dropIndexStatement->removeType == OBJECT_INDEX); - - if (list_length(dropIndexStatement->objects) > 1) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot drop multiple distributed objects in a " - "single command"), - errhint("Try dropping each object in a separate DROP " - "command."))); - } -} - - -/* - * ErrorIfUnsupportedAlterTableStmt checks if the corresponding alter table - * statement is supported for distributed tables and errors out if it is not. - * Currently, only the following commands are supported. - * - * ALTER TABLE ADD|DROP COLUMN - * ALTER TABLE ALTER COLUMN SET DATA TYPE - * ALTER TABLE SET|DROP NOT NULL - * ALTER TABLE SET|DROP DEFAULT - * ALTER TABLE ADD|DROP CONSTRAINT - * ALTER TABLE REPLICA IDENTITY - * ALTER TABLE SET () - * ALTER TABLE RESET () - */ -static void -ErrorIfUnsupportedAlterTableStmt(AlterTableStmt *alterTableStatement) -{ - List *commandList = alterTableStatement->cmds; - ListCell *commandCell = NULL; - - /* error out if any of the subcommands are unsupported */ - foreach(commandCell, commandList) - { - AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); - AlterTableType alterTableType = command->subtype; - - switch (alterTableType) - { - case AT_AddColumn: - { - if (IsA(command->def, ColumnDef)) - { - ColumnDef *column = (ColumnDef *) command->def; - - /* - * Check for SERIAL pseudo-types. The structure of this - * check is copied from transformColumnDefinition. - */ - if (column->typeName && list_length(column->typeName->names) == 1 && - !column->typeName->pct_type) - { - char *typeName = strVal(linitial(column->typeName->names)); - - if (strcmp(typeName, "smallserial") == 0 || - strcmp(typeName, "serial2") == 0 || - strcmp(typeName, "serial") == 0 || - strcmp(typeName, "serial4") == 0 || - strcmp(typeName, "bigserial") == 0 || - strcmp(typeName, "serial8") == 0) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot execute ADD COLUMN commands " - "involving serial pseudotypes"))); - } - } - } - - break; - } - - case AT_DropColumn: - case AT_ColumnDefault: - case AT_AlterColumnType: - case AT_DropNotNull: - { - if (AlterInvolvesPartitionColumn(alterTableStatement, command)) - { - ereport(ERROR, (errmsg("cannot execute ALTER TABLE command " - "involving partition column"))); - } - break; - } - - case AT_AddConstraint: - { - Constraint *constraint = (Constraint *) command->def; - - /* we only allow constraints if they are only subcommand */ - if (commandList->length > 1) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot execute ADD CONSTRAINT command with " - "other subcommands"), - errhint("You can issue each subcommand separately"))); - } - - /* - * We will use constraint name in each placement by extending it at - * workers. Therefore we require it to be exist. - */ - if (constraint->conname == NULL) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot create constraint without a name on a " - "distributed table"))); - } - - break; - } - -#if (PG_VERSION_NUM >= 100000) - case AT_AttachPartition: - { - Oid relationId = AlterTableLookupRelation(alterTableStatement, - NoLock); - PartitionCmd *partitionCommand = (PartitionCmd *) command->def; - bool missingOK = false; - Oid partitionRelationId = RangeVarGetRelid(partitionCommand->name, - NoLock, missingOK); - - /* we only allow partitioning commands if they are only subcommand */ - if (commandList->length > 1) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot execute ATTACH PARTITION command " - "with other subcommands"), - errhint("You can issue each subcommand " - "separately."))); - } - - if (IsDistributedTable(partitionRelationId) && - !TablesColocated(relationId, partitionRelationId)) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("distributed tables cannot have " - "non-colocated distributed tables as a " - "partition "))); - } - - break; - } - - case AT_DetachPartition: - { - /* we only allow partitioning commands if they are only subcommand */ - if (commandList->length > 1) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot execute DETACH PARTITION command " - "with other subcommands"), - errhint("You can issue each subcommand " - "separately."))); - } - - break; - } - -#endif - case AT_DropConstraint: - { - LOCKMODE lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); - Oid relationId = AlterTableLookupRelation(alterTableStatement, lockmode); - - if (!OidIsValid(relationId)) - { - return; - } - - if (ConstraintIsAForeignKey(command->name, relationId)) - { - shouldInvalidateForeignKeyGraph = true; - } - - break; - } - - case AT_SetNotNull: - case AT_EnableTrigAll: - case AT_DisableTrigAll: - case AT_ReplicaIdentity: - { - /* - * We will not perform any special check for ALTER TABLE DROP CONSTRAINT - * , ALTER TABLE .. ALTER COLUMN .. SET NOT NULL and ALTER TABLE ENABLE/ - * DISABLE TRIGGER ALL, ALTER TABLE .. REPLICA IDENTITY .. - */ - break; - } - - case AT_SetRelOptions: /* SET (...) */ - case AT_ResetRelOptions: /* RESET (...) */ - case AT_ReplaceRelOptions: /* replace entire option list */ - { - /* this command is supported by Citus */ - break; - } - - default: - { - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("alter table command is currently unsupported"), - errdetail("Only ADD|DROP COLUMN, SET|DROP NOT NULL, " - "SET|DROP DEFAULT, ADD|DROP CONSTRAINT, " - "SET (), RESET (), " - "ATTACH|DETACH PARTITION and TYPE subcommands " - "are supported."))); - } - } - } -} - - -/* - * ErrorIfUnsupportedAlterIndexStmt checks if the corresponding alter index - * statement is supported for distributed tables and errors out if it is not. - * Currently, only the following commands are supported. - * - * ALTER INDEX SET () - * ALTER INDEX RESET () - */ -static void -ErrorIfUnsupportedAlterIndexStmt(AlterTableStmt *alterTableStatement) -{ - List *commandList = alterTableStatement->cmds; - ListCell *commandCell = NULL; - - /* error out if any of the subcommands are unsupported */ - foreach(commandCell, commandList) - { - AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); - AlterTableType alterTableType = command->subtype; - - switch (alterTableType) - { - case AT_SetRelOptions: /* SET (...) */ - case AT_ResetRelOptions: /* RESET (...) */ - case AT_ReplaceRelOptions: /* replace entire option list */ - { - /* this command is supported by Citus */ - break; - } - - /* unsupported create index statements */ - case AT_SetTableSpace: - default: - { - ereport(ERROR, - (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("alter index ... set tablespace ... " - "is currently unsupported"), - errdetail("Only RENAME TO, SET (), and RESET () " - "are supported."))); - return; /* keep compiler happy */ - } - } - } -} - - -/* - * ErrorIfDropPartitionColumn checks if any subcommands of the given alter table - * command is a DROP COLUMN command which drops the partition column of a distributed - * table. If there is such a subcommand, this function errors out. - */ -static void -ErrorIfAlterDropsPartitionColumn(AlterTableStmt *alterTableStatement) -{ - LOCKMODE lockmode = 0; - Oid leftRelationId = InvalidOid; - bool isDistributedRelation = false; - List *commandList = alterTableStatement->cmds; - ListCell *commandCell = NULL; - - /* first check whether a distributed relation is affected */ - if (alterTableStatement->relation == NULL) - { - return; - } - - lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); - leftRelationId = AlterTableLookupRelation(alterTableStatement, lockmode); - if (!OidIsValid(leftRelationId)) - { - return; - } - - isDistributedRelation = IsDistributedTable(leftRelationId); - if (!isDistributedRelation) - { - return; - } - - /* then check if any of subcommands drop partition column.*/ - foreach(commandCell, commandList) - { - AlterTableCmd *command = (AlterTableCmd *) lfirst(commandCell); - AlterTableType alterTableType = command->subtype; - if (alterTableType == AT_DropColumn) - { - if (AlterInvolvesPartitionColumn(alterTableStatement, command)) - { - ereport(ERROR, (errmsg("cannot execute ALTER TABLE command " - "dropping partition column"))); - } - } - } -} - - -/* - * ErrorIfUnsopprtedAlterAddConstraintStmt runs the constraint checks on distributed - * table using the same logic with create_distributed_table. - */ -static void -ErrorIfUnsupportedAlterAddConstraintStmt(AlterTableStmt *alterTableStatement) -{ - LOCKMODE lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); - Oid relationId = AlterTableLookupRelation(alterTableStatement, lockmode); - char distributionMethod = PartitionMethod(relationId); - Var *distributionColumn = DistPartitionKey(relationId); - uint32 colocationId = TableColocationId(relationId); - Relation relation = relation_open(relationId, ExclusiveLock); - - ErrorIfUnsupportedConstraint(relation, distributionMethod, distributionColumn, - colocationId); - relation_close(relation, NoLock); -} - - -/* - * ErrorIfUnsupportedConstraint run checks related to unique index / exclude - * constraints. - * - * The function skips the uniqeness checks for reference tables (i.e., distribution - * method is 'none'). - * - * Forbid UNIQUE, PRIMARY KEY, or EXCLUDE constraints on append partitioned - * tables, since currently there is no way of enforcing uniqueness for - * overlapping shards. - * - * Similarly, do not allow such constraints if they do not include partition - * column. This check is important for two reasons: - * i. First, currently Citus does not enforce uniqueness constraint on multiple - * shards. - * ii. Second, INSERT INTO .. ON CONFLICT (i.e., UPSERT) queries can be executed - * with no further check for constraints. - */ -void -ErrorIfUnsupportedConstraint(Relation relation, char distributionMethod, - Var *distributionColumn, uint32 colocationId) -{ - char *relationName = NULL; - List *indexOidList = NULL; - ListCell *indexOidCell = NULL; - - /* - * We first perform check for foreign constraints. It is important to do this check - * before next check, because other types of constraints are allowed on reference - * tables and we return early for those constraints thanks to next check. Therefore, - * for reference tables, we first check for foreing constraints and if they are OK, - * we do not error out for other types of constraints. - */ - ErrorIfUnsupportedForeignConstraint(relation, distributionMethod, distributionColumn, - colocationId); - - /* - * Citus supports any kind of uniqueness constraints for reference tables - * given that they only consist of a single shard and we can simply rely on - * Postgres. - */ - if (distributionMethod == DISTRIBUTE_BY_NONE) - { - return; - } - - relationName = RelationGetRelationName(relation); - indexOidList = RelationGetIndexList(relation); - - foreach(indexOidCell, indexOidList) - { - Oid indexOid = lfirst_oid(indexOidCell); - Relation indexDesc = index_open(indexOid, RowExclusiveLock); - IndexInfo *indexInfo = NULL; - AttrNumber *attributeNumberArray = NULL; - bool hasDistributionColumn = false; - int attributeCount = 0; - int attributeIndex = 0; - - /* extract index key information from the index's pg_index info */ - indexInfo = BuildIndexInfo(indexDesc); - - /* only check unique indexes and exclusion constraints. */ - if (indexInfo->ii_Unique == false && indexInfo->ii_ExclusionOps == NULL) - { - index_close(indexDesc, NoLock); - continue; - } - - /* - * Citus cannot enforce uniqueness/exclusion constraints with overlapping shards. - * Thus, emit a warning for unique indexes and exclusion constraints on - * append partitioned tables. - */ - if (distributionMethod == DISTRIBUTE_BY_APPEND) - { - ereport(WARNING, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("table \"%s\" has a UNIQUE or EXCLUDE constraint", - relationName), - errdetail("UNIQUE constraints, EXCLUDE constraints, " - "and PRIMARY KEYs on " - "append-partitioned tables cannot be enforced."), - errhint("Consider using hash partitioning."))); - } - - attributeCount = indexInfo->ii_NumIndexAttrs; - attributeNumberArray = IndexInfoAttributeNumberArray(indexInfo); - - for (attributeIndex = 0; attributeIndex < attributeCount; attributeIndex++) - { - AttrNumber attributeNumber = attributeNumberArray[attributeIndex]; - bool uniqueConstraint = false; - bool exclusionConstraintWithEquality = false; - - if (distributionColumn->varattno != attributeNumber) - { - continue; - } - - uniqueConstraint = indexInfo->ii_Unique; - exclusionConstraintWithEquality = (indexInfo->ii_ExclusionOps != NULL && - OperatorImplementsEquality( - indexInfo->ii_ExclusionOps[ - attributeIndex])); - - if (uniqueConstraint || exclusionConstraintWithEquality) - { - hasDistributionColumn = true; - break; - } - } - - if (!hasDistributionColumn) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot create constraint on \"%s\"", - relationName), - errdetail("Distributed relations cannot have UNIQUE, " - "EXCLUDE, or PRIMARY KEY constraints that do not " - "include the partition column (with an equality " - "operator if EXCLUDE)."))); - } - - index_close(indexDesc, NoLock); - } -} - - -/* - * ErrorIfUnsupportedSeqStmt errors out if the provided create sequence - * statement specifies a distributed table in its OWNED BY clause. - */ -static void -ErrorIfUnsupportedSeqStmt(CreateSeqStmt *createSeqStmt) -{ - Oid ownedByTableId = InvalidOid; - - /* create is easy: just prohibit any distributed OWNED BY */ - if (OptionsSpecifyOwnedBy(createSeqStmt->options, &ownedByTableId)) - { - if (IsDistributedTable(ownedByTableId)) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot create sequences that specify a distributed " - "table in their OWNED BY option"), - errhint("Use a sequence in a distributed table by specifying " - "a serial column type before creating any shards."))); - } - } -} - - -/* - * ErrorIfDistributedAlterSeqOwnedBy errors out if the provided alter sequence - * statement attempts to change the owned by property of a distributed sequence - * or attempt to change a local sequence to be owned by a distributed table. - */ -static void -ErrorIfDistributedAlterSeqOwnedBy(AlterSeqStmt *alterSeqStmt) -{ - Oid sequenceId = RangeVarGetRelid(alterSeqStmt->sequence, AccessShareLock, - alterSeqStmt->missing_ok); - bool sequenceOwned = false; - Oid ownedByTableId = InvalidOid; - Oid newOwnedByTableId = InvalidOid; - int32 ownedByColumnId = 0; - bool hasDistributedOwner = false; - - /* alter statement referenced nonexistent sequence; return */ - if (sequenceId == InvalidOid) - { - return; - } - -#if (PG_VERSION_NUM >= 100000) - sequenceOwned = sequenceIsOwned(sequenceId, DEPENDENCY_AUTO, &ownedByTableId, - &ownedByColumnId); - if (!sequenceOwned) - { - sequenceOwned = sequenceIsOwned(sequenceId, DEPENDENCY_INTERNAL, &ownedByTableId, - &ownedByColumnId); - } -#else - sequenceOwned = sequenceIsOwned(sequenceId, &ownedByTableId, &ownedByColumnId); -#endif - - /* see whether the sequence is already owned by a distributed table */ - if (sequenceOwned) - { - hasDistributedOwner = IsDistributedTable(ownedByTableId); - } - - if (OptionsSpecifyOwnedBy(alterSeqStmt->options, &newOwnedByTableId)) - { - /* if a distributed sequence tries to change owner, error */ - if (hasDistributedOwner && ownedByTableId != newOwnedByTableId) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot alter OWNED BY option of a sequence " - "already owned by a distributed table"))); - } - else if (!hasDistributedOwner && IsDistributedTable(newOwnedByTableId)) - { - /* and don't let local sequences get a distributed OWNED BY */ - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot associate an existing sequence with a " - "distributed table"), - errhint("Use a sequence in a distributed table by specifying " - "a serial column type before creating any shards."))); - } - } -} - - -/* - * ErrorIfUnsupportedTruncateStmt errors out if the command attempts to - * truncate a distributed foreign table. - */ -static void -ErrorIfUnsupportedTruncateStmt(TruncateStmt *truncateStatement) -{ - List *relationList = truncateStatement->relations; - ListCell *relationCell = NULL; - foreach(relationCell, relationList) - { - RangeVar *rangeVar = (RangeVar *) lfirst(relationCell); - Oid relationId = RangeVarGetRelid(rangeVar, NoLock, true); - char relationKind = get_rel_relkind(relationId); - if (IsDistributedTable(relationId) && - relationKind == RELKIND_FOREIGN_TABLE) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("truncating distributed foreign tables is " - "currently unsupported"), - errhint("Use master_drop_all_shards to remove " - "foreign table's shards."))); - } - } -} - - -/* - * ProcessTruncateStatement handles few things that should be - * done before standard process utility is called for truncate - * command. - */ -static void -ProcessTruncateStatement(TruncateStmt *truncateStatement) -{ - EnsurePartitionTableNotReplicatedForTruncate(truncateStatement); - ExecuteTruncateStmtSequentialIfNecessary(truncateStatement); - LockTruncatedRelationMetadataInWorkers(truncateStatement); -} - - -/* - * ExecuteTruncateStmtSequentialIfNecessary decides if the TRUNCATE stmt needs - * to run sequential. If so, it calls SetLocalMultiShardModifyModeToSequential(). - * - * If a reference table which has a foreign key from a distributed table is truncated - * we need to execute the command sequentially to avoid self-deadlock. - */ -static void -ExecuteTruncateStmtSequentialIfNecessary(TruncateStmt *command) -{ - List *relationList = command->relations; - ListCell *relationCell = NULL; - bool failOK = false; - - foreach(relationCell, relationList) - { - RangeVar *rangeVar = (RangeVar *) lfirst(relationCell); - Oid relationId = RangeVarGetRelid(rangeVar, NoLock, failOK); - - if (IsDistributedTable(relationId) && - PartitionMethod(relationId) == DISTRIBUTE_BY_NONE && - TableReferenced(relationId)) - { - char *relationName = get_rel_name(relationId); - - ereport(DEBUG1, (errmsg("switching to sequential query execution mode"), - errdetail( - "Reference relation \"%s\" is modified, which might lead " - "to data inconsistencies or distributed deadlocks via " - "parallel accesses to hash distributed relations due to " - "foreign keys. Any parallel modification to " - "those hash distributed relations in the same " - "transaction can only be executed in sequential query " - "execution mode", relationName))); - - SetLocalMultiShardModifyModeToSequential(); - - /* nothing to do more */ - return; - } - } -} - - -/* - * EnsurePartitionTableNotReplicatedForTruncate a simple wrapper around - * EnsurePartitionTableNotReplicated for TRUNCATE command. - */ -static void -EnsurePartitionTableNotReplicatedForTruncate(TruncateStmt *truncateStatement) -{ - ListCell *relationCell = NULL; - - foreach(relationCell, truncateStatement->relations) - { - RangeVar *relationRV = (RangeVar *) lfirst(relationCell); - Relation relation = heap_openrv(relationRV, NoLock); - Oid relationId = RelationGetRelid(relation); - - if (!IsDistributedTable(relationId)) - { - heap_close(relation, NoLock); - continue; - } - - EnsurePartitionTableNotReplicated(relationId); - - heap_close(relation, NoLock); - } -} - - -/* - * LockTruncatedRelationMetadataInWorkers determines if distributed - * lock is necessary for truncated relations, and acquire locks. - * - * LockTruncatedRelationMetadataInWorkers handles distributed locking - * of truncated tables before standard utility takes over. - * - * Actual distributed truncation occurs inside truncate trigger. - * - * This is only for distributed serialization of truncate commands. - * The function assumes that there is no foreign key relation between - * non-distributed and distributed relations. - */ -static void -LockTruncatedRelationMetadataInWorkers(TruncateStmt *truncateStatement) -{ - List *distributedRelationList = NIL; - ListCell *relationCell = NULL; - - /* nothing to do if there is no metadata at worker nodes */ - if (!ClusterHasKnownMetadataWorkers()) - { - return; - } - - foreach(relationCell, truncateStatement->relations) - { - RangeVar *relationRV = (RangeVar *) lfirst(relationCell); - Relation relation = heap_openrv(relationRV, NoLock); - Oid relationId = RelationGetRelid(relation); - DistTableCacheEntry *cacheEntry = NULL; - List *referencingTableList = NIL; - ListCell *referencingTableCell = NULL; - - if (!IsDistributedTable(relationId)) - { - heap_close(relation, NoLock); - continue; - } - - if (list_member_oid(distributedRelationList, relationId)) - { - heap_close(relation, NoLock); - continue; - } - - distributedRelationList = lappend_oid(distributedRelationList, relationId); - - cacheEntry = DistributedTableCacheEntry(relationId); - Assert(cacheEntry != NULL); - - referencingTableList = cacheEntry->referencingRelationsViaForeignKey; - foreach(referencingTableCell, referencingTableList) - { - Oid referencingRelationId = lfirst_oid(referencingTableCell); - distributedRelationList = list_append_unique_oid(distributedRelationList, - referencingRelationId); - } - - heap_close(relation, NoLock); - } - - if (distributedRelationList != NIL) - { - AcquireDistributedLockOnRelations(distributedRelationList, AccessExclusiveLock); - } -} - - -/* - * AcquireDistributedLockOnRelations acquire a distributed lock on worker nodes - * for given list of relations ids. Relation id list and worker node list - * sorted so that the lock is acquired in the same order regardless of which - * node it was run on. Notice that no lock is acquired on coordinator node. - * - * Notice that the locking functions is sent to all workers regardless of if - * it has metadata or not. This is because a worker node only knows itself - * and previous workers that has metadata sync turned on. The node does not - * know about other nodes that have metadata sync turned on afterwards. - */ -static void -AcquireDistributedLockOnRelations(List *relationIdList, LOCKMODE lockMode) -{ - ListCell *relationIdCell = NULL; - List *workerNodeList = ActivePrimaryNodeList(); - const char *lockModeText = LockModeToLockModeText(lockMode); - - /* - * We want to acquire locks in the same order accross the nodes. - * Although relation ids may change, their ordering will not. - */ - relationIdList = SortList(relationIdList, CompareOids); - workerNodeList = SortList(workerNodeList, CompareWorkerNodes); - - BeginOrContinueCoordinatedTransaction(); - - foreach(relationIdCell, relationIdList) - { - Oid relationId = lfirst_oid(relationIdCell); - - /* - * We only acquire distributed lock on relation if - * the relation is sync'ed between mx nodes. - */ - if (ShouldSyncTableMetadata(relationId)) - { - char *qualifiedRelationName = generate_qualified_relation_name(relationId); - StringInfo lockRelationCommand = makeStringInfo(); - ListCell *workerNodeCell = NULL; - - appendStringInfo(lockRelationCommand, LOCK_RELATION_IF_EXISTS, - qualifiedRelationName, lockModeText); - - foreach(workerNodeCell, workerNodeList) - { - WorkerNode *workerNode = (WorkerNode *) lfirst(workerNodeCell); - char *nodeName = workerNode->workerName; - int nodePort = workerNode->workerPort; - - /* if local node is one of the targets, acquire the lock locally */ - if (workerNode->groupId == GetLocalGroupId()) - { - LockRelationOid(relationId, lockMode); - continue; - } - - SendCommandToWorker(nodeName, nodePort, lockRelationCommand->data); - } - } - } -} - - -/* - * OptionsSpecifyOwnedBy processes the options list of either a CREATE or ALTER - * SEQUENCE command, extracting the first OWNED BY option it encounters. The - * identifier for the specified table is placed in the Oid out parameter before - * returning true. Returns false if no such option is found. Still returns true - * for OWNED BY NONE, but leaves the out paramter set to InvalidOid. - */ -static bool -OptionsSpecifyOwnedBy(List *optionList, Oid *ownedByTableId) -{ - ListCell *optionCell = NULL; - - foreach(optionCell, optionList) - { - DefElem *defElem = (DefElem *) lfirst(optionCell); - if (strcmp(defElem->defname, "owned_by") == 0) - { - List *ownedByNames = defGetQualifiedName(defElem); - int nameCount = list_length(ownedByNames); - - /* if only one name is present, this is OWNED BY NONE */ - if (nameCount == 1) - { - *ownedByTableId = InvalidOid; - return true; - } - else - { - /* - * Otherwise, we have a list of schema, table, column, which we - * need to truncate to simply the schema and table to determine - * the relevant relation identifier. - */ - List *relNameList = list_truncate(list_copy(ownedByNames), nameCount - 1); - RangeVar *rangeVar = makeRangeVarFromNameList(relNameList); - bool failOK = true; - - *ownedByTableId = RangeVarGetRelid(rangeVar, NoLock, failOK); - return true; - } - } - } - - return false; -} - - -/* - * ErrorIfDistributedRenameStmt errors out if the corresponding rename statement - * operates on any part of a distributed table other than a column. - * - * Note: This function handles RenameStmt applied to relations handed by Citus. - * At the moment of writing this comment, it could be either tables or indexes. - */ -static void -ErrorIfUnsupportedRenameStmt(RenameStmt *renameStmt) -{ - if (IsAlterTableRenameStmt(renameStmt) && - renameStmt->renameType == OBJECT_TABCONSTRAINT) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("renaming constraints belonging to distributed tables is " - "currently unsupported"))); - } -} - - -/* - * CreateLocalTable gets DDL commands from the remote node for the given - * relation. Then, it creates the local relation as temporary and on commit drop. - */ -static void -CreateLocalTable(RangeVar *relation, char *nodeName, int32 nodePort) -{ - List *ddlCommandList = NIL; - ListCell *ddlCommandCell = NULL; - - char *relationName = relation->relname; - char *schemaName = relation->schemaname; - char *qualifiedRelationName = quote_qualified_identifier(schemaName, relationName); - - /* - * The warning message created in TableDDLCommandList() is descriptive - * enough; therefore, we just throw an error which says that we could not - * run the copy operation. - */ - ddlCommandList = TableDDLCommandList(nodeName, nodePort, qualifiedRelationName); - if (ddlCommandList == NIL) - { - ereport(ERROR, (errmsg("could not run copy from the worker node"))); - } - - /* apply DDL commands against the local database */ - foreach(ddlCommandCell, ddlCommandList) - { - StringInfo ddlCommand = (StringInfo) lfirst(ddlCommandCell); - Node *ddlCommandNode = ParseTreeNode(ddlCommand->data); - bool applyDDLCommand = false; - - if (IsA(ddlCommandNode, CreateStmt) || - IsA(ddlCommandNode, CreateForeignTableStmt)) - { - CreateStmt *createStatement = (CreateStmt *) ddlCommandNode; - - /* create the local relation as temporary and on commit drop */ - createStatement->relation->relpersistence = RELPERSISTENCE_TEMP; - createStatement->oncommit = ONCOMMIT_DROP; - - /* temporarily strip schema name */ - createStatement->relation->schemaname = NULL; - - applyDDLCommand = true; - } - else if (IsA(ddlCommandNode, CreateForeignServerStmt)) - { - CreateForeignServerStmt *createServerStmt = - (CreateForeignServerStmt *) ddlCommandNode; - if (GetForeignServerByName(createServerStmt->servername, true) == NULL) - { - /* create server if not exists */ - applyDDLCommand = true; - } - } - else if ((IsA(ddlCommandNode, CreateExtensionStmt))) - { - applyDDLCommand = true; - } - else if ((IsA(ddlCommandNode, CreateSeqStmt))) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot copy to table with serial column from worker"), - errhint("Connect to the master node to COPY to tables which " - "use serial column types."))); - } - - /* run only a selected set of DDL commands */ - if (applyDDLCommand) - { - CitusProcessUtility(ddlCommandNode, CreateCommandTag(ddlCommandNode), - PROCESS_UTILITY_TOPLEVEL, NULL, None_Receiver, NULL); - - CommandCounterIncrement(); - } - } -} - - -/* - * IsAlterTableRenameStmt returns whether the passed-in RenameStmt is one of - * the following forms: - * - * - ALTER TABLE RENAME - * - ALTER TABLE RENAME COLUMN - * - ALTER TABLE RENAME CONSTRAINT - */ -static bool -IsAlterTableRenameStmt(RenameStmt *renameStmt) -{ - bool isAlterTableRenameStmt = false; - - if (renameStmt->renameType == OBJECT_TABLE) - { - isAlterTableRenameStmt = true; - } - else if (renameStmt->renameType == OBJECT_COLUMN && - renameStmt->relationType == OBJECT_TABLE) - { - isAlterTableRenameStmt = true; - } - else if (renameStmt->renameType == OBJECT_TABCONSTRAINT) - { - isAlterTableRenameStmt = true; - } - - return isAlterTableRenameStmt; -} - - -/* - * IsIndexRenameStmt returns whether the passed-in RenameStmt is the following - * form: - * - * - ALTER INDEX RENAME - */ -static bool -IsIndexRenameStmt(RenameStmt *renameStmt) -{ - bool isIndexRenameStmt = false; - - if (renameStmt->renameType == OBJECT_INDEX) - { - isIndexRenameStmt = true; - } - - return isIndexRenameStmt; -} - - -/* - * AlterInvolvesPartitionColumn checks if the given alter table command - * involves relation's partition column. - */ -static bool -AlterInvolvesPartitionColumn(AlterTableStmt *alterTableStatement, - AlterTableCmd *command) -{ - bool involvesPartitionColumn = false; - Var *partitionColumn = NULL; - HeapTuple tuple = NULL; - char *alterColumnName = command->name; - - LOCKMODE lockmode = AlterTableGetLockLevel(alterTableStatement->cmds); - Oid relationId = AlterTableLookupRelation(alterTableStatement, lockmode); - if (!OidIsValid(relationId)) - { - return false; - } - - partitionColumn = DistPartitionKey(relationId); - - tuple = SearchSysCacheAttName(relationId, alterColumnName); - if (HeapTupleIsValid(tuple)) - { - Form_pg_attribute targetAttr = (Form_pg_attribute) GETSTRUCT(tuple); - - /* reference tables do not have partition column, so allow them */ - if (partitionColumn != NULL && - targetAttr->attnum == partitionColumn->varattno) - { - involvesPartitionColumn = true; - } - - ReleaseSysCache(tuple); - } - - return involvesPartitionColumn; -} - - -/* - * ExecuteDistributedDDLJob simply executes a provided DDLJob in a distributed trans- - * action, including metadata sync if needed. If the multi shard commit protocol is - * in its default value of '1pc', then a notice message indicating that '2pc' might be - * used for extra safety. In the commit protocol, a BEGIN is sent after connection to - * each shard placement and COMMIT/ROLLBACK is handled by - * CoordinatedTransactionCallback function. - * - * The function errors out if the node is not the coordinator or if the DDL is on - * a partitioned table which has replication factor > 1. - * - */ -static void -ExecuteDistributedDDLJob(DDLJob *ddlJob) -{ - bool shouldSyncMetadata = ShouldSyncTableMetadata(ddlJob->targetRelationId); - - EnsureCoordinator(); - EnsurePartitionTableNotReplicated(ddlJob->targetRelationId); - - if (!ddlJob->concurrentIndexCmd) - { - if (shouldSyncMetadata) - { - char *setSearchPathCommand = SetSearchPathToCurrentSearchPathCommand(); - - SendCommandToWorkers(WORKERS_WITH_METADATA, DISABLE_DDL_PROPAGATION); - - /* - * Given that we're relaying the query to the worker nodes directly, - * we should set the search path exactly the same when necessary. - */ - if (setSearchPathCommand != NULL) - { - SendCommandToWorkers(WORKERS_WITH_METADATA, setSearchPathCommand); - } - - SendCommandToWorkers(WORKERS_WITH_METADATA, (char *) ddlJob->commandString); - } - - if (MultiShardConnectionType == SEQUENTIAL_CONNECTION || - ddlJob->executeSequentially) - { - ExecuteModifyTasksSequentiallyWithoutResults(ddlJob->taskList, CMD_UTILITY); - } - else - { - ExecuteModifyTasksWithoutResults(ddlJob->taskList); - } - } - else - { - /* save old commit protocol to restore at xact end */ - Assert(SavedMultiShardCommitProtocol == COMMIT_PROTOCOL_BARE); - SavedMultiShardCommitProtocol = MultiShardCommitProtocol; - MultiShardCommitProtocol = COMMIT_PROTOCOL_BARE; - - PG_TRY(); - { - ExecuteModifyTasksSequentiallyWithoutResults(ddlJob->taskList, CMD_UTILITY); - - if (shouldSyncMetadata) - { - List *commandList = list_make1(DISABLE_DDL_PROPAGATION); - char *setSearchPathCommand = SetSearchPathToCurrentSearchPathCommand(); - - /* - * Given that we're relaying the query to the worker nodes directly, - * we should set the search path exactly the same when necessary. - */ - if (setSearchPathCommand != NULL) - { - commandList = lappend(commandList, setSearchPathCommand); - } - - commandList = lappend(commandList, (char *) ddlJob->commandString); - - SendBareCommandListToWorkers(WORKERS_WITH_METADATA, commandList); - } - } - PG_CATCH(); - { - ereport(ERROR, - (errmsg("CONCURRENTLY-enabled index command failed"), - errdetail("CONCURRENTLY-enabled index commands can fail partially, " - "leaving behind an INVALID index."), - errhint("Use DROP INDEX CONCURRENTLY IF EXISTS to remove the " - "invalid index, then retry the original command."))); - } - PG_END_TRY(); - } -} - - -/* - * SetSearchPathToCurrentSearchPathCommand generates a command which can - * set the search path to the exact same search path that the issueing node - * has. - * - * If the current search path is null (or doesn't have any valid schemas), - * the function returns NULL. - */ -static char * -SetSearchPathToCurrentSearchPathCommand(void) -{ - StringInfo setCommand = NULL; - char *currentSearchPath = CurrentSearchPath(); - - if (currentSearchPath == NULL) - { - return NULL; - } - - setCommand = makeStringInfo(); - appendStringInfo(setCommand, "SET search_path TO %s;", currentSearchPath); - - return setCommand->data; -} - - -/* - * CurrentSearchPath is a C interface for calling current_schemas(bool) that - * PostgreSQL exports. - * - * CurrentSchemas returns all the schemas in the seach_path that are seperated - * with comma (,) sign. The returned string can be used to set the search_path. - * - * The function omits implicit schemas. - * - * The function returns NULL if there are no valid schemas in the search_path, - * mimicing current_schemas(false) function. - */ -static char * -CurrentSearchPath(void) -{ - StringInfo currentSearchPath = makeStringInfo(); - List *searchPathList = fetch_search_path(false); - ListCell *searchPathCell; - bool schemaAdded = false; - - foreach(searchPathCell, searchPathList) - { - char *schemaName = get_namespace_name(lfirst_oid(searchPathCell)); - - /* watch out for deleted namespace */ - if (schemaName) - { - if (schemaAdded) - { - appendStringInfoString(currentSearchPath, ","); - schemaAdded = false; - } - - appendStringInfoString(currentSearchPath, quote_identifier(schemaName)); - schemaAdded = true; - } - } - - /* fetch_search_path() returns a palloc'd list that we should free now */ - list_free(searchPathList); - - return (currentSearchPath->len > 0 ? currentSearchPath->data : NULL); -} - - -/* - * DDLTaskList builds a list of tasks to execute a DDL command on a - * given list of shards. - */ -List * -DDLTaskList(Oid relationId, const char *commandString) -{ - List *taskList = NIL; - List *shardIntervalList = LoadShardIntervalList(relationId); - ListCell *shardIntervalCell = NULL; - Oid schemaId = get_rel_namespace(relationId); - char *schemaName = get_namespace_name(schemaId); - char *escapedSchemaName = quote_literal_cstr(schemaName); - char *escapedCommandString = quote_literal_cstr(commandString); - uint64 jobId = INVALID_JOB_ID; - int taskId = 1; - - /* lock metadata before getting placement lists */ - LockShardListMetadata(shardIntervalList, ShareLock); - - foreach(shardIntervalCell, shardIntervalList) - { - ShardInterval *shardInterval = (ShardInterval *) lfirst(shardIntervalCell); - uint64 shardId = shardInterval->shardId; - StringInfo applyCommand = makeStringInfo(); - Task *task = NULL; - - /* - * If rightRelationId is not InvalidOid, instead of worker_apply_shard_ddl_command - * we use worker_apply_inter_shard_ddl_command. - */ - appendStringInfo(applyCommand, WORKER_APPLY_SHARD_DDL_COMMAND, shardId, - escapedSchemaName, escapedCommandString); - - task = CitusMakeNode(Task); - task->jobId = jobId; - task->taskId = taskId++; - task->taskType = DDL_TASK; - task->queryString = applyCommand->data; - task->replicationModel = REPLICATION_MODEL_INVALID; - task->dependedTaskList = NULL; - task->anchorShardId = shardId; - task->taskPlacementList = FinalizedShardPlacementList(shardId); - - taskList = lappend(taskList, task); - } - - return taskList; -} - - -/* - * CreateIndexTaskList builds a list of tasks to execute a CREATE INDEX command - * against a specified distributed table. - */ -static List * -CreateIndexTaskList(Oid relationId, IndexStmt *indexStmt) -{ - List *taskList = NIL; - List *shardIntervalList = LoadShardIntervalList(relationId); - ListCell *shardIntervalCell = NULL; - StringInfoData ddlString; - uint64 jobId = INVALID_JOB_ID; - int taskId = 1; - - initStringInfo(&ddlString); - - /* lock metadata before getting placement lists */ - LockShardListMetadata(shardIntervalList, ShareLock); - - foreach(shardIntervalCell, shardIntervalList) - { - ShardInterval *shardInterval = (ShardInterval *) lfirst(shardIntervalCell); - uint64 shardId = shardInterval->shardId; - Task *task = NULL; - - deparse_shard_index_statement(indexStmt, relationId, shardId, &ddlString); - - task = CitusMakeNode(Task); - task->jobId = jobId; - task->taskId = taskId++; - task->taskType = DDL_TASK; - task->queryString = pstrdup(ddlString.data); - task->replicationModel = REPLICATION_MODEL_INVALID; - task->dependedTaskList = NULL; - task->anchorShardId = shardId; - task->taskPlacementList = FinalizedShardPlacementList(shardId); - - taskList = lappend(taskList, task); - - resetStringInfo(&ddlString); - } - - return taskList; -} - - -/* - * DropIndexTaskList builds a list of tasks to execute a DROP INDEX command - * against a specified distributed table. - */ -static List * -DropIndexTaskList(Oid relationId, Oid indexId, DropStmt *dropStmt) -{ - List *taskList = NIL; - List *shardIntervalList = LoadShardIntervalList(relationId); - ListCell *shardIntervalCell = NULL; - char *indexName = get_rel_name(indexId); - Oid schemaId = get_rel_namespace(indexId); - char *schemaName = get_namespace_name(schemaId); - StringInfoData ddlString; - uint64 jobId = INVALID_JOB_ID; - int taskId = 1; - - initStringInfo(&ddlString); - - /* lock metadata before getting placement lists */ - LockShardListMetadata(shardIntervalList, ShareLock); - - foreach(shardIntervalCell, shardIntervalList) - { - ShardInterval *shardInterval = (ShardInterval *) lfirst(shardIntervalCell); - uint64 shardId = shardInterval->shardId; - char *shardIndexName = pstrdup(indexName); - Task *task = NULL; - - AppendShardIdToName(&shardIndexName, shardId); - - /* deparse shard-specific DROP INDEX command */ - appendStringInfo(&ddlString, "DROP INDEX %s %s %s %s", - (dropStmt->concurrent ? "CONCURRENTLY" : ""), - (dropStmt->missing_ok ? "IF EXISTS" : ""), - quote_qualified_identifier(schemaName, shardIndexName), - (dropStmt->behavior == DROP_RESTRICT ? "RESTRICT" : "CASCADE")); - - task = CitusMakeNode(Task); - task->jobId = jobId; - task->taskId = taskId++; - task->taskType = DDL_TASK; - task->queryString = pstrdup(ddlString.data); - task->replicationModel = REPLICATION_MODEL_INVALID; - task->dependedTaskList = NULL; - task->anchorShardId = shardId; - task->taskPlacementList = FinalizedShardPlacementList(shardId); - - taskList = lappend(taskList, task); - - resetStringInfo(&ddlString); - } - - return taskList; -} - - -/* - * InterShardDDLTaskList builds a list of tasks to execute a inter shard DDL command on a - * shards of given list of distributed table. At the moment this function is used to run - * foreign key and partitioning command on worker node. - * - * leftRelationId is the relation id of actual distributed table which given command is - * applied. rightRelationId is the relation id of distributed table which given command - * refers to. - */ -static List * -InterShardDDLTaskList(Oid leftRelationId, Oid rightRelationId, - const char *commandString) -{ - List *taskList = NIL; - - List *leftShardList = LoadShardIntervalList(leftRelationId); - ListCell *leftShardCell = NULL; - Oid leftSchemaId = get_rel_namespace(leftRelationId); - char *leftSchemaName = get_namespace_name(leftSchemaId); - char *escapedLeftSchemaName = quote_literal_cstr(leftSchemaName); - - char rightPartitionMethod = PartitionMethod(rightRelationId); - List *rightShardList = LoadShardIntervalList(rightRelationId); - ListCell *rightShardCell = NULL; - Oid rightSchemaId = get_rel_namespace(rightRelationId); - char *rightSchemaName = get_namespace_name(rightSchemaId); - char *escapedRightSchemaName = quote_literal_cstr(rightSchemaName); - - char *escapedCommandString = quote_literal_cstr(commandString); - uint64 jobId = INVALID_JOB_ID; - int taskId = 1; - - /* - * If the rightPartitionMethod is a reference table, we need to make sure - * that the tasks are created in a way that the right shard stays the same - * since we only have one placement per worker. This hack is first implemented - * for foreign constraint support from distributed tables to reference tables. - */ - if (rightPartitionMethod == DISTRIBUTE_BY_NONE) - { - ShardInterval *rightShardInterval = NULL; - int rightShardCount = list_length(rightShardList); - int leftShardCount = list_length(leftShardList); - int shardCounter = 0; - - Assert(rightShardCount == 1); - - rightShardInterval = (ShardInterval *) linitial(rightShardList); - for (shardCounter = rightShardCount; shardCounter < leftShardCount; - shardCounter++) - { - rightShardList = lappend(rightShardList, rightShardInterval); - } - } - - /* lock metadata before getting placement lists */ - LockShardListMetadata(leftShardList, ShareLock); - - forboth(leftShardCell, leftShardList, rightShardCell, rightShardList) - { - ShardInterval *leftShardInterval = (ShardInterval *) lfirst(leftShardCell); - uint64 leftShardId = leftShardInterval->shardId; - StringInfo applyCommand = makeStringInfo(); - Task *task = NULL; - RelationShard *leftRelationShard = CitusMakeNode(RelationShard); - RelationShard *rightRelationShard = CitusMakeNode(RelationShard); - - ShardInterval *rightShardInterval = (ShardInterval *) lfirst(rightShardCell); - uint64 rightShardId = rightShardInterval->shardId; - - leftRelationShard->relationId = leftRelationId; - leftRelationShard->shardId = leftShardId; - - rightRelationShard->relationId = rightRelationId; - rightRelationShard->shardId = rightShardId; - - appendStringInfo(applyCommand, WORKER_APPLY_INTER_SHARD_DDL_COMMAND, - leftShardId, escapedLeftSchemaName, rightShardId, - escapedRightSchemaName, escapedCommandString); - - task = CitusMakeNode(Task); - task->jobId = jobId; - task->taskId = taskId++; - task->taskType = DDL_TASK; - task->queryString = applyCommand->data; - task->dependedTaskList = NULL; - task->replicationModel = REPLICATION_MODEL_INVALID; - task->anchorShardId = leftShardId; - task->taskPlacementList = FinalizedShardPlacementList(leftShardId); - task->relationShardList = list_make2(leftRelationShard, rightRelationShard); - - taskList = lappend(taskList, task); - } - - return taskList; -} - - -/* - * Before acquiring a table lock, check whether we have sufficient rights. - * In the case of DROP INDEX, also try to lock the table before the index. - * - * This code is heavily borrowed from RangeVarCallbackForDropRelation() in - * commands/tablecmds.c in Postgres source. We need this to ensure the right - * order of locking while dealing with DROP INDEX statements. Because we are - * exclusively using this callback for INDEX processing, the PARTITION-related - * logic from PostgreSQL's similar callback has been omitted as unneeded. - */ -static void -RangeVarCallbackForDropIndex(const RangeVar *rel, Oid relOid, Oid oldRelOid, void *arg) -{ - /* *INDENT-OFF* */ - HeapTuple tuple; - struct DropRelationCallbackState *state; - char relkind; - char expected_relkind; - Form_pg_class classform; - LOCKMODE heap_lockmode; - - state = (struct DropRelationCallbackState *) arg; - relkind = state->relkind; - heap_lockmode = state->concurrent ? - ShareUpdateExclusiveLock : AccessExclusiveLock; - - Assert(relkind == RELKIND_INDEX); - - /* - * If we previously locked some other index's heap, and the name we're - * looking up no longer refers to that relation, release the now-useless - * lock. - */ - if (relOid != oldRelOid && OidIsValid(state->heapOid)) - { - UnlockRelationOid(state->heapOid, heap_lockmode); - state->heapOid = InvalidOid; - } - - /* Didn't find a relation, so no need for locking or permission checks. */ - if (!OidIsValid(relOid)) - return; - - tuple = SearchSysCache1(RELOID, ObjectIdGetDatum(relOid)); - if (!HeapTupleIsValid(tuple)) - return; /* concurrently dropped, so nothing to do */ - classform = (Form_pg_class) GETSTRUCT(tuple); - - /* - * PG 11 sends relkind as partitioned index for an index - * on partitioned table. It is handled the same - * as regular index as far as we are concerned here. - * - * See tablecmds.c:RangeVarCallbackForDropRelation() - */ - expected_relkind = classform->relkind; - -#if PG_VERSION_NUM >= 110000 - if (expected_relkind == RELKIND_PARTITIONED_INDEX) - expected_relkind = RELKIND_INDEX; -#endif - - if (expected_relkind != relkind) - ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("\"%s\" is not an index", rel->relname))); - - /* Allow DROP to either table owner or schema owner */ - if (!pg_class_ownercheck(relOid, GetUserId()) && - !pg_namespace_ownercheck(classform->relnamespace, GetUserId())) - { - aclcheck_error(ACLCHECK_NOT_OWNER, ACLCHECK_OBJECT_INDEX, rel->relname); - } - - if (!allowSystemTableMods && IsSystemClass(relOid, classform)) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("permission denied: \"%s\" is a system catalog", - rel->relname))); - - ReleaseSysCache(tuple); - - /* - * In DROP INDEX, attempt to acquire lock on the parent table before - * locking the index. index_drop() will need this anyway, and since - * regular queries lock tables before their indexes, we risk deadlock if - * we do it the other way around. No error if we don't find a pg_index - * entry, though --- the relation may have been dropped. - */ - if (relkind == RELKIND_INDEX && relOid != oldRelOid) - { - state->heapOid = IndexGetRelation(relOid, true); - if (OidIsValid(state->heapOid)) - LockRelationOid(state->heapOid, heap_lockmode); - } - /* *INDENT-ON* */ -} - - -/* - * Check whether the current user has the permission to execute a COPY - * statement, raise ERROR if not. In some cases we have to do this separately - * from postgres' copy.c, because we have to execute the copy with elevated - * privileges. - * - * Copied from postgres, where it's part of DoCopy(). - */ -static void -CheckCopyPermissions(CopyStmt *copyStatement) -{ - /* *INDENT-OFF* */ - bool is_from = copyStatement->is_from; - Relation rel; - Oid relid; - List *range_table = NIL; - TupleDesc tupDesc; - AclMode required_access = (is_from ? ACL_INSERT : ACL_SELECT); - List *attnums; - ListCell *cur; - RangeTblEntry *rte; - - rel = heap_openrv(copyStatement->relation, - is_from ? RowExclusiveLock : AccessShareLock); - - relid = RelationGetRelid(rel); - - rte = makeNode(RangeTblEntry); - rte->rtekind = RTE_RELATION; - rte->relid = relid; - rte->relkind = rel->rd_rel->relkind; - rte->requiredPerms = required_access; - range_table = list_make1(rte); - - tupDesc = RelationGetDescr(rel); - - attnums = CopyGetAttnums(tupDesc, rel, copyStatement->attlist); - foreach(cur, attnums) - { - int attno = lfirst_int(cur) - FirstLowInvalidHeapAttributeNumber; - - if (is_from) - { - rte->insertedCols = bms_add_member(rte->insertedCols, attno); - } - else - { - rte->selectedCols = bms_add_member(rte->selectedCols, attno); - } - } - - ExecCheckRTPerms(range_table, true); - - /* TODO: Perform RLS checks once supported */ - - heap_close(rel, NoLock); - /* *INDENT-ON* */ -} - - -/* Helper for CheckCopyPermissions(), copied from postgres */ -static List * -CopyGetAttnums(TupleDesc tupDesc, Relation rel, List *attnamelist) -{ - /* *INDENT-OFF* */ - List *attnums = NIL; - - if (attnamelist == NIL) - { - /* Generate default column list */ - int attr_count = tupDesc->natts; - int i; - - for (i = 0; i < attr_count; i++) - { - if (TupleDescAttr(tupDesc, i)->attisdropped) - continue; - attnums = lappend_int(attnums, i + 1); - } - } - else - { - /* Validate the user-supplied list and extract attnums */ - ListCell *l; - - foreach(l, attnamelist) - { - char *name = strVal(lfirst(l)); - int attnum; - int i; - - /* Lookup column name */ - attnum = InvalidAttrNumber; - for (i = 0; i < tupDesc->natts; i++) - { - Form_pg_attribute att = TupleDescAttr(tupDesc, i); - - if (att->attisdropped) - continue; - if (namestrcmp(&(att->attname), name) == 0) - { - attnum = att->attnum; - break; - } - } - if (attnum == InvalidAttrNumber) - { - if (rel != NULL) - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" of relation \"%s\" does not exist", - name, RelationGetRelationName(rel)))); - else - ereport(ERROR, - (errcode(ERRCODE_UNDEFINED_COLUMN), - errmsg("column \"%s\" does not exist", - name))); - } - /* Check for duplicates */ - if (list_member_int(attnums, attnum)) - ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_COLUMN), - errmsg("column \"%s\" specified more than once", - name))); - attnums = lappend_int(attnums, attnum); - } - } - - return attnums; - /* *INDENT-ON* */ -} - - -/* - * PostProcessUtility performs additional tasks after a utility's local portion - * has been completed. Right now, the sole use is marking new indexes invalid - * if they were created using the CONCURRENTLY flag. This (non-transactional) - * change provides the fallback state if an error is raised, otherwise a sub- - * sequent change to valid will be committed. - */ -static void -PostProcessUtility(Node *parsetree) -{ - IndexStmt *indexStmt = NULL; - Relation relation = NULL; - Oid indexRelationId = InvalidOid; - Relation indexRelation = NULL; - Relation pg_index = NULL; - HeapTuple indexTuple = NULL; - Form_pg_index indexForm = NULL; - - /* only IndexStmts are processed */ - if (!IsA(parsetree, IndexStmt)) - { - return; - } - - /* and even then only if they're CONCURRENT */ - indexStmt = (IndexStmt *) parsetree; - if (!indexStmt->concurrent) - { - return; - } - - /* finally, this logic only applies to the coordinator */ - if (!IsCoordinator()) - { - return; - } - - /* commit the current transaction and start anew */ - CommitTransactionCommand(); - StartTransactionCommand(); - - /* get the affected relation and index */ - relation = heap_openrv(indexStmt->relation, ShareUpdateExclusiveLock); - indexRelationId = get_relname_relid(indexStmt->idxname, - RelationGetNamespace(relation)); - indexRelation = index_open(indexRelationId, RowExclusiveLock); - - /* close relations but retain locks */ - heap_close(relation, NoLock); - index_close(indexRelation, NoLock); - - /* mark index as invalid, in-place (cannot be rolled back) */ - index_set_state_flags(indexRelationId, INDEX_DROP_CLEAR_VALID); - - /* re-open a transaction command from here on out */ - CommitTransactionCommand(); - StartTransactionCommand(); - - /* now, update index's validity in a way that can roll back */ - pg_index = heap_open(IndexRelationId, RowExclusiveLock); - - indexTuple = SearchSysCacheCopy1(INDEXRELID, ObjectIdGetDatum(indexRelationId)); - Assert(HeapTupleIsValid(indexTuple)); /* better be present, we have lock! */ - - /* mark as valid, save, and update pg_index indexes */ - indexForm = (Form_pg_index) GETSTRUCT(indexTuple); - indexForm->indisvalid = true; - - CatalogTupleUpdate(pg_index, &indexTuple->t_self, indexTuple); - - /* clean up; index now marked valid, but ROLLBACK will mark invalid */ - heap_freetuple(indexTuple); - heap_close(pg_index, RowExclusiveLock); -} - - -/* - * PlanGrantStmt determines whether a given GRANT/REVOKE statement involves - * a distributed table. If so, it creates DDLJobs to encapsulate information - * needed during the worker node portion of DDL execution before returning the - * DDLJobs in a List. If no distributed table is involved, this returns NIL. - * - * NB: So far column level privileges are not supported. - */ -List * -PlanGrantStmt(GrantStmt *grantStmt) -{ - StringInfoData privsString; - StringInfoData granteesString; - StringInfoData targetString; - StringInfoData ddlString; - ListCell *granteeCell = NULL; - List *tableIdList = NIL; - ListCell *tableListCell = NULL; - bool isFirst = true; - List *ddlJobs = NIL; - - initStringInfo(&privsString); - initStringInfo(&granteesString); - initStringInfo(&targetString); - initStringInfo(&ddlString); - - /* - * So far only table level grants are supported. Most other types of - * grants aren't interesting anyway. - */ - if (grantStmt->objtype != RELATION_OBJECT_TYPE) - { - return NIL; - } - - tableIdList = CollectGrantTableIdList(grantStmt); - - /* nothing to do if there is no distributed table in the grant list */ - if (tableIdList == NIL) - { - return NIL; - } - - /* deparse the privileges */ - if (grantStmt->privileges == NIL) - { - appendStringInfo(&privsString, "ALL"); - } - else - { - ListCell *privilegeCell = NULL; - - isFirst = true; - foreach(privilegeCell, grantStmt->privileges) - { - AccessPriv *priv = lfirst(privilegeCell); - - if (!isFirst) - { - appendStringInfoString(&privsString, ", "); - } - isFirst = false; - - if (priv->cols != NIL) - { - ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("grant/revoke on column list is currently " - "unsupported"))); - } - - Assert(priv->priv_name != NULL); - - appendStringInfo(&privsString, "%s", priv->priv_name); - } - } - - /* deparse the grantees */ - isFirst = true; - foreach(granteeCell, grantStmt->grantees) - { - RoleSpec *spec = lfirst(granteeCell); - - if (!isFirst) - { - appendStringInfoString(&granteesString, ", "); - } - isFirst = false; - - appendStringInfoString(&granteesString, RoleSpecString(spec)); - } - - /* - * Deparse the target objects, and issue the deparsed statements to - * workers, if applicable. That's so we easily can replicate statements - * only to distributed relations. - */ - isFirst = true; - foreach(tableListCell, tableIdList) - { - Oid relationId = lfirst_oid(tableListCell); - const char *grantOption = ""; - DDLJob *ddlJob = NULL; - - Assert(IsDistributedTable(relationId)); - - resetStringInfo(&targetString); - appendStringInfo(&targetString, "%s", generate_relation_name(relationId, NIL)); - - if (grantStmt->is_grant) - { - if (grantStmt->grant_option) - { - grantOption = " WITH GRANT OPTION"; - } - - appendStringInfo(&ddlString, "GRANT %s ON %s TO %s%s", - privsString.data, targetString.data, granteesString.data, - grantOption); - } - else - { - if (grantStmt->grant_option) - { - grantOption = "GRANT OPTION FOR "; - } - - appendStringInfo(&ddlString, "REVOKE %s%s ON %s FROM %s", - grantOption, privsString.data, targetString.data, - granteesString.data); - } - - ddlJob = palloc0(sizeof(DDLJob)); - ddlJob->targetRelationId = relationId; - ddlJob->concurrentIndexCmd = false; - ddlJob->commandString = pstrdup(ddlString.data); - ddlJob->taskList = DDLTaskList(relationId, ddlString.data); - - ddlJobs = lappend(ddlJobs, ddlJob); - - resetStringInfo(&ddlString); - } - - return ddlJobs; -} - - -/* - * CollectGrantTableIdList determines and returns a list of distributed table - * Oids from grant statement. - * Grant statement may appear in two forms - * 1 - grant on table: - * each distributed table oid in grant object list is added to returned list. - * 2 - grant all tables in schema: - * Collect namespace oid list from grant statement - * Add each distributed table oid in the target namespace list to the returned list. - */ -static List * -CollectGrantTableIdList(GrantStmt *grantStmt) -{ - List *grantTableList = NIL; - bool grantOnTableCommand = false; - bool grantAllTablesOnSchemaCommand = false; - - grantOnTableCommand = (grantStmt->targtype == ACL_TARGET_OBJECT && - grantStmt->objtype == RELATION_OBJECT_TYPE); - grantAllTablesOnSchemaCommand = (grantStmt->targtype == ACL_TARGET_ALL_IN_SCHEMA && - grantStmt->objtype == RELATION_OBJECT_TYPE); - - /* we are only interested in table level grants */ - if (!grantOnTableCommand && !grantAllTablesOnSchemaCommand) - { - return NIL; - } - - if (grantAllTablesOnSchemaCommand) - { - List *distTableOidList = DistTableOidList(); - ListCell *distributedTableOidCell = NULL; - List *namespaceOidList = NIL; - - ListCell *objectCell = NULL; - foreach(objectCell, grantStmt->objects) - { - char *nspname = strVal(lfirst(objectCell)); - bool missing_ok = false; - Oid namespaceOid = get_namespace_oid(nspname, missing_ok); - Assert(namespaceOid != InvalidOid); - namespaceOidList = list_append_unique_oid(namespaceOidList, namespaceOid); - } - - foreach(distributedTableOidCell, distTableOidList) - { - Oid relationId = lfirst_oid(distributedTableOidCell); - Oid namespaceOid = get_rel_namespace(relationId); - if (list_member_oid(namespaceOidList, namespaceOid)) - { - grantTableList = lappend_oid(grantTableList, relationId); - } - } - } - else - { - ListCell *objectCell = NULL; - foreach(objectCell, grantStmt->objects) - { - RangeVar *relvar = (RangeVar *) lfirst(objectCell); - Oid relationId = RangeVarGetRelid(relvar, NoLock, false); - if (IsDistributedTable(relationId)) - { - grantTableList = lappend_oid(grantTableList, relationId); - } - } - } - - return grantTableList; -} - - -/* - * RoleSpecString resolves the role specification to its string form that is suitable for transport to a worker node. - * This function resolves the following identifiers from the current context so they are safe to transfer. - * - * CURRENT_USER - resolved to the user name of the current role being used - * SESSION_USER - resolved to the user name of the user that opened the session - */ -const char * -RoleSpecString(RoleSpec *spec) -{ - switch (spec->roletype) - { - case ROLESPEC_CSTRING: - { - return quote_identifier(spec->rolename); - } - - case ROLESPEC_CURRENT_USER: - { - return quote_identifier(GetUserNameFromId(GetUserId(), false)); - } - - case ROLESPEC_SESSION_USER: - { - return quote_identifier(GetUserNameFromId(GetSessionUserId(), false)); - } - - case ROLESPEC_PUBLIC: - { - return "PUBLIC"; - } - - default: - { - elog(ERROR, "unexpected role type %d", spec->roletype); - } - } -} - - -/* - * ProcessDropSchemaStmt invalidates the foreign key cache if any table created - * under dropped schema involved in any foreign key relationship. - */ -static void -ProcessDropSchemaStmt(DropStmt *dropStatement) -{ - Relation pgClass = NULL; - HeapTuple heapTuple = NULL; - SysScanDesc scanDescriptor = NULL; - ScanKeyData scanKey[1]; - int scanKeyCount = 1; - Oid scanIndexId = InvalidOid; - bool useIndex = false; - ListCell *dropSchemaCell; - - if (dropStatement->behavior != DROP_CASCADE) - { - return; - } - - foreach(dropSchemaCell, dropStatement->objects) - { - char *schemaString = GetSchemaNameFromDropObject(dropSchemaCell); - Oid namespaceOid = get_namespace_oid(schemaString, true); - - if (namespaceOid == InvalidOid) - { - continue; - } - - pgClass = heap_open(RelationRelationId, AccessShareLock); - - ScanKeyInit(&scanKey[0], Anum_pg_class_relnamespace, BTEqualStrategyNumber, - F_OIDEQ, namespaceOid); - scanDescriptor = systable_beginscan(pgClass, scanIndexId, useIndex, NULL, - scanKeyCount, scanKey); - - heapTuple = systable_getnext(scanDescriptor); - while (HeapTupleIsValid(heapTuple)) - { - Form_pg_class relationForm = (Form_pg_class) GETSTRUCT(heapTuple); - char *relationName = NameStr(relationForm->relname); - Oid relationId = get_relname_relid(relationName, namespaceOid); - - /* we're not interested in non-valid, non-distributed relations */ - if (relationId == InvalidOid || !IsDistributedTable(relationId)) - { - heapTuple = systable_getnext(scanDescriptor); - continue; - } - - /* invalidate foreign key cache if the table involved in any foreign key */ - if (TableReferenced(relationId) || TableReferencing(relationId)) - { - shouldInvalidateForeignKeyGraph = true; - - systable_endscan(scanDescriptor); - heap_close(pgClass, NoLock); - return; - } - - heapTuple = systable_getnext(scanDescriptor); - } - - systable_endscan(scanDescriptor); - heap_close(pgClass, NoLock); - } -} - - -/* - * GetSchemaNameFromDropObject gets the name of the drop schema from given - * list cell. This function is defined due to API change between PG 9.6 and - * PG 10. - */ -static char * -GetSchemaNameFromDropObject(ListCell *dropSchemaCell) -{ - char *schemaString = NULL; - -#if (PG_VERSION_NUM >= 100000) - Value *schemaValue = (Value *) lfirst(dropSchemaCell); - schemaString = strVal(schemaValue); -#else - List *schemaNameList = (List *) lfirst(dropSchemaCell); - schemaString = NameListToString(schemaNameList); -#endif - - return schemaString; -} - - -/* - * ProcessDropTableStmt processes DROP TABLE commands for partitioned tables. - * If we are trying to DROP partitioned tables, we first need to go to MX nodes - * and DETACH partitions from their parents. Otherwise, we process DROP command - * multiple times in MX workers. For shards, we send DROP commands with IF EXISTS - * parameter which solves problem of processing same command multiple times. - * However, for distributed table itself, we directly remove related table from - * Postgres catalogs via performDeletion function, thus we need to be cautious - * about not processing same DROP command twice. - */ -static void -ProcessDropTableStmt(DropStmt *dropTableStatement) -{ - ListCell *dropTableCell = NULL; - - Assert(dropTableStatement->removeType == OBJECT_TABLE); - - foreach(dropTableCell, dropTableStatement->objects) - { - List *tableNameList = (List *) lfirst(dropTableCell); - RangeVar *tableRangeVar = makeRangeVarFromNameList(tableNameList); - bool missingOK = true; - List *partitionList = NIL; - ListCell *partitionCell = NULL; - - Oid relationId = RangeVarGetRelid(tableRangeVar, AccessShareLock, missingOK); - - /* we're not interested in non-valid, non-distributed relations */ - if (relationId == InvalidOid || !IsDistributedTable(relationId)) - { - continue; - } - - /* invalidate foreign key cache if the table involved in any foreign key */ - if ((TableReferenced(relationId) || TableReferencing(relationId))) - { - shouldInvalidateForeignKeyGraph = true; - } - - /* we're only interested in partitioned and mx tables */ - if (!ShouldSyncTableMetadata(relationId) || !PartitionedTable(relationId)) - { - continue; - } - - EnsureCoordinator(); - - partitionList = PartitionList(relationId); - if (list_length(partitionList) == 0) - { - continue; - } - - SendCommandToWorkers(WORKERS_WITH_METADATA, DISABLE_DDL_PROPAGATION); - - foreach(partitionCell, partitionList) - { - Oid partitionRelationId = lfirst_oid(partitionCell); - char *detachPartitionCommand = - GenerateDetachPartitionCommand(partitionRelationId); - - SendCommandToWorkers(WORKERS_WITH_METADATA, detachPartitionCommand); - } - } -} - - -/* - * SetupExecutionModeForAlterTable is the function that is responsible - * for two things for practial purpose for not doing the same checks - * twice: - * (a) For any command, decide and return whether we should - * run the command in sequntial mode - * (b) For commands in a transaction block, set the transaction local - * multi-shard modify mode to sequential when necessary - * - * The commands that operate on the same reference table shard in parallel - * is in the interest of (a), where the return value indicates the executor - * to run the command sequentially to prevent self-deadlocks. - * - * The commands that both operate on the same reference table shard in parallel - * and cascades to run any parallel operation is in the interest of (b). By - * setting the multi-shard mode, we ensure that the cascading parallel commands - * are executed sequentially to prevent self-deadlocks. - * - * One final note on the function is that if the function decides to execute - * the command in sequential mode, and a parallel command has already been - * executed in the same transaction, the function errors out. See the comment - * in the function for the rationale. - */ -static bool -SetupExecutionModeForAlterTable(Oid relationId, AlterTableCmd *command) -{ - bool executeSequentially = false; - AlterTableType alterTableType = command->subtype; - if (alterTableType == AT_DropConstraint) - { - char *constraintName = command->name; - if (ConstraintIsAForeignKeyToReferenceTable(constraintName, relationId)) - { - executeSequentially = true; - } - } - else if (alterTableType == AT_AddColumn) - { - /* - * TODO: This code path will never be executed since we do not - * support foreign constraint creation via - * ALTER TABLE %s ADD COLUMN %s [constraint]. However, the code - * is kept in case we fix the constraint creation without a name - * and allow foreign key creation with the mentioned command. - */ - ColumnDef *columnDefinition = (ColumnDef *) command->def; - List *columnConstraints = columnDefinition->constraints; - - ListCell *columnConstraint = NULL; - foreach(columnConstraint, columnConstraints) - { - Constraint *constraint = (Constraint *) lfirst(columnConstraint); - if (constraint->contype == CONSTR_FOREIGN) - { - Oid rightRelationId = RangeVarGetRelid(constraint->pktable, NoLock, - false); - if (IsDistributedTable(rightRelationId) && - PartitionMethod(rightRelationId) == DISTRIBUTE_BY_NONE) - { - executeSequentially = true; - } - } - } - } - else if (alterTableType == AT_DropColumn || alterTableType == AT_AlterColumnType) - { - char *affectedColumnName = command->name; - - if (ColumnAppearsInForeignKeyToReferenceTable(affectedColumnName, - relationId)) - { - if (IsTransactionBlock() && alterTableType == AT_AlterColumnType) - { - SetLocalMultiShardModifyModeToSequential(); - } - - executeSequentially = true; - } - } - else if (alterTableType == AT_AddConstraint) - { - /* - * We need to execute the ddls working with reference tables on the - * right side sequentially, because parallel ddl operations - * relating to one and only shard of a reference table on a worker - * may cause self-deadlocks. - */ - Constraint *constraint = (Constraint *) command->def; - if (constraint->contype == CONSTR_FOREIGN) - { - Oid rightRelationId = RangeVarGetRelid(constraint->pktable, NoLock, - false); - if (IsDistributedTable(rightRelationId) && - PartitionMethod(rightRelationId) == DISTRIBUTE_BY_NONE) - { - executeSequentially = true; - } - } - } - - /* - * If there has already been a parallel query executed, the sequential mode - * would still use the already opened parallel connections to the workers for - * the distributed tables, thus contradicting our purpose of using - * sequential mode. - */ - if (executeSequentially && IsDistributedTable(relationId) && - PartitionMethod(relationId) != DISTRIBUTE_BY_NONE && - ParallelQueryExecutedInTransaction()) - { - char *relationName = get_rel_name(relationId); - - ereport(ERROR, (errmsg("cannot modify table \"%s\" because there " - "was a parallel operation on a distributed table " - "in the transaction", relationName), - errdetail("When there is a foreign key to a reference " - "table, Citus needs to perform all operations " - "over a single connection per node to ensure " - "consistency."), - errhint("Try re-running the transaction with " - "\"SET LOCAL citus.multi_shard_modify_mode TO " - "\'sequential\';\""))); - } - - return executeSequentially; -} diff --git a/src/backend/distributed/master/master_delete_protocol.c b/src/backend/distributed/master/master_delete_protocol.c index 2da61afeb1e..91a0b70a38b 100644 --- a/src/backend/distributed/master/master_delete_protocol.c +++ b/src/backend/distributed/master/master_delete_protocol.c @@ -25,6 +25,7 @@ #include "access/xact.h" #include "catalog/namespace.h" #include "commands/dbcommands.h" +#include "distributed/commands/utility_hook.h" #include "distributed/connection_management.h" #include "distributed/master_protocol.h" #include "distributed/metadata_sync.h" @@ -33,7 +34,6 @@ #include "distributed/multi_logical_planner.h" #include "distributed/multi_physical_planner.h" #include "distributed/multi_server_executor.h" -#include "distributed/multi_utility.h" #include "distributed/pg_dist_partition.h" #include "distributed/pg_dist_shard.h" #include "distributed/placement_connection.h" diff --git a/src/backend/distributed/master/master_modify_multiple_shards.c b/src/backend/distributed/master/master_modify_multiple_shards.c index 6cd9fbd4b65..2f11044729a 100644 --- a/src/backend/distributed/master/master_modify_multiple_shards.c +++ b/src/backend/distributed/master/master_modify_multiple_shards.c @@ -25,7 +25,7 @@ #include "commands/event_trigger.h" #include "distributed/citus_clauses.h" #include "distributed/citus_ruleutils.h" -#include "distributed/foreign_constraint.h" +#include "distributed/commands.h" #include "distributed/listutils.h" #include "distributed/master_metadata_utility.h" #include "distributed/master_protocol.h" diff --git a/src/backend/distributed/master/master_node_protocol.c b/src/backend/distributed/master/master_node_protocol.c index 14ce0495016..53412723ad8 100644 --- a/src/backend/distributed/master/master_node_protocol.c +++ b/src/backend/distributed/master/master_node_protocol.c @@ -41,12 +41,12 @@ #include "catalog/pg_namespace.h" #include "commands/sequence.h" #include "distributed/citus_ruleutils.h" +#include "distributed/commands.h" #include "distributed/listutils.h" #include "distributed/master_protocol.h" #include "distributed/metadata_cache.h" #include "distributed/metadata_sync.h" #include "distributed/pg_dist_shard.h" -#include "distributed/policy.h" #include "distributed/worker_manager.h" #include "foreign/foreign.h" #include "lib/stringinfo.h" diff --git a/src/backend/distributed/master/master_repair_shards.c b/src/backend/distributed/master/master_repair_shards.c index 2fc95d0eb22..29bb09651c6 100644 --- a/src/backend/distributed/master/master_repair_shards.c +++ b/src/backend/distributed/master/master_repair_shards.c @@ -19,8 +19,8 @@ #include "catalog/pg_class.h" #include "distributed/colocation_utils.h" +#include "distributed/commands.h" #include "distributed/connection_management.h" -#include "distributed/foreign_constraint.h" #include "distributed/listutils.h" #include "distributed/master_protocol.h" #include "distributed/metadata_cache.h" diff --git a/src/backend/distributed/master/master_stage_protocol.c b/src/backend/distributed/master/master_stage_protocol.c index 9bb487ecf7e..5e28b5f0dc8 100644 --- a/src/backend/distributed/master/master_stage_protocol.c +++ b/src/backend/distributed/master/master_stage_protocol.c @@ -29,9 +29,9 @@ #endif #include "distributed/citus_ruleutils.h" #include "distributed/colocation_utils.h" +#include "distributed/commands.h" #include "distributed/connection_management.h" #include "distributed/distributed_planner.h" -#include "distributed/foreign_constraint.h" #include "distributed/multi_client_executor.h" #include "distributed/multi_router_executor.h" #include "distributed/master_metadata_utility.h" diff --git a/src/backend/distributed/master/master_truncate.c b/src/backend/distributed/master/master_truncate.c index 828c7ff2997..0457a66e953 100644 --- a/src/backend/distributed/master/master_truncate.c +++ b/src/backend/distributed/master/master_truncate.c @@ -15,9 +15,10 @@ #include "commands/tablecmds.h" #include "commands/trigger.h" +#include "distributed/commands/utility_hook.h" #include "distributed/master_metadata_utility.h" +#include "distributed/master_protocol.h" #include "distributed/multi_join_order.h" -#include "distributed/multi_utility.h" #include "distributed/pg_dist_partition.h" #include "utils/builtins.h" #include "utils/lsyscache.h" diff --git a/src/backend/distributed/metadata/metadata_sync.c b/src/backend/distributed/metadata/metadata_sync.c index fa6f2ee6e21..babbec86d3b 100644 --- a/src/backend/distributed/metadata/metadata_sync.c +++ b/src/backend/distributed/metadata/metadata_sync.c @@ -28,8 +28,8 @@ #include "catalog/pg_namespace.h" #include "catalog/pg_type.h" #include "distributed/citus_ruleutils.h" +#include "distributed/commands.h" #include "distributed/distribution_column.h" -#include "distributed/foreign_constraint.h" #include "distributed/listutils.h" #include "distributed/master_metadata_utility.h" #include "distributed/master_protocol.h" diff --git a/src/backend/distributed/planner/recursive_planning.c b/src/backend/distributed/planner/recursive_planning.c index f5412d7bcbf..030b83ab89c 100644 --- a/src/backend/distributed/planner/recursive_planning.c +++ b/src/backend/distributed/planner/recursive_planning.c @@ -55,10 +55,10 @@ #include "catalog/pg_class.h" #include "distributed/citus_nodes.h" #include "distributed/citus_ruleutils.h" +#include "distributed/commands/multi_copy.h" #include "distributed/distributed_planner.h" #include "distributed/errormessage.h" #include "distributed/metadata_cache.h" -#include "distributed/multi_copy.h" #include "distributed/multi_logical_planner.h" #include "distributed/multi_router_planner.h" #include "distributed/multi_physical_planner.h" diff --git a/src/backend/distributed/relay/relay_event_utility.c b/src/backend/distributed/relay/relay_event_utility.c index c0c0f32bc3f..9d5cbd390ee 100644 --- a/src/backend/distributed/relay/relay_event_utility.c +++ b/src/backend/distributed/relay/relay_event_utility.c @@ -29,8 +29,8 @@ #include "catalog/namespace.h" #include "catalog/pg_class.h" #include "catalog/pg_constraint.h" +#include "distributed/commands.h" #include "distributed/metadata_cache.h" -#include "distributed/policy.h" #include "distributed/relay_utility.h" #include "distributed/version_compat.h" #include "lib/stringinfo.h" diff --git a/src/backend/distributed/shared_library_init.c b/src/backend/distributed/shared_library_init.c index 5a0a17e3e38..c490e954b93 100644 --- a/src/backend/distributed/shared_library_init.c +++ b/src/backend/distributed/shared_library_init.c @@ -21,13 +21,14 @@ #include "executor/executor.h" #include "distributed/backend_data.h" #include "distributed/citus_nodefuncs.h" +#include "distributed/commands/multi_copy.h" +#include "distributed/commands/utility_hook.h" #include "distributed/connection_management.h" #include "distributed/distributed_deadlock_detection.h" #include "distributed/maintenanced.h" #include "distributed/master_metadata_utility.h" #include "distributed/master_protocol.h" #include "distributed/metadata_cache.h" -#include "distributed/multi_copy.h" #include "distributed/multi_explain.h" #include "distributed/multi_join_order.h" #include "distributed/multi_logical_optimizer.h" @@ -35,7 +36,6 @@ #include "distributed/multi_router_executor.h" #include "distributed/multi_router_planner.h" #include "distributed/multi_server_executor.h" -#include "distributed/multi_utility.h" #include "distributed/pg_dist_partition.h" #include "distributed/placement_connection.h" #include "distributed/relation_access_tracking.h" diff --git a/src/backend/distributed/utils/citus_ruleutils.c b/src/backend/distributed/utils/citus_ruleutils.c index 340657538b6..bbe5707ce9e 100644 --- a/src/backend/distributed/utils/citus_ruleutils.c +++ b/src/backend/distributed/utils/citus_ruleutils.c @@ -1308,3 +1308,43 @@ simple_quote_literal(StringInfo buf, const char *val) } appendStringInfoChar(buf, '\''); } + + +/* + * RoleSpecString resolves the role specification to its string form that is suitable for transport to a worker node. + * This function resolves the following identifiers from the current context so they are safe to transfer. + * + * CURRENT_USER - resolved to the user name of the current role being used + * SESSION_USER - resolved to the user name of the user that opened the session + */ +const char * +RoleSpecString(RoleSpec *spec) +{ + switch (spec->roletype) + { + case ROLESPEC_CSTRING: + { + return quote_identifier(spec->rolename); + } + + case ROLESPEC_CURRENT_USER: + { + return quote_identifier(GetUserNameFromId(GetUserId(), false)); + } + + case ROLESPEC_SESSION_USER: + { + return quote_identifier(GetUserNameFromId(GetSessionUserId(), false)); + } + + case ROLESPEC_PUBLIC: + { + return "PUBLIC"; + } + + default: + { + elog(ERROR, "unexpected role type %d", spec->roletype); + } + } +} diff --git a/src/backend/distributed/utils/reference_table_utils.c b/src/backend/distributed/utils/reference_table_utils.c index ccefdae4bd6..a6b0b29f071 100644 --- a/src/backend/distributed/utils/reference_table_utils.c +++ b/src/backend/distributed/utils/reference_table_utils.c @@ -16,7 +16,7 @@ #include "access/htup_details.h" #include "access/genam.h" #include "distributed/colocation_utils.h" -#include "distributed/foreign_constraint.h" +#include "distributed/commands.h" #include "distributed/listutils.h" #include "distributed/master_protocol.h" #include "distributed/master_metadata_utility.h" diff --git a/src/backend/distributed/worker/worker_data_fetch_protocol.c b/src/backend/distributed/worker/worker_data_fetch_protocol.c index 0b8415ea1bf..2ebc56c3e68 100644 --- a/src/backend/distributed/worker/worker_data_fetch_protocol.c +++ b/src/backend/distributed/worker/worker_data_fetch_protocol.c @@ -27,6 +27,7 @@ #include "commands/extension.h" #include "commands/sequence.h" #include "distributed/citus_ruleutils.h" +#include "distributed/commands/utility_hook.h" #include "distributed/connection_management.h" #include "distributed/master_protocol.h" #include "distributed/metadata_cache.h" @@ -34,7 +35,6 @@ #include "distributed/multi_logical_optimizer.h" #include "distributed/multi_partitioning_utils.h" #include "distributed/multi_server_executor.h" -#include "distributed/multi_utility.h" #include "distributed/relay_utility.h" #include "distributed/remote_commands.h" #include "distributed/resource_lock.h" diff --git a/src/backend/distributed/worker/worker_partition_protocol.c b/src/backend/distributed/worker/worker_partition_protocol.c index 9be7dafda10..386e7fd2899 100644 --- a/src/backend/distributed/worker/worker_partition_protocol.c +++ b/src/backend/distributed/worker/worker_partition_protocol.c @@ -33,7 +33,7 @@ #include "catalog/pg_type.h" #include "commands/copy.h" #include "commands/defrem.h" -#include "distributed/multi_copy.h" +#include "distributed/commands/multi_copy.h" #include "distributed/multi_physical_planner.h" #include "distributed/resource_lock.h" #include "distributed/transmit.h" diff --git a/src/include/distributed/citus_ruleutils.h b/src/include/distributed/citus_ruleutils.h index 4f1413e2fe9..c6653c43f49 100644 --- a/src/include/distributed/citus_ruleutils.h +++ b/src/include/distributed/citus_ruleutils.h @@ -42,6 +42,7 @@ extern char * pg_get_indexclusterdef_string(Oid indexRelationId); extern List * pg_get_table_grants(Oid relationId); extern bool contain_nextval_expression_walker(Node *node, void *context); extern char * pg_get_replica_identity_command(Oid tableRelationId); +extern const char * RoleSpecString(RoleSpec *spec); /* Function declarations for version dependent PostgreSQL ruleutils functions */ extern void pg_get_query_def(Query *query, StringInfo buffer); diff --git a/src/include/distributed/commands.h b/src/include/distributed/commands.h new file mode 100644 index 00000000000..74b4bc0e681 --- /dev/null +++ b/src/include/distributed/commands.h @@ -0,0 +1,127 @@ +/*------------------------------------------------------------------------- + * + * commands.h + * Declarations for public functions and variables used for all commands + * and DDL operations for citus. All declarations are grouped by the + * file that implements them. + * + * Copyright (c) 2018, Citus Data, Inc. + * + *------------------------------------------------------------------------- + */ + +#ifndef CITUS_COMMANDS_H +#define CITUS_COMMANDS_H + +#include "postgres.h" + +#include "utils/rel.h" +#include "nodes/parsenodes.h" + + +/* cluster.c - forward declarations */ +extern List * PlanClusterStmt(ClusterStmt *clusterStmt, const char *clusterCommand); + + +/* extension.c - forward declarations */ +extern bool IsCitusExtensionStmt(Node *parsetree); +extern void ErrorIfUnstableCreateOrAlterExtensionStmt(Node *parsetree); + + +/* foreign_constraint.c - forward declarations */ +extern bool ConstraintIsAForeignKeyToReferenceTable(char *constraintName, + Oid leftRelationId); +extern void ErrorIfUnsupportedForeignConstraint(Relation relation, char + distributionMethod, + Var *distributionColumn, uint32 + colocationId); +extern bool ColumnAppearsInForeignKeyToReferenceTable(char *columnName, Oid + relationId); +extern List * GetTableForeignConstraintCommands(Oid relationId); +extern bool HasForeignKeyToReferenceTable(Oid relationId); +extern bool TableReferenced(Oid relationId); +extern bool TableReferencing(Oid relationId); +extern bool ConstraintIsAForeignKey(char *constraintName, Oid relationId); + + +/* grant.c - forward declarations */ +extern List * PlanGrantStmt(GrantStmt *grantStmt); + + +/* index.c - forward declarations */ +extern bool IsIndexRenameStmt(RenameStmt *renameStmt); +extern List * PlanIndexStmt(IndexStmt *createIndexStatement, + const char *createIndexCommand); +extern List * PlanDropIndexStmt(DropStmt *dropIndexStatement, + const char *dropIndexCommand); +extern void PostProcessIndexStmt(IndexStmt *indexStmt); +extern void ErrorIfUnsupportedAlterIndexStmt(AlterTableStmt *alterTableStatement); + + +/* policy.c - forward declarations */ +extern List * CreatePolicyCommands(Oid relationId); +extern void ErrorIfUnsupportedPolicy(Relation relation); +extern void ErrorIfUnsupportedPolicyExpr(Node *expr); +extern List * PlanCreatePolicyStmt(CreatePolicyStmt *stmt); +extern List * PlanAlterPolicyStmt(AlterPolicyStmt *stmt); +extern List * PlanDropPolicyStmt(DropStmt *stmt, const char *queryString); +extern bool IsPolicyRenameStmt(RenameStmt *stmt); +extern void CreatePolicyEventExtendNames(CreatePolicyStmt *stmt, const char *schemaName, + uint64 shardId); +extern void AlterPolicyEventExtendNames(AlterPolicyStmt *stmt, const char *schemaName, + uint64 shardId); +extern void RenamePolicyEventExtendNames(RenameStmt *stmt, const char *schemaName, uint64 + shardId); +extern void DropPolicyEventExtendNames(DropStmt *stmt, const char *schemaName, uint64 + shardId); + + +/* rename.c - forward declarations*/ +extern List * PlanRenameStmt(RenameStmt *renameStmt, const char *renameCommand); +extern void ErrorIfUnsupportedRenameStmt(RenameStmt *renameStmt); + + +/* schema.c - forward declarations */ +extern void ProcessDropSchemaStmt(DropStmt *dropSchemaStatement); +extern List * PlanAlterObjectSchemaStmt(AlterObjectSchemaStmt *alterObjectSchemaStmt, + const char *alterObjectSchemaCommand); + + +/* sequence.c - forward declarations */ +extern void ErrorIfUnsupportedSeqStmt(CreateSeqStmt *createSeqStmt); +extern void ErrorIfDistributedAlterSeqOwnedBy(AlterSeqStmt *alterSeqStmt); + + +#if (PG_VERSION_NUM >= 100000) + +/* subscription.c - forward declarations */ +extern Node * ProcessCreateSubscriptionStmt(CreateSubscriptionStmt *createSubStmt); +#endif /* PG_VERSION_NUM >= 100000 */ + + +/* table.c - forward declarations */ +extern void ProcessDropTableStmt(DropStmt *dropTableStatement); +extern void ProcessCreateTableStmtPartitionOf(CreateStmt *createStatement); +extern void ProcessAlterTableStmtAttachPartition(AlterTableStmt *alterTableStatement); +extern List * PlanAlterTableStmt(AlterTableStmt *alterTableStatement, + const char *alterTableCommand); +extern Node * WorkerProcessAlterTableStmt(AlterTableStmt *alterTableStatement, + const char *alterTableCommand); +extern bool IsAlterTableRenameStmt(RenameStmt *renameStmt); +extern void ErrorIfAlterDropsPartitionColumn(AlterTableStmt *alterTableStatement); +extern void PostProcessAlterTableStmt(AlterTableStmt *pStmt); +extern void ErrorUnsupportedAlterTableAddColumn(Oid relationId, AlterTableCmd *command, + Constraint *constraint); +extern void ErrorIfUnsupportedConstraint(Relation relation, char distributionMethod, + Var *distributionColumn, uint32 colocationId); + + +/* truncate.c - forward declarations */ +extern void ProcessTruncateStatement(TruncateStmt *truncateStatement); + + +/* vacuum.c - froward declarations */ +extern void ProcessVacuumStmt(VacuumStmt *vacuumStmt, const char *vacuumCommand); + + +#endif /*CITUS_COMMANDS_H */ diff --git a/src/include/distributed/multi_copy.h b/src/include/distributed/commands/multi_copy.h similarity index 95% rename from src/include/distributed/multi_copy.h rename to src/include/distributed/commands/multi_copy.h index deb74fa92f0..c4d7a8f4b67 100644 --- a/src/include/distributed/multi_copy.h +++ b/src/include/distributed/commands/multi_copy.h @@ -128,9 +128,8 @@ extern void AppendCopyRowData(Datum *valueArray, bool *isNullArray, extern void AppendCopyBinaryHeaders(CopyOutState headerOutputState); extern void AppendCopyBinaryFooters(CopyOutState footerOutputState); extern void EndRemoteCopy(int64 shardId, List *connectionList, bool stopOnFailure); -extern void CitusCopyFrom(CopyStmt *copyStatement, char *completionTag); -extern bool IsCopyFromWorker(CopyStmt *copyStatement); -extern NodeAddress * MasterNodeAddress(CopyStmt *copyStatement); +extern Node * ProcessCopyStmt(CopyStmt *copyStatement, char *completionTag, + bool *commandMustRunAsOwner); #endif /* MULTI_COPY_H */ diff --git a/src/include/distributed/multi_utility.h b/src/include/distributed/commands/utility_hook.h similarity index 76% rename from src/include/distributed/multi_utility.h rename to src/include/distributed/commands/utility_hook.h index a10174fbb76..68c078d24e5 100644 --- a/src/include/distributed/multi_utility.h +++ b/src/include/distributed/commands/utility_hook.h @@ -1,6 +1,6 @@ /*------------------------------------------------------------------------- * - * multi_utility.h + * utility_hook.h * Citus utility hook and related functionality. * * Copyright (c) 2012-2016, Citus Data, Inc. @@ -10,10 +10,14 @@ #ifndef MULTI_UTILITY_H #define MULTI_UTILITY_H +#include "postgres.h" + +#include "distributed/version_compat.h" +#include "utils/relcache.h" #include "tcop/utility.h" extern bool EnableDDLPropagation; -extern bool EnableVersionChecks; + /* * A DDLJob encapsulates the remote tasks and commands needed to process all or @@ -30,9 +34,6 @@ typedef struct DDLJob List *taskList; /* worker DDL tasks to execute */ } DDLJob; -#if (PG_VERSION_NUM < 100000) -struct QueryEnvironment; /* forward-declare to appease compiler */ -#endif extern void multi_ProcessUtility(PlannedStmt *pstmt, const char *queryString, ProcessUtilityContext context, ParamListInfo params, @@ -44,14 +45,8 @@ extern void multi_ProcessUtility9x(Node *parsetree, const char *queryString, extern void CitusProcessUtility(Node *node, const char *queryString, ProcessUtilityContext context, ParamListInfo params, DestReceiver *dest, char *completionTag); -extern List * PlanGrantStmt(GrantStmt *grantStmt); -extern void ErrorIfUnsupportedConstraint(Relation relation, char distributionMethod, - Var *distributionColumn, uint32 colocationId); - -extern Datum master_drop_all_shards(PG_FUNCTION_ARGS); -extern Datum master_modify_multiple_shards(PG_FUNCTION_ARGS); - +extern void MarkInvalidateForeignKeyGraph(void); +extern void InvalidateForeignKeyGraphForDDL(void); extern List * DDLTaskList(Oid relationId, const char *commandString); -extern const char * RoleSpecString(RoleSpec *spec); #endif /* MULTI_UTILITY_H */ diff --git a/src/include/distributed/foreign_constraint.h b/src/include/distributed/foreign_constraint.h deleted file mode 100644 index 732f22bf34a..00000000000 --- a/src/include/distributed/foreign_constraint.h +++ /dev/null @@ -1,32 +0,0 @@ -/*------------------------------------------------------------------------- - * foreign_constraint.h - * - * Copyright (c) 2016, Citus Data, Inc. - * - *------------------------------------------------------------------------- - */ - -#ifndef FOREIGN_CONSTRAINT_H -#define FOREIGN_CONSTRAINT_H - -#include "postgres.h" -#include "postgres_ext.h" -#include "utils/relcache.h" -#include "utils/hsearch.h" -#include "nodes/primnodes.h" - -extern bool ConstraintIsAForeignKeyToReferenceTable(char *constraintName, - Oid leftRelationId); -extern void ErrorIfUnsupportedForeignConstraint(Relation relation, char - distributionMethod, - Var *distributionColumn, uint32 - colocationId); -extern bool ColumnAppearsInForeignKeyToReferenceTable(char *columnName, Oid - relationId); -extern List * GetTableForeignConstraintCommands(Oid relationId); -extern bool HasForeignKeyToReferenceTable(Oid relationId); -extern bool TableReferenced(Oid relationId); -extern bool TableReferencing(Oid relationId); -extern bool ConstraintIsAForeignKey(char *constraintName, Oid relationId); - -#endif diff --git a/src/include/distributed/intermediate_results.h b/src/include/distributed/intermediate_results.h index e52a273ef8e..e95ddd35b24 100644 --- a/src/include/distributed/intermediate_results.h +++ b/src/include/distributed/intermediate_results.h @@ -14,7 +14,7 @@ #include "fmgr.h" -#include "distributed/multi_copy.h" +#include "distributed/commands/multi_copy.h" #include "nodes/execnodes.h" #include "nodes/pg_list.h" #include "tcop/dest.h" diff --git a/src/include/distributed/policy.h b/src/include/distributed/policy.h deleted file mode 100644 index 65ac6c18be6..00000000000 --- a/src/include/distributed/policy.h +++ /dev/null @@ -1,32 +0,0 @@ -/*------------------------------------------------------------------------- - * policy.h - * - * Copyright (c) 2018, Citus Data, Inc. - * - *------------------------------------------------------------------------- - */ - -#ifndef CITUS_POLICY_H -#define CITUS_POLICY_H - -#include "utils/rel.h" - -extern List * CreatePolicyCommands(Oid relationId); -extern void ErrorIfUnsupportedPolicy(Relation relation); -extern void ErrorIfUnsupportedPolicyExpr(Node *expr); - -extern List * PlanCreatePolicyStmt(CreatePolicyStmt *stmt); -extern List * PlanAlterPolicyStmt(AlterPolicyStmt *stmt); -extern List * PlanDropPolicyStmt(DropStmt *stmt, const char *queryString); -extern bool IsPolicyRenameStmt(RenameStmt *stmt); - -extern void CreatePolicyEventExtendNames(CreatePolicyStmt *stmt, const char *schemaName, - uint64 shardId); -extern void AlterPolicyEventExtendNames(AlterPolicyStmt *stmt, const char *schemaName, - uint64 shardId); -extern void RenamePolicyEventExtendNames(RenameStmt *stmt, const char *schemaName, uint64 - shardId); -extern void DropPolicyEventExtendNames(DropStmt *stmt, const char *schemaName, uint64 - shardId); - -#endif /*CITUS_POLICY_H */ diff --git a/src/include/distributed/transmit.h b/src/include/distributed/transmit.h index 32e2c84d5e9..28ed0dad343 100644 --- a/src/include/distributed/transmit.h +++ b/src/include/distributed/transmit.h @@ -11,7 +11,10 @@ #ifndef TRANSMIT_H #define TRANSMIT_H +#include "c.h" + #include "lib/stringinfo.h" +#include "nodes/parsenodes.h" #include "storage/fd.h" @@ -23,5 +26,9 @@ extern File FileOpenForTransmit(const char *filename, int fileFlags, int fileMod /* Function declaration local to commands and worker modules */ extern void FreeStringInfo(StringInfo stringInfo); +/* Local functions forward declarations for Transmit statement */ +extern bool IsTransmitStmt(Node *parsetree); +extern void VerifyTransmitStmt(CopyStmt *copyStatement); + #endif /* TRANSMIT_H */ diff --git a/src/include/distributed/version_compat.h b/src/include/distributed/version_compat.h index 19c8025b94a..f7e1e09e016 100644 --- a/src/include/distributed/version_compat.h +++ b/src/include/distributed/version_compat.h @@ -24,11 +24,16 @@ #endif +#if (PG_VERSION_NUM < 100000) +struct QueryEnvironment; /* forward-declare to appease compiler */ +#endif + #if (PG_VERSION_NUM >= 90600 && PG_VERSION_NUM < 110000) #include "access/hash.h" #include "storage/fd.h" #include "optimizer/prep.h" +#include "postmaster/bgworker.h" #include "utils/memutils.h" /* PostgreSQL 11 splits hash procs into "standard" and "extended" */