/* * RecordRelationAccess associates the access to the distributed relation. The * function takes partitioned relations into account as well. * * We implemented this function to prevent accessing placement metadata during * recursive calls of the function itself (e.g., avoid * AssociatePlacementAccessWithRelation()). */ static void RecordRelationAccess(Oid relationId, ShardPlacementAccessType accessType) { /* make sure that this is not a conflicting access */ CheckConflictingRelationAccesses(relationId, accessType); /* * If a relation is partitioned, record accesses to all of its partitions as well. * We prefer to use PartitionedTableNoLock() because at this point the necessary * locks on the relation has already been acquired. */ if (PartitionedTableNoLock(relationId)) { List *partitionList = PartitionList(relationId); ListCell *partitionCell = NULL; foreach(partitionCell, partitionList) { Oid partitionOid = lfirst_oid(partitionCell); /* * During create_distributed_table, the partitions may not * have been created yet and so there are no placements yet. * We're already going to register them when we distribute * the partitions. */ if (!IsDistributedTable(partitionOid)) { continue; } /* recursively call the function to cover multi-level partitioned tables */ RecordRelationAccess(partitionOid, accessType); } }
/* * multi_relation_restriction_hook is a hook called by postgresql standard planner * to notify us about various planning information regarding a relation. We use * it to retrieve restrictions on relations. */ void multi_relation_restriction_hook(PlannerInfo *root, RelOptInfo *relOptInfo, Index index, RangeTblEntry *rte) { RelationRestrictionContext *restrictionContext = NULL; RelationRestriction *relationRestriction = NULL; bool distributedTable = false; bool localTable = false; if (rte->rtekind != RTE_RELATION) { return; } distributedTable = IsDistributedTable(rte->relid); localTable = !distributedTable; restrictionContext = CurrentRestrictionContext(); Assert(restrictionContext != NULL); relationRestriction = palloc0(sizeof(RelationRestriction)); relationRestriction->index = index; relationRestriction->relationId = rte->relid; relationRestriction->rte = rte; relationRestriction->relOptInfo = relOptInfo; relationRestriction->distributedRelation = distributedTable; relationRestriction->plannerInfo = root; relationRestriction->prunedShardIntervalList = NIL; restrictionContext->hasDistributedRelation |= distributedTable; restrictionContext->hasLocalRelation |= localTable; restrictionContext->relationRestrictionList = lappend(restrictionContext->relationRestrictionList, relationRestriction); }
/* * ConstraintIsAForeignKeyToReferenceTable function scans the pgConstraint to * fetch all of the constraints on the given relationId and see if at least one * of them is a foreign key referencing to a reference table. */ bool ConstraintIsAForeignKeyToReferenceTable(char *constraintName, Oid relationId) { Relation pgConstraint = NULL; SysScanDesc scanDescriptor = NULL; ScanKeyData scanKey[1]; int scanKeyCount = 1; HeapTuple heapTuple = NULL; bool foreignKeyToReferenceTable = false; pgConstraint = heap_open(ConstraintRelationId, AccessShareLock); ScanKeyInit(&scanKey[0], Anum_pg_constraint_contype, BTEqualStrategyNumber, F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN)); scanDescriptor = systable_beginscan(pgConstraint, InvalidOid, false, NULL, scanKeyCount, scanKey); heapTuple = systable_getnext(scanDescriptor); while (HeapTupleIsValid(heapTuple)) { Oid referencedTableId = InvalidOid; Form_pg_constraint constraintForm = (Form_pg_constraint) GETSTRUCT(heapTuple); char *constraintName = (constraintForm->conname).data; if (strncmp(constraintName, constraintName, NAMEDATALEN) != 0 || constraintForm->conrelid != relationId) { heapTuple = systable_getnext(scanDescriptor); continue; } referencedTableId = constraintForm->confrelid; Assert(IsDistributedTable(referencedTableId)); if (PartitionMethod(referencedTableId) == DISTRIBUTE_BY_NONE) { foreignKeyToReferenceTable = true; break; } heapTuple = systable_getnext(scanDescriptor); } /* clean up scan and close system catalog */ systable_endscan(scanDescriptor); heap_close(pgConstraint, AccessShareLock); return foreignKeyToReferenceTable; }
/* * HasForeignKeyToReferenceTable function scans the pgConstraint table to * fetch all of the constraints on the given relationId and see if at least one * of them is a foreign key referencing to a reference table. */ bool HasForeignKeyToReferenceTable(Oid relationId) { Relation pgConstraint = NULL; SysScanDesc scanDescriptor = NULL; ScanKeyData scanKey[1]; int scanKeyCount = 1; HeapTuple heapTuple = NULL; bool hasForeignKeyToReferenceTable = false; pgConstraint = heap_open(ConstraintRelationId, AccessShareLock); ScanKeyInit(&scanKey[0], Anum_pg_constraint_conrelid, BTEqualStrategyNumber, F_OIDEQ, relationId); scanDescriptor = systable_beginscan(pgConstraint, ConstraintRelidIndexId, true, NULL, scanKeyCount, scanKey); heapTuple = systable_getnext(scanDescriptor); while (HeapTupleIsValid(heapTuple)) { Oid referencedTableId = InvalidOid; Form_pg_constraint constraintForm = (Form_pg_constraint) GETSTRUCT(heapTuple); if (constraintForm->contype != CONSTRAINT_FOREIGN) { heapTuple = systable_getnext(scanDescriptor); continue; } referencedTableId = constraintForm->confrelid; if (!IsDistributedTable(referencedTableId)) { continue; } if (PartitionMethod(referencedTableId) == DISTRIBUTE_BY_NONE) { hasForeignKeyToReferenceTable = true; break; } heapTuple = systable_getnext(scanDescriptor); } /* clean up scan and close system catalog */ systable_endscan(scanDescriptor); heap_close(pgConstraint, NoLock); return hasForeignKeyToReferenceTable; }
/* * master_drop_all_shards attempts to drop all shards for a given relation. * Unlike master_apply_delete_command, this function can be called even * if the table has already been dropped. */ Datum master_drop_all_shards(PG_FUNCTION_ARGS) { Oid relationId = PG_GETARG_OID(0); text *schemaNameText = PG_GETARG_TEXT_P(1); text *relationNameText = PG_GETARG_TEXT_P(2); List *shardIntervalList = NIL; int droppedShardCount = 0; char *schemaName = text_to_cstring(schemaNameText); char *relationName = text_to_cstring(relationNameText); CheckCitusVersion(ERROR); /* * The SQL_DROP trigger calls this function even for tables that are * not distributed. In that case, silently ignore and return -1. */ if (!IsDistributedTable(relationId) || !EnableDDLPropagation) { PG_RETURN_INT32(-1); } EnsureCoordinator(); CheckTableSchemaNameForDrop(relationId, &schemaName, &relationName); /* * master_drop_all_shards is typically called from the DROP TABLE trigger, * but could be called by a user directly. Make sure we have an * AccessExlusiveLock to prevent any other commands from running on this table * concurrently. */ LockRelationOid(relationId, AccessExclusiveLock); shardIntervalList = LoadShardIntervalList(relationId); droppedShardCount = DropShards(relationId, schemaName, relationName, shardIntervalList); PG_RETURN_INT32(droppedShardCount); }
/* * ColumnAppearsInForeignKeyToReferenceTable checks if there is foreign constraint * from/to a reference table on the given column. We iterate pgConstraint to fetch * the constraint on the given relationId and find if any of the constraints * includes the given column. */ bool ColumnAppearsInForeignKeyToReferenceTable(char *columnName, Oid relationId) { Relation pgConstraint = NULL; SysScanDesc scanDescriptor = NULL; ScanKeyData scanKey[1]; int scanKeyCount = 1; HeapTuple heapTuple = NULL; bool foreignKeyToReferenceTableIncludesGivenColumn = false; pgConstraint = heap_open(ConstraintRelationId, AccessShareLock); ScanKeyInit(&scanKey[0], Anum_pg_constraint_contype, BTEqualStrategyNumber, F_CHAREQ, CharGetDatum(CONSTRAINT_FOREIGN)); scanDescriptor = systable_beginscan(pgConstraint, InvalidOid, false, NULL, scanKeyCount, scanKey); heapTuple = systable_getnext(scanDescriptor); while (HeapTupleIsValid(heapTuple)) { Oid referencedTableId = InvalidOid; Oid referencingTableId = InvalidOid; int pgConstraintKey = 0; Form_pg_constraint constraintForm = (Form_pg_constraint) GETSTRUCT(heapTuple); referencedTableId = constraintForm->confrelid; referencingTableId = constraintForm->conrelid; if (referencedTableId == relationId) { pgConstraintKey = Anum_pg_constraint_confkey; } else if (referencingTableId == relationId) { pgConstraintKey = Anum_pg_constraint_conkey; } else { /* * If the constraint is not from/to the given relation, we should simply * skip. */ heapTuple = systable_getnext(scanDescriptor); continue; } /* * We check if the referenced table is a reference table. There cannot be * any foreign constraint from a distributed table to a local table. */ Assert(IsDistributedTable(referencedTableId)); if (PartitionMethod(referencedTableId) != DISTRIBUTE_BY_NONE) { heapTuple = systable_getnext(scanDescriptor); continue; } if (HeapTupleOfForeignConstraintIncludesColumn(heapTuple, relationId, pgConstraintKey, columnName)) { foreignKeyToReferenceTableIncludesGivenColumn = true; break; } heapTuple = systable_getnext(scanDescriptor); } /* clean up scan and close system catalog */ systable_endscan(scanDescriptor); heap_close(pgConstraint, AccessShareLock); return foreignKeyToReferenceTableIncludesGivenColumn; }
/* * ErrorIfUnsupportedForeignConstraint runs checks related to foreign constraints and * errors out if it is not possible to create one of the foreign constraint in distributed * environment. * * To support foreign constraints, we require that; * - If referencing and referenced tables are hash-distributed * - Referencing and referenced tables are co-located. * - Foreign constraint is defined over distribution column. * - ON DELETE/UPDATE SET NULL, ON DELETE/UPDATE SET DEFAULT and ON UPDATE CASCADE options * are not used. * - Replication factors of referencing and referenced table are 1. * - If referenced table is a reference table * - ON DELETE/UPDATE SET NULL, ON DELETE/UPDATE SET DEFAULT and ON UPDATE CASCADE options * are not used on the distribution key of the referencing column. * - If referencing table is a reference table, error out */ void ErrorIfUnsupportedForeignConstraint(Relation relation, char distributionMethod, Var *distributionColumn, uint32 colocationId) { Relation pgConstraint = NULL; SysScanDesc scanDescriptor = NULL; ScanKeyData scanKey[1]; int scanKeyCount = 1; HeapTuple heapTuple = NULL; Oid referencingTableId = relation->rd_id; Oid referencedTableId = InvalidOid; uint32 referencedTableColocationId = INVALID_COLOCATION_ID; Var *referencedTablePartitionColumn = NULL; Datum referencingColumnsDatum = 0; Datum *referencingColumnArray = NULL; int referencingColumnCount = 0; Datum referencedColumnsDatum = 0; Datum *referencedColumnArray = NULL; int referencedColumnCount = 0; bool isNull = false; int attrIdx = 0; bool foreignConstraintOnPartitionColumn = false; bool selfReferencingTable = false; bool referencedTableIsAReferenceTable = false; bool referencingColumnsIncludeDistKey = false; pgConstraint = heap_open(ConstraintRelationId, AccessShareLock); ScanKeyInit(&scanKey[0], Anum_pg_constraint_conrelid, BTEqualStrategyNumber, F_OIDEQ, relation->rd_id); scanDescriptor = systable_beginscan(pgConstraint, ConstraintRelidIndexId, true, NULL, scanKeyCount, scanKey); heapTuple = systable_getnext(scanDescriptor); while (HeapTupleIsValid(heapTuple)) { Form_pg_constraint constraintForm = (Form_pg_constraint) GETSTRUCT(heapTuple); bool singleReplicatedTable = true; if (constraintForm->contype != CONSTRAINT_FOREIGN) { heapTuple = systable_getnext(scanDescriptor); continue; } /* * We should make this check in this loop because the error message will only * be given if the table has a foreign constraint and the table is a reference * table. */ if (distributionMethod == DISTRIBUTE_BY_NONE) { ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot create foreign key constraint because " "reference tables are not supported as the " "referencing table of a foreign constraint"), errdetail("Reference tables are only supported as the " "referenced table of a foreign key when the " "referencing table is a hash distributed " "table"))); } referencedTableId = constraintForm->confrelid; selfReferencingTable = referencingTableId == referencedTableId; /* * Some checks are not meaningful if foreign key references the table itself. * Therefore we will skip those checks. */ if (!selfReferencingTable) { if (!IsDistributedTable(referencedTableId)) { ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("cannot create foreign key constraint"), errdetail("Referenced table must be a distributed " "table."))); } /* * PartitionMethod errors out when it is called for non-distributed * tables. This is why we make this check under !selfReferencingTable * and after !IsDistributedTable(referencedTableId). */ if (PartitionMethod(referencedTableId) == DISTRIBUTE_BY_NONE) { referencedTableIsAReferenceTable = true; } /* * To enforce foreign constraints, tables must be co-located unless a * reference table is referenced. */ referencedTableColocationId = TableColocationId(referencedTableId); if (colocationId == INVALID_COLOCATION_ID || (colocationId != referencedTableColocationId && !referencedTableIsAReferenceTable)) { ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot create foreign key constraint since " "relations are not colocated or not referencing " "a reference table"), errdetail( "A distributed table can only have foreign keys " "if it is referencing another colocated hash " "distributed table or a reference table"))); } referencedTablePartitionColumn = DistPartitionKey(referencedTableId); } else { /* * If the referenced table is not a reference table, the distribution * column in referencing table should be the distribution column in * referenced table as well. */ referencedTablePartitionColumn = distributionColumn; } /* * Column attributes are not available in Form_pg_constraint, therefore we need * to find them in the system catalog. After finding them, we iterate over column * attributes together because partition column must be at the same place in both * referencing and referenced side of the foreign key constraint */ referencingColumnsDatum = SysCacheGetAttr(CONSTROID, heapTuple, Anum_pg_constraint_conkey, &isNull); referencedColumnsDatum = SysCacheGetAttr(CONSTROID, heapTuple, Anum_pg_constraint_confkey, &isNull); deconstruct_array(DatumGetArrayTypeP(referencingColumnsDatum), INT2OID, 2, true, 's', &referencingColumnArray, NULL, &referencingColumnCount); deconstruct_array(DatumGetArrayTypeP(referencedColumnsDatum), INT2OID, 2, true, 's', &referencedColumnArray, NULL, &referencedColumnCount); Assert(referencingColumnCount == referencedColumnCount); for (attrIdx = 0; attrIdx < referencingColumnCount; ++attrIdx) { AttrNumber referencingAttrNo = DatumGetInt16(referencingColumnArray[attrIdx]); AttrNumber referencedAttrNo = DatumGetInt16(referencedColumnArray[attrIdx]); if (distributionColumn->varattno == referencingAttrNo && (!referencedTableIsAReferenceTable && referencedTablePartitionColumn->varattno == referencedAttrNo)) { foreignConstraintOnPartitionColumn = true; } if (distributionColumn->varattno == referencingAttrNo) { referencingColumnsIncludeDistKey = true; } } /* * If columns in the foreign key includes the distribution key from the * referencing side, we do not allow update/delete operations through * foreign key constraints (e.g. ... ON UPDATE SET NULL) */ if (referencingColumnsIncludeDistKey) { /* * ON DELETE SET NULL and ON DELETE SET DEFAULT is not supported. Because we do * not want to set partition column to NULL or default value. */ if (constraintForm->confdeltype == FKCONSTR_ACTION_SETNULL || constraintForm->confdeltype == FKCONSTR_ACTION_SETDEFAULT) { ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot create foreign key constraint"), errdetail("SET NULL or SET DEFAULT is not supported" " in ON DELETE operation when distribution " "key is included in the foreign key constraint"))); } /* * ON UPDATE SET NULL, ON UPDATE SET DEFAULT and UPDATE CASCADE is not supported. * Because we do not want to set partition column to NULL or default value. Also * cascading update operation would require re-partitioning. Updating partition * column value is not allowed anyway even outside of foreign key concept. */ if (constraintForm->confupdtype == FKCONSTR_ACTION_SETNULL || constraintForm->confupdtype == FKCONSTR_ACTION_SETDEFAULT || constraintForm->confupdtype == FKCONSTR_ACTION_CASCADE) { ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot create foreign key constraint"), errdetail("SET NULL, SET DEFAULT or CASCADE is not " "supported in ON UPDATE operation when " "distribution key included in the foreign " "constraint."))); } } /* * if tables are hash-distributed and colocated, we need to make sure that * the distribution key is included in foreign constraint. */ if (!referencedTableIsAReferenceTable && !foreignConstraintOnPartitionColumn) { ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot create foreign key constraint"), errdetail("Foreign keys are supported in two cases, " "either in between two colocated tables including " "partition column in the same ordinal in the both " "tables or from distributed to reference tables"))); } /* * We do not allow to create foreign constraints if shard replication factor is * greater than 1. Because in our current design, multiple replicas may cause * locking problems and inconsistent shard contents. * * Note that we allow referenced table to be a reference table (e.g., not a * single replicated table). This is allowed since (a) we are sure that * placements always be in the same state (b) executors are aware of reference * tables and handle concurrency related issues accordingly. */ if (IsDistributedTable(referencingTableId)) { /* check whether ALTER TABLE command is applied over single replicated table */ if (!SingleReplicatedTable(referencingTableId)) { singleReplicatedTable = false; } } else { Assert(distributionMethod == DISTRIBUTE_BY_HASH); /* check whether creating single replicated table with foreign constraint */ if (ShardReplicationFactor > 1) { singleReplicatedTable = false; } } if (!singleReplicatedTable) { ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot create foreign key constraint"), errdetail("Citus Community Edition currently supports " "foreign key constraints only for " "\"citus.shard_replication_factor = 1\"."), errhint("Please change \"citus.shard_replication_factor to " "1\". To learn more about using foreign keys with " "other replication factors, please contact us at " "https://citusdata.com/about/contact_us."))); } heapTuple = systable_getnext(scanDescriptor); } /* clean up scan and close system catalog */ systable_endscan(scanDescriptor); heap_close(pgConstraint, AccessShareLock); }
/* * ConvertToDistributedTable converts the given regular PostgreSQL table into a * distributed table. First, it checks if the given table can be distributed, * then it creates related tuple in pg_dist_partition. * * XXX: We should perform more checks here to see if this table is fit for * partitioning. At a minimum, we should validate the following: (i) this node * runs as the master node, (ii) table does not make use of the inheritance * mechanism, (iii) table does not own columns that are sequences, and (iv) * table does not have collated columns. */ static void ConvertToDistributedTable(Oid relationId, char *distributionColumnName, char distributionMethod, uint32 colocationId) { Relation relation = NULL; TupleDesc relationDesc = NULL; char *relationName = NULL; char relationKind = 0; Var *distributionColumn = NULL; /* * Lock target relation with an exclusive lock - there's no way to make * sense of this table until we've committed, and we don't want multiple * backends manipulating this relation. */ relation = relation_open(relationId, ExclusiveLock); relationDesc = RelationGetDescr(relation); relationName = RelationGetRelationName(relation); EnsureTableOwner(relationId); /* check that the relation is not already distributed */ if (IsDistributedTable(relationId)) { ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("table \"%s\" is already distributed", relationName))); } /* verify target relation does not use WITH (OIDS) PostgreSQL feature */ if (relationDesc->tdhasoid) { ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot distribute relation: %s", relationName), errdetail("Distributed relations must not specify the WITH " "(OIDS) option in their definitions."))); } /* verify target relation is either regular or foreign table */ relationKind = relation->rd_rel->relkind; if (relationKind != RELKIND_RELATION && relationKind != RELKIND_FOREIGN_TABLE) { ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot distribute relation: %s", relationName), errdetail("Distributed relations must be regular or " "foreign tables."))); } /* check that the relation does not contain any rows */ if (!LocalTableEmpty(relationId)) { ereport(ERROR, (errcode(ERRCODE_INVALID_TABLE_DEFINITION), errmsg("cannot distribute relation \"%s\"", relationName), errdetail("Relation \"%s\" contains data.", relationName), errhint("Empty your table before distributing it."))); } distributionColumn = BuildDistributionKeyFromColumnName(relation, distributionColumnName); /* check for support function needed by specified partition method */ if (distributionMethod == DISTRIBUTE_BY_HASH) { Oid hashSupportFunction = SupportFunctionForColumn(distributionColumn, HASH_AM_OID, HASHPROC); if (hashSupportFunction == InvalidOid) { ereport(ERROR, (errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("could not identify a hash function for type %s", format_type_be(distributionColumn->vartype)), errdatatype(distributionColumn->vartype), errdetail("Partition column types must have a hash function " "defined to use hash partitioning."))); } } else if (distributionMethod == DISTRIBUTE_BY_RANGE) { Oid btreeSupportFunction = SupportFunctionForColumn(distributionColumn, BTREE_AM_OID, BTORDER_PROC); if (btreeSupportFunction == InvalidOid) { ereport(ERROR, (errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("could not identify a comparison function for type %s", format_type_be(distributionColumn->vartype)), errdatatype(distributionColumn->vartype), errdetail("Partition column types must have a comparison function " "defined to use range partitioning."))); } } ErrorIfNotSupportedConstraint(relation, distributionMethod, distributionColumn, colocationId); InsertIntoPgDistPartition(relationId, distributionMethod, distributionColumn, colocationId); relation_close(relation, NoLock); /* * PostgreSQL supports truncate trigger for regular relations only. * Truncate on foreign tables is not supported. */ if (relationKind == RELKIND_RELATION) { CreateTruncateTrigger(relationId); } }