/*------------------------------------------------------------------------- * * publicationcmds.c * publication manipulation * * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * IDENTIFICATION * src/backend/commands/publicationcmds.c * *------------------------------------------------------------------------- */ #include "postgres.h" #include "access/genam.h" #include "access/htup_details.h" #include "access/table.h" #include "access/xact.h" #include "catalog/catalog.h" #include "catalog/indexing.h" #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/objectaddress.h" #include "catalog/partition.h" #include "catalog/pg_database.h" #include "catalog/pg_inherits.h" #include "catalog/pg_namespace.h" #include "catalog/pg_proc.h" #include "catalog/pg_publication.h" #include "catalog/pg_publication_namespace.h" #include "catalog/pg_publication_rel.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" #include "commands/defrem.h" #include "commands/event_trigger.h" #include "commands/publicationcmds.h" #include "funcapi.h" #include "miscadmin.h" #include "nodes/nodeFuncs.h" #include "parser/parse_clause.h" #include "parser/parse_collate.h" #include "parser/parse_relation.h" #include "storage/lmgr.h" #include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/catcache.h" #include "utils/fmgroids.h" #include "utils/inval.h" #include "utils/lsyscache.h" #include "utils/rel.h" #include "utils/syscache.h" #include "utils/varlena.h" /* * Information used to validate the columns in the row filter expression. See * contain_invalid_rfcolumn_walker for details. */ typedef struct rf_context { Bitmapset *bms_replident; /* bitset of replica identity columns */ bool pubviaroot; /* true if we are validating the parent * relation's row filter */ Oid relid; /* relid of the relation */ Oid parentid; /* relid of the parent relation */ } rf_context; static List *OpenTableList(List *tables); static void CloseTableList(List *rels); static void LockSchemaList(List *schemalist); static void PublicationAddTables(Oid pubid, List *rels, bool if_not_exists, AlterPublicationStmt *stmt); static void PublicationDropTables(Oid pubid, List *rels, bool missing_ok); static void PublicationAddSchemas(Oid pubid, List *schemas, bool if_not_exists, AlterPublicationStmt *stmt); static void PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok); static void parse_publication_options(ParseState *pstate, List *options, bool *publish_given, PublicationActions *pubactions, bool *publish_via_partition_root_given, bool *publish_via_partition_root) { ListCell *lc; *publish_given = false; *publish_via_partition_root_given = false; /* defaults */ pubactions->pubinsert = true; pubactions->pubupdate = true; pubactions->pubdelete = true; pubactions->pubtruncate = true; *publish_via_partition_root = false; /* Parse options */ foreach(lc, options) { DefElem *defel = (DefElem *) lfirst(lc); if (strcmp(defel->defname, "publish") == 0) { char *publish; List *publish_list; ListCell *lc2; if (*publish_given) errorConflictingDefElem(defel, pstate); /* * If publish option was given only the explicitly listed actions * should be published. */ pubactions->pubinsert = false; pubactions->pubupdate = false; pubactions->pubdelete = false; pubactions->pubtruncate = false; *publish_given = true; publish = defGetString(defel); if (!SplitIdentifierString(publish, ',', &publish_list)) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("invalid list syntax in parameter \"%s\"", "publish"))); /* Process the option list. */ foreach(lc2, publish_list) { char *publish_opt = (char *) lfirst(lc2); if (strcmp(publish_opt, "insert") == 0) pubactions->pubinsert = true; else if (strcmp(publish_opt, "update") == 0) pubactions->pubupdate = true; else if (strcmp(publish_opt, "delete") == 0) pubactions->pubdelete = true; else if (strcmp(publish_opt, "truncate") == 0) pubactions->pubtruncate = true; else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("unrecognized value for publication option \"%s\": \"%s\"", "publish", publish_opt))); } } else if (strcmp(defel->defname, "publish_via_partition_root") == 0) { if (*publish_via_partition_root_given) errorConflictingDefElem(defel, pstate); *publish_via_partition_root_given = true; *publish_via_partition_root = defGetBoolean(defel); } else ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("unrecognized publication parameter: \"%s\"", defel->defname))); } } /* * Convert the PublicationObjSpecType list into schema oid list and * PublicationTable list. */ static void ObjectsInPublicationToOids(List *pubobjspec_list, ParseState *pstate, List **rels, List **schemas) { ListCell *cell; PublicationObjSpec *pubobj; if (!pubobjspec_list) return; foreach(cell, pubobjspec_list) { Oid schemaid; List *search_path; pubobj = (PublicationObjSpec *) lfirst(cell); switch (pubobj->pubobjtype) { case PUBLICATIONOBJ_TABLE: *rels = lappend(*rels, pubobj->pubtable); break; case PUBLICATIONOBJ_TABLES_IN_SCHEMA: schemaid = get_namespace_oid(pubobj->name, false); /* Filter out duplicates if user specifies "sch1, sch1" */ *schemas = list_append_unique_oid(*schemas, schemaid); break; case PUBLICATIONOBJ_TABLES_IN_CUR_SCHEMA: search_path = fetch_search_path(false); if (search_path == NIL) /* nothing valid in search_path? */ ereport(ERROR, errcode(ERRCODE_UNDEFINED_SCHEMA), errmsg("no schema has been selected for CURRENT_SCHEMA")); schemaid = linitial_oid(search_path); list_free(search_path); /* Filter out duplicates if user specifies "sch1, sch1" */ *schemas = list_append_unique_oid(*schemas, schemaid); break; default: /* shouldn't happen */ elog(ERROR, "invalid publication object type %d", pubobj->pubobjtype); break; } } } /* * Returns true if any of the columns used in the row filter WHERE expression is * not part of REPLICA IDENTITY, false otherwise. */ static bool contain_invalid_rfcolumn_walker(Node *node, rf_context *context) { if (node == NULL) return false; if (IsA(node, Var)) { Var *var = (Var *) node; AttrNumber attnum = var->varattno; /* * If pubviaroot is true, we are validating the row filter of the * parent table, but the bitmap contains the replica identity * information of the child table. So, get the column number of the * child table as parent and child column order could be different. */ if (context->pubviaroot) { char *colname = get_attname(context->parentid, attnum, false); attnum = get_attnum(context->relid, colname); } if (!bms_is_member(attnum - FirstLowInvalidHeapAttributeNumber, context->bms_replident)) return true; } return expression_tree_walker(node, contain_invalid_rfcolumn_walker, (void *) context); } /* * Check if all columns referenced in the filter expression are part of the * REPLICA IDENTITY index or not. * * Returns true if any invalid column is found. */ bool pub_rf_contains_invalid_column(Oid pubid, Relation relation, List *ancestors, bool pubviaroot) { HeapTuple rftuple; Oid relid = RelationGetRelid(relation); Oid publish_as_relid = RelationGetRelid(relation); bool result = false; Datum rfdatum; bool rfisnull; /* * FULL means all columns are in the REPLICA IDENTITY, so all columns are * allowed in the row filter and we can skip the validation. */ if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL) return false; /* * For a partition, if pubviaroot is true, find the topmost ancestor that * is published via this publication as we need to use its row filter * expression to filter the partition's changes. * * Note that even though the row filter used is for an ancestor, the * REPLICA IDENTITY used will be for the actual child table. */ if (pubviaroot && relation->rd_rel->relispartition) { publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL); if (!OidIsValid(publish_as_relid)) publish_as_relid = relid; } rftuple = SearchSysCache2(PUBLICATIONRELMAP, ObjectIdGetDatum(publish_as_relid), ObjectIdGetDatum(pubid)); if (!HeapTupleIsValid(rftuple)) return false; rfdatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple, Anum_pg_publication_rel_prqual, &rfisnull); if (!rfisnull) { rf_context context = {0}; Node *rfnode; Bitmapset *bms = NULL; context.pubviaroot = pubviaroot; context.parentid = publish_as_relid; context.relid = relid; /* Remember columns that are part of the REPLICA IDENTITY */ bms = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_IDENTITY_KEY); context.bms_replident = bms; rfnode = stringToNode(TextDatumGetCString(rfdatum)); result = contain_invalid_rfcolumn_walker(rfnode, &context); } ReleaseSysCache(rftuple); return result; } /* * Check if all columns referenced in the REPLICA IDENTITY are covered by * the column list. * * Returns true if any replica identity column is not covered by column list. */ bool pub_collist_contains_invalid_column(Oid pubid, Relation relation, List *ancestors, bool pubviaroot) { HeapTuple tuple; Oid relid = RelationGetRelid(relation); Oid publish_as_relid = RelationGetRelid(relation); bool result = false; Datum datum; bool isnull; /* * For a partition, if pubviaroot is true, find the topmost ancestor that * is published via this publication as we need to use its column list for * the changes. * * Note that even though the column list used is for an ancestor, the * REPLICA IDENTITY used will be for the actual child table. */ if (pubviaroot && relation->rd_rel->relispartition) { publish_as_relid = GetTopMostAncestorInPublication(pubid, ancestors, NULL); if (!OidIsValid(publish_as_relid)) publish_as_relid = relid; } tuple = SearchSysCache2(PUBLICATIONRELMAP, ObjectIdGetDatum(publish_as_relid), ObjectIdGetDatum(pubid)); if (!HeapTupleIsValid(tuple)) return false; datum = SysCacheGetAttr(PUBLICATIONRELMAP, tuple, Anum_pg_publication_rel_prattrs, &isnull); if (!isnull) { int x; Bitmapset *idattrs; Bitmapset *columns = NULL; /* With REPLICA IDENTITY FULL, no column list is allowed. */ if (relation->rd_rel->relreplident == REPLICA_IDENTITY_FULL) result = true; /* Transform the column list datum to a bitmapset. */ columns = pub_collist_to_bitmapset(NULL, datum, NULL); /* Remember columns that are part of the REPLICA IDENTITY */ idattrs = RelationGetIndexAttrBitmap(relation, INDEX_ATTR_BITMAP_IDENTITY_KEY); /* * Attnums in the bitmap returned by RelationGetIndexAttrBitmap are * offset (to handle system columns the usual way), while column list * does not use offset, so we can't do bms_is_subset(). Instead, we * have to loop over the idattrs and check all of them are in the * list. */ x = -1; while ((x = bms_next_member(idattrs, x)) >= 0) { AttrNumber attnum = (x + FirstLowInvalidHeapAttributeNumber); /* * If pubviaroot is true, we are validating the column list of the * parent table, but the bitmap contains the replica identity * information of the child table. The parent/child attnums may * not match, so translate them to the parent - get the attname * from the child, and look it up in the parent. */ if (pubviaroot) { /* attribute name in the child table */ char *colname = get_attname(relid, attnum, false); /* * Determine the attnum for the attribute name in parent (we * are using the column list defined on the parent). */ attnum = get_attnum(publish_as_relid, colname); } /* replica identity column, not covered by the column list */ if (!bms_is_member(attnum, columns)) { result = true; break; } } bms_free(idattrs); bms_free(columns); } ReleaseSysCache(tuple); return result; } /* check_functions_in_node callback */ static bool contain_mutable_or_user_functions_checker(Oid func_id, void *context) { return (func_volatile(func_id) != PROVOLATILE_IMMUTABLE || func_id >= FirstNormalObjectId); } /* * The row filter walker checks if the row filter expression is a "simple * expression". * * It allows only simple or compound expressions such as: * - (Var Op Const) * - (Var Op Var) * - (Var Op Const) AND/OR (Var Op Const) * - etc * (where Var is a column of the table this filter belongs to) * * The simple expression has the following restrictions: * - User-defined operators are not allowed; * - User-defined functions are not allowed; * - User-defined types are not allowed; * - User-defined collations are not allowed; * - Non-immutable built-in functions are not allowed; * - System columns are not allowed. * * NOTES * * We don't allow user-defined functions/operators/types/collations because * (a) if a user drops a user-defined object used in a row filter expression or * if there is any other error while using it, the logical decoding * infrastructure won't be able to recover from such an error even if the * object is recreated again because a historic snapshot is used to evaluate * the row filter; * (b) a user-defined function can be used to access tables that could have * unpleasant results because a historic snapshot is used. That's why only * immutable built-in functions are allowed in row filter expressions. * * We don't allow system columns because currently, we don't have that * information in the tuple passed to downstream. Also, as we don't replicate * those to subscribers, there doesn't seem to be a need for a filter on those * columns. * * We can allow other node types after more analysis and testing. */ static bool check_simple_rowfilter_expr_walker(Node *node, ParseState *pstate) { char *errdetail_msg = NULL; if (node == NULL) return false; switch (nodeTag(node)) { case T_Var: /* System columns are not allowed. */ if (((Var *) node)->varattno < InvalidAttrNumber) errdetail_msg = _("System columns are not allowed."); break; case T_OpExpr: case T_DistinctExpr: case T_NullIfExpr: /* OK, except user-defined operators are not allowed. */ if (((OpExpr *) node)->opno >= FirstNormalObjectId) errdetail_msg = _("User-defined operators are not allowed."); break; case T_ScalarArrayOpExpr: /* OK, except user-defined operators are not allowed. */ if (((ScalarArrayOpExpr *) node)->opno >= FirstNormalObjectId) errdetail_msg = _("User-defined operators are not allowed."); /* * We don't need to check the hashfuncid and negfuncid of * ScalarArrayOpExpr as those functions are only built for a * subquery. */ break; case T_RowCompareExpr: { ListCell *opid; /* OK, except user-defined operators are not allowed. */ foreach(opid, ((RowCompareExpr *) node)->opnos) { if (lfirst_oid(opid) >= FirstNormalObjectId) { errdetail_msg = _("User-defined operators are not allowed."); break; } } } break; case T_Const: case T_FuncExpr: case T_BoolExpr: case T_RelabelType: case T_CollateExpr: case T_CaseExpr: case T_CaseTestExpr: case T_ArrayExpr: case T_RowExpr: case T_CoalesceExpr: case T_MinMaxExpr: case T_XmlExpr: case T_NullTest: case T_BooleanTest: case T_List: /* OK, supported */ break; default: errdetail_msg = _("Only columns, constants, built-in operators, built-in data types, built-in collations, and immutable built-in functions are allowed."); break; } /* * For all the supported nodes, if we haven't already found a problem, * check the types, functions, and collations used in it. We check List * by walking through each element. */ if (!errdetail_msg && !IsA(node, List)) { if (exprType(node) >= FirstNormalObjectId) errdetail_msg = _("User-defined types are not allowed."); else if (check_functions_in_node(node, contain_mutable_or_user_functions_checker, (void *) pstate)) errdetail_msg = _("User-defined or built-in mutable functions are not allowed."); else if (exprCollation(node) >= FirstNormalObjectId || exprInputCollation(node) >= FirstNormalObjectId) errdetail_msg = _("User-defined collations are not allowed."); } /* * If we found a problem in this node, throw error now. Otherwise keep * going. */ if (errdetail_msg) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("invalid publication WHERE expression"), errdetail_internal("%s", errdetail_msg), parser_errposition(pstate, exprLocation(node)))); return expression_tree_walker(node, check_simple_rowfilter_expr_walker, (void *) pstate); } /* * Check if the row filter expression is a "simple expression". * * See check_simple_rowfilter_expr_walker for details. */ static bool check_simple_rowfilter_expr(Node *node, ParseState *pstate) { return check_simple_rowfilter_expr_walker(node, pstate); } /* * Transform the publication WHERE expression for all the relations in the list, * ensuring it is coerced to boolean and necessary collation information is * added if required, and add a new nsitem/RTE for the associated relation to * the ParseState's namespace list. * * Also check the publication row filter expression and throw an error if * anything not permitted or unexpected is encountered. */ static void TransformPubWhereClauses(List *tables, const char *queryString, bool pubviaroot) { ListCell *lc; foreach(lc, tables) { ParseNamespaceItem *nsitem; Node *whereclause = NULL; ParseState *pstate; PublicationRelInfo *pri = (PublicationRelInfo *) lfirst(lc); if (pri->whereClause == NULL) continue; /* * If the publication doesn't publish changes via the root partitioned * table, the partition's row filter will be used. So disallow using * WHERE clause on partitioned table in this case. */ if (!pubviaroot && pri->relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("cannot use publication WHERE clause for relation \"%s\"", RelationGetRelationName(pri->relation)), errdetail("WHERE clause cannot be used for a partitioned table when %s is false.", "publish_via_partition_root"))); /* * A fresh pstate is required so that we only have "this" table in its * rangetable */ pstate = make_parsestate(NULL); pstate->p_sourcetext = queryString; nsitem = addRangeTableEntryForRelation(pstate, pri->relation, AccessShareLock, NULL, false, false); addNSItemToQuery(pstate, nsitem, false, true, true); whereclause = transformWhereClause(pstate, copyObject(pri->whereClause), EXPR_KIND_WHERE, "PUBLICATION WHERE"); /* Fix up collation information */ assign_expr_collations(pstate, whereclause); /* * We allow only simple expressions in row filters. See * check_simple_rowfilter_expr_walker. */ check_simple_rowfilter_expr(whereclause, pstate); free_parsestate(pstate); pri->whereClause = whereclause; } } /* * Given a list of tables that are going to be added to a publication, * verify that they fulfill the necessary preconditions, namely: no tables * have a column list if any schema is published; and partitioned tables do * not have column lists if publish_via_partition_root is not set. * * 'publish_schema' indicates that the publication contains any TABLES IN * SCHEMA elements (newly added in this command, or preexisting). * 'pubviaroot' is the value of publish_via_partition_root. */ static void CheckPubRelationColumnList(char *pubname, List *tables, bool publish_schema, bool pubviaroot) { ListCell *lc; foreach(lc, tables) { PublicationRelInfo *pri = (PublicationRelInfo *) lfirst(lc); if (pri->columns == NIL) continue; /* * Disallow specifying column list if any schema is in the * publication. * * XXX We could instead just forbid the case when the publication * tries to publish the table with a column list and a schema for that * table. However, if we do that then we need a restriction during * ALTER TABLE ... SET SCHEMA to prevent such a case which doesn't * seem to be a good idea. */ if (publish_schema) ereport(ERROR, errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("cannot use column list for relation \"%s.%s\" in publication \"%s\"", get_namespace_name(RelationGetNamespace(pri->relation)), RelationGetRelationName(pri->relation), pubname), errdetail("Column lists cannot be specified in publications containing FOR TABLES IN SCHEMA elements.")); /* * If the publication doesn't publish changes via the root partitioned * table, the partition's column list will be used. So disallow using * a column list on the partitioned table in this case. */ if (!pubviaroot && pri->relation->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("cannot use column list for relation \"%s.%s\" in publication \"%s\"", get_namespace_name(RelationGetNamespace(pri->relation)), RelationGetRelationName(pri->relation), pubname), errdetail("Column lists cannot be specified for partitioned tables when %s is false.", "publish_via_partition_root"))); } } /* * Create new publication. */ ObjectAddress CreatePublication(ParseState *pstate, CreatePublicationStmt *stmt) { Relation rel; ObjectAddress myself; Oid puboid; bool nulls[Natts_pg_publication]; Datum values[Natts_pg_publication]; HeapTuple tup; bool publish_given; PublicationActions pubactions; bool publish_via_partition_root_given; bool publish_via_partition_root; AclResult aclresult; List *relations = NIL; List *schemaidlist = NIL; /* must have CREATE privilege on database */ aclresult = object_aclcheck(DatabaseRelationId, MyDatabaseId, GetUserId(), ACL_CREATE); if (aclresult != ACLCHECK_OK) aclcheck_error(aclresult, OBJECT_DATABASE, get_database_name(MyDatabaseId)); /* FOR ALL TABLES requires superuser */ if (stmt->for_all_tables && !superuser()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to create FOR ALL TABLES publication"))); rel = table_open(PublicationRelationId, RowExclusiveLock); /* Check if name is used */ puboid = GetSysCacheOid1(PUBLICATIONNAME, Anum_pg_publication_oid, CStringGetDatum(stmt->pubname)); if (OidIsValid(puboid)) ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("publication \"%s\" already exists", stmt->pubname))); /* Form a tuple. */ memset(values, 0, sizeof(values)); memset(nulls, false, sizeof(nulls)); values[Anum_pg_publication_pubname - 1] = DirectFunctionCall1(namein, CStringGetDatum(stmt->pubname)); values[Anum_pg_publication_pubowner - 1] = ObjectIdGetDatum(GetUserId()); parse_publication_options(pstate, stmt->options, &publish_given, &pubactions, &publish_via_partition_root_given, &publish_via_partition_root); puboid = GetNewOidWithIndex(rel, PublicationObjectIndexId, Anum_pg_publication_oid); values[Anum_pg_publication_oid - 1] = ObjectIdGetDatum(puboid); values[Anum_pg_publication_puballtables - 1] = BoolGetDatum(stmt->for_all_tables); values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(pubactions.pubinsert); values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(pubactions.pubupdate); values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(pubactions.pubdelete); values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(pubactions.pubtruncate); values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(publish_via_partition_root); tup = heap_form_tuple(RelationGetDescr(rel), values, nulls); /* Insert tuple into catalog. */ CatalogTupleInsert(rel, tup); heap_freetuple(tup); recordDependencyOnOwner(PublicationRelationId, puboid, GetUserId()); ObjectAddressSet(myself, PublicationRelationId, puboid); /* Make the changes visible. */ CommandCounterIncrement(); /* Associate objects with the publication. */ if (stmt->for_all_tables) { /* Invalidate relcache so that publication info is rebuilt. */ CacheInvalidateRelcacheAll(); } else { ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, &schemaidlist); /* FOR TABLES IN SCHEMA requires superuser */ if (schemaidlist != NIL && !superuser()) ereport(ERROR, errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to create FOR TABLES IN SCHEMA publication")); if (relations != NIL) { List *rels; rels = OpenTableList(relations); TransformPubWhereClauses(rels, pstate->p_sourcetext, publish_via_partition_root); CheckPubRelationColumnList(stmt->pubname, rels, schemaidlist != NIL, publish_via_partition_root); PublicationAddTables(puboid, rels, true, NULL); CloseTableList(rels); } if (schemaidlist != NIL) { /* * Schema lock is held until the publication is created to prevent * concurrent schema deletion. */ LockSchemaList(schemaidlist); PublicationAddSchemas(puboid, schemaidlist, true, NULL); } } table_close(rel, RowExclusiveLock); InvokeObjectPostCreateHook(PublicationRelationId, puboid, 0); if (wal_level != WAL_LEVEL_LOGICAL) ereport(WARNING, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("wal_level is insufficient to publish logical changes"), errhint("Set wal_level to \"logical\" before creating subscriptions."))); return myself; } /* * Change options of a publication. */ static void AlterPublicationOptions(ParseState *pstate, AlterPublicationStmt *stmt, Relation rel, HeapTuple tup) { bool nulls[Natts_pg_publication]; bool replaces[Natts_pg_publication]; Datum values[Natts_pg_publication]; bool publish_given; PublicationActions pubactions; bool publish_via_partition_root_given; bool publish_via_partition_root; ObjectAddress obj; Form_pg_publication pubform; List *root_relids = NIL; ListCell *lc; parse_publication_options(pstate, stmt->options, &publish_given, &pubactions, &publish_via_partition_root_given, &publish_via_partition_root); pubform = (Form_pg_publication) GETSTRUCT(tup); /* * If the publication doesn't publish changes via the root partitioned * table, the partition's row filter and column list will be used. So * disallow using WHERE clause and column lists on partitioned table in * this case. */ if (!pubform->puballtables && publish_via_partition_root_given && !publish_via_partition_root) { /* * Lock the publication so nobody else can do anything with it. This * prevents concurrent alter to add partitioned table(s) with WHERE * clause(s) and/or column lists which we don't allow when not * publishing via root. */ LockDatabaseObject(PublicationRelationId, pubform->oid, 0, AccessShareLock); root_relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT); foreach(lc, root_relids) { Oid relid = lfirst_oid(lc); HeapTuple rftuple; char relkind; char *relname; bool has_rowfilter; bool has_collist; /* * Beware: we don't have lock on the relations, so cope silently * with the cache lookups returning NULL. */ rftuple = SearchSysCache2(PUBLICATIONRELMAP, ObjectIdGetDatum(relid), ObjectIdGetDatum(pubform->oid)); if (!HeapTupleIsValid(rftuple)) continue; has_rowfilter = !heap_attisnull(rftuple, Anum_pg_publication_rel_prqual, NULL); has_collist = !heap_attisnull(rftuple, Anum_pg_publication_rel_prattrs, NULL); if (!has_rowfilter && !has_collist) { ReleaseSysCache(rftuple); continue; } relkind = get_rel_relkind(relid); if (relkind != RELKIND_PARTITIONED_TABLE) { ReleaseSysCache(rftuple); continue; } relname = get_rel_name(relid); if (relname == NULL) /* table concurrently dropped */ { ReleaseSysCache(rftuple); continue; } if (has_rowfilter) ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("cannot set parameter \"%s\" to false for publication \"%s\"", "publish_via_partition_root", stmt->pubname), errdetail("The publication contains a WHERE clause for partitioned table \"%s\", which is not allowed when \"%s\" is false.", relname, "publish_via_partition_root"))); Assert(has_collist); ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("cannot set parameter \"%s\" to false for publication \"%s\"", "publish_via_partition_root", stmt->pubname), errdetail("The publication contains a column list for partitioned table \"%s\", which is not allowed when \"%s\" is false.", relname, "publish_via_partition_root"))); } } /* Everything ok, form a new tuple. */ memset(values, 0, sizeof(values)); memset(nulls, false, sizeof(nulls)); memset(replaces, false, sizeof(replaces)); if (publish_given) { values[Anum_pg_publication_pubinsert - 1] = BoolGetDatum(pubactions.pubinsert); replaces[Anum_pg_publication_pubinsert - 1] = true; values[Anum_pg_publication_pubupdate - 1] = BoolGetDatum(pubactions.pubupdate); replaces[Anum_pg_publication_pubupdate - 1] = true; values[Anum_pg_publication_pubdelete - 1] = BoolGetDatum(pubactions.pubdelete); replaces[Anum_pg_publication_pubdelete - 1] = true; values[Anum_pg_publication_pubtruncate - 1] = BoolGetDatum(pubactions.pubtruncate); replaces[Anum_pg_publication_pubtruncate - 1] = true; } if (publish_via_partition_root_given) { values[Anum_pg_publication_pubviaroot - 1] = BoolGetDatum(publish_via_partition_root); replaces[Anum_pg_publication_pubviaroot - 1] = true; } tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls, replaces); /* Update the catalog. */ CatalogTupleUpdate(rel, &tup->t_self, tup); CommandCounterIncrement(); pubform = (Form_pg_publication) GETSTRUCT(tup); /* Invalidate the relcache. */ if (pubform->puballtables) { CacheInvalidateRelcacheAll(); } else { List *relids = NIL; List *schemarelids = NIL; /* * For any partitioned tables contained in the publication, we must * invalidate all partitions contained in the respective partition * trees, not just those explicitly mentioned in the publication. */ if (root_relids == NIL) relids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ALL); else { /* * We already got tables explicitly mentioned in the publication. * Now get all partitions for the partitioned table in the list. */ foreach(lc, root_relids) relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_ALL, lfirst_oid(lc)); } schemarelids = GetAllSchemaPublicationRelations(pubform->oid, PUBLICATION_PART_ALL); relids = list_concat_unique_oid(relids, schemarelids); InvalidatePublicationRels(relids); } ObjectAddressSet(obj, PublicationRelationId, pubform->oid); EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress, (Node *) stmt); InvokeObjectPostAlterHook(PublicationRelationId, pubform->oid, 0); } /* * Invalidate the relations. */ void InvalidatePublicationRels(List *relids) { /* * We don't want to send too many individual messages, at some point it's * cheaper to just reset whole relcache. */ if (list_length(relids) < MAX_RELCACHE_INVAL_MSGS) { ListCell *lc; foreach(lc, relids) CacheInvalidateRelcacheByRelid(lfirst_oid(lc)); } else CacheInvalidateRelcacheAll(); } /* * Add or remove table to/from publication. */ static void AlterPublicationTables(AlterPublicationStmt *stmt, HeapTuple tup, List *tables, const char *queryString, bool publish_schema) { List *rels = NIL; Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup); Oid pubid = pubform->oid; /* * Nothing to do if no objects, except in SET: for that it is quite * possible that user has not specified any tables in which case we need * to remove all the existing tables. */ if (!tables && stmt->action != AP_SetObjects) return; rels = OpenTableList(tables); if (stmt->action == AP_AddObjects) { TransformPubWhereClauses(rels, queryString, pubform->pubviaroot); publish_schema |= is_schema_publication(pubid); CheckPubRelationColumnList(stmt->pubname, rels, publish_schema, pubform->pubviaroot); PublicationAddTables(pubid, rels, false, stmt); } else if (stmt->action == AP_DropObjects) PublicationDropTables(pubid, rels, false); else /* AP_SetObjects */ { List *oldrelids = GetPublicationRelations(pubid, PUBLICATION_PART_ROOT); List *delrels = NIL; ListCell *oldlc; TransformPubWhereClauses(rels, queryString, pubform->pubviaroot); CheckPubRelationColumnList(stmt->pubname, rels, publish_schema, pubform->pubviaroot); /* * To recreate the relation list for the publication, look for * existing relations that do not need to be dropped. */ foreach(oldlc, oldrelids) { Oid oldrelid = lfirst_oid(oldlc); ListCell *newlc; PublicationRelInfo *oldrel; bool found = false; HeapTuple rftuple; Node *oldrelwhereclause = NULL; Bitmapset *oldcolumns = NULL; /* look up the cache for the old relmap */ rftuple = SearchSysCache2(PUBLICATIONRELMAP, ObjectIdGetDatum(oldrelid), ObjectIdGetDatum(pubid)); /* * See if the existing relation currently has a WHERE clause or a * column list. We need to compare those too. */ if (HeapTupleIsValid(rftuple)) { bool isnull = true; Datum whereClauseDatum; Datum columnListDatum; /* Load the WHERE clause for this table. */ whereClauseDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple, Anum_pg_publication_rel_prqual, &isnull); if (!isnull) oldrelwhereclause = stringToNode(TextDatumGetCString(whereClauseDatum)); /* Transform the int2vector column list to a bitmap. */ columnListDatum = SysCacheGetAttr(PUBLICATIONRELMAP, rftuple, Anum_pg_publication_rel_prattrs, &isnull); if (!isnull) oldcolumns = pub_collist_to_bitmapset(NULL, columnListDatum, NULL); ReleaseSysCache(rftuple); } foreach(newlc, rels) { PublicationRelInfo *newpubrel; Oid newrelid; Bitmapset *newcolumns = NULL; newpubrel = (PublicationRelInfo *) lfirst(newlc); newrelid = RelationGetRelid(newpubrel->relation); /* * If the new publication has column list, transform it to a * bitmap too. */ if (newpubrel->columns) { ListCell *lc; foreach(lc, newpubrel->columns) { char *colname = strVal(lfirst(lc)); AttrNumber attnum = get_attnum(newrelid, colname); newcolumns = bms_add_member(newcolumns, attnum); } } /* * Check if any of the new set of relations matches with the * existing relations in the publication. Additionally, if the * relation has an associated WHERE clause, check the WHERE * expressions also match. Same for the column list. Drop the * rest. */ if (RelationGetRelid(newpubrel->relation) == oldrelid) { if (equal(oldrelwhereclause, newpubrel->whereClause) && bms_equal(oldcolumns, newcolumns)) { found = true; break; } } } /* * Add the non-matched relations to a list so that they can be * dropped. */ if (!found) { oldrel = palloc(sizeof(PublicationRelInfo)); oldrel->whereClause = NULL; oldrel->columns = NIL; oldrel->relation = table_open(oldrelid, ShareUpdateExclusiveLock); delrels = lappend(delrels, oldrel); } } /* And drop them. */ PublicationDropTables(pubid, delrels, true); /* * Don't bother calculating the difference for adding, we'll catch and * skip existing ones when doing catalog update. */ PublicationAddTables(pubid, rels, true, stmt); CloseTableList(delrels); } CloseTableList(rels); } /* * Alter the publication schemas. * * Add or remove schemas to/from publication. */ static void AlterPublicationSchemas(AlterPublicationStmt *stmt, HeapTuple tup, List *schemaidlist) { Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup); /* * Nothing to do if no objects, except in SET: for that it is quite * possible that user has not specified any schemas in which case we need * to remove all the existing schemas. */ if (!schemaidlist && stmt->action != AP_SetObjects) return; /* * Schema lock is held until the publication is altered to prevent * concurrent schema deletion. */ LockSchemaList(schemaidlist); if (stmt->action == AP_AddObjects) { ListCell *lc; List *reloids; reloids = GetPublicationRelations(pubform->oid, PUBLICATION_PART_ROOT); foreach(lc, reloids) { HeapTuple coltuple; coltuple = SearchSysCache2(PUBLICATIONRELMAP, ObjectIdGetDatum(lfirst_oid(lc)), ObjectIdGetDatum(pubform->oid)); if (!HeapTupleIsValid(coltuple)) continue; /* * Disallow adding schema if column list is already part of the * publication. See CheckPubRelationColumnList. */ if (!heap_attisnull(coltuple, Anum_pg_publication_rel_prattrs, NULL)) ereport(ERROR, errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("cannot add schema to publication \"%s\"", stmt->pubname), errdetail("Schemas cannot be added if any tables that specify a column list are already part of the publication.")); ReleaseSysCache(coltuple); } PublicationAddSchemas(pubform->oid, schemaidlist, false, stmt); } else if (stmt->action == AP_DropObjects) PublicationDropSchemas(pubform->oid, schemaidlist, false); else /* AP_SetObjects */ { List *oldschemaids = GetPublicationSchemas(pubform->oid); List *delschemas = NIL; /* Identify which schemas should be dropped */ delschemas = list_difference_oid(oldschemaids, schemaidlist); /* * Schema lock is held until the publication is altered to prevent * concurrent schema deletion. */ LockSchemaList(delschemas); /* And drop them */ PublicationDropSchemas(pubform->oid, delschemas, true); /* * Don't bother calculating the difference for adding, we'll catch and * skip existing ones when doing catalog update. */ PublicationAddSchemas(pubform->oid, schemaidlist, true, stmt); } } /* * Check if relations and schemas can be in a given publication and throw * appropriate error if not. */ static void CheckAlterPublication(AlterPublicationStmt *stmt, HeapTuple tup, List *tables, List *schemaidlist) { Form_pg_publication pubform = (Form_pg_publication) GETSTRUCT(tup); if ((stmt->action == AP_AddObjects || stmt->action == AP_SetObjects) && schemaidlist && !superuser()) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("must be superuser to add or set schemas"))); /* * Check that user is allowed to manipulate the publication tables in * schema */ if (schemaidlist && pubform->puballtables) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("publication \"%s\" is defined as FOR ALL TABLES", NameStr(pubform->pubname)), errdetail("Schemas cannot be added to or dropped from FOR ALL TABLES publications."))); /* Check that user is allowed to manipulate the publication tables. */ if (tables && pubform->puballtables) ereport(ERROR, (errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), errmsg("publication \"%s\" is defined as FOR ALL TABLES", NameStr(pubform->pubname)), errdetail("Tables cannot be added to or dropped from FOR ALL TABLES publications."))); } /* * Alter the existing publication. * * This is dispatcher function for AlterPublicationOptions, * AlterPublicationSchemas and AlterPublicationTables. */ void AlterPublication(ParseState *pstate, AlterPublicationStmt *stmt) { Relation rel; HeapTuple tup; Form_pg_publication pubform; rel = table_open(PublicationRelationId, RowExclusiveLock); tup = SearchSysCacheCopy1(PUBLICATIONNAME, CStringGetDatum(stmt->pubname)); if (!HeapTupleIsValid(tup)) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("publication \"%s\" does not exist", stmt->pubname))); pubform = (Form_pg_publication) GETSTRUCT(tup); /* must be owner */ if (!object_ownercheck(PublicationRelationId, pubform->oid, GetUserId())) aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION, stmt->pubname); if (stmt->options) AlterPublicationOptions(pstate, stmt, rel, tup); else { List *relations = NIL; List *schemaidlist = NIL; Oid pubid = pubform->oid; ObjectsInPublicationToOids(stmt->pubobjects, pstate, &relations, &schemaidlist); CheckAlterPublication(stmt, tup, relations, schemaidlist); heap_freetuple(tup); /* Lock the publication so nobody else can do anything with it. */ LockDatabaseObject(PublicationRelationId, pubid, 0, AccessExclusiveLock); /* * It is possible that by the time we acquire the lock on publication, * concurrent DDL has removed it. We can test this by checking the * existence of publication. We get the tuple again to avoid the risk * of any publication option getting changed. */ tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(pubid)); if (!HeapTupleIsValid(tup)) ereport(ERROR, errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("publication \"%s\" does not exist", stmt->pubname)); AlterPublicationTables(stmt, tup, relations, pstate->p_sourcetext, schemaidlist != NIL); AlterPublicationSchemas(stmt, tup, schemaidlist); } /* Cleanup. */ heap_freetuple(tup); table_close(rel, RowExclusiveLock); } /* * Remove relation from publication by mapping OID. */ void RemovePublicationRelById(Oid proid) { Relation rel; HeapTuple tup; Form_pg_publication_rel pubrel; List *relids = NIL; rel = table_open(PublicationRelRelationId, RowExclusiveLock); tup = SearchSysCache1(PUBLICATIONREL, ObjectIdGetDatum(proid)); if (!HeapTupleIsValid(tup)) elog(ERROR, "cache lookup failed for publication table %u", proid); pubrel = (Form_pg_publication_rel) GETSTRUCT(tup); /* * Invalidate relcache so that publication info is rebuilt. * * For the partitioned tables, we must invalidate all partitions contained * in the respective partition hierarchies, not just the one explicitly * mentioned in the publication. This is required because we implicitly * publish the child tables when the parent table is published. */ relids = GetPubPartitionOptionRelations(relids, PUBLICATION_PART_ALL, pubrel->prrelid); InvalidatePublicationRels(relids); CatalogTupleDelete(rel, &tup->t_self); ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); } /* * Remove the publication by mapping OID. */ void RemovePublicationById(Oid pubid) { Relation rel; HeapTuple tup; Form_pg_publication pubform; rel = table_open(PublicationRelationId, RowExclusiveLock); tup = SearchSysCache1(PUBLICATIONOID, ObjectIdGetDatum(pubid)); if (!HeapTupleIsValid(tup)) elog(ERROR, "cache lookup failed for publication %u", pubid); pubform = (Form_pg_publication) GETSTRUCT(tup); /* Invalidate relcache so that publication info is rebuilt. */ if (pubform->puballtables) CacheInvalidateRelcacheAll(); CatalogTupleDelete(rel, &tup->t_self); ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); } /* * Remove schema from publication by mapping OID. */ void RemovePublicationSchemaById(Oid psoid) { Relation rel; HeapTuple tup; List *schemaRels = NIL; Form_pg_publication_namespace pubsch; rel = table_open(PublicationNamespaceRelationId, RowExclusiveLock); tup = SearchSysCache1(PUBLICATIONNAMESPACE, ObjectIdGetDatum(psoid)); if (!HeapTupleIsValid(tup)) elog(ERROR, "cache lookup failed for publication schema %u", psoid); pubsch = (Form_pg_publication_namespace) GETSTRUCT(tup); /* * Invalidate relcache so that publication info is rebuilt. See * RemovePublicationRelById for why we need to consider all the * partitions. */ schemaRels = GetSchemaPublicationRelations(pubsch->pnnspid, PUBLICATION_PART_ALL); InvalidatePublicationRels(schemaRels); CatalogTupleDelete(rel, &tup->t_self); ReleaseSysCache(tup); table_close(rel, RowExclusiveLock); } /* * Open relations specified by a PublicationTable list. * The returned tables are locked in ShareUpdateExclusiveLock mode in order to * add them to a publication. */ static List * OpenTableList(List *tables) { List *relids = NIL; List *rels = NIL; ListCell *lc; List *relids_with_rf = NIL; List *relids_with_collist = NIL; /* * Open, share-lock, and check all the explicitly-specified relations */ foreach(lc, tables) { PublicationTable *t = lfirst_node(PublicationTable, lc); bool recurse = t->relation->inh; Relation rel; Oid myrelid; PublicationRelInfo *pub_rel; /* Allow query cancel in case this takes a long time */ CHECK_FOR_INTERRUPTS(); rel = table_openrv(t->relation, ShareUpdateExclusiveLock); myrelid = RelationGetRelid(rel); /* * Filter out duplicates if user specifies "foo, foo". * * Note that this algorithm is known to not be very efficient (O(N^2)) * but given that it only works on list of tables given to us by user * it's deemed acceptable. */ if (list_member_oid(relids, myrelid)) { /* Disallow duplicate tables if there are any with row filters. */ if (t->whereClause || list_member_oid(relids_with_rf, myrelid)) ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("conflicting or redundant WHERE clauses for table \"%s\"", RelationGetRelationName(rel)))); /* Disallow duplicate tables if there are any with column lists. */ if (t->columns || list_member_oid(relids_with_collist, myrelid)) ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("conflicting or redundant column lists for table \"%s\"", RelationGetRelationName(rel)))); table_close(rel, ShareUpdateExclusiveLock); continue; } pub_rel = palloc(sizeof(PublicationRelInfo)); pub_rel->relation = rel; pub_rel->whereClause = t->whereClause; pub_rel->columns = t->columns; rels = lappend(rels, pub_rel); relids = lappend_oid(relids, myrelid); if (t->whereClause) relids_with_rf = lappend_oid(relids_with_rf, myrelid); if (t->columns) relids_with_collist = lappend_oid(relids_with_collist, myrelid); /* * Add children of this rel, if requested, so that they too are added * to the publication. A partitioned table can't have any inheritance * children other than its partitions, which need not be explicitly * added to the publication. */ if (recurse && rel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE) { List *children; ListCell *child; children = find_all_inheritors(myrelid, ShareUpdateExclusiveLock, NULL); foreach(child, children) { Oid childrelid = lfirst_oid(child); /* Allow query cancel in case this takes a long time */ CHECK_FOR_INTERRUPTS(); /* * Skip duplicates if user specified both parent and child * tables. */ if (list_member_oid(relids, childrelid)) { /* * We don't allow to specify row filter for both parent * and child table at the same time as it is not very * clear which one should be given preference. */ if (childrelid != myrelid && (t->whereClause || list_member_oid(relids_with_rf, childrelid))) ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("conflicting or redundant WHERE clauses for table \"%s\"", RelationGetRelationName(rel)))); /* * We don't allow to specify column list for both parent * and child table at the same time as it is not very * clear which one should be given preference. */ if (childrelid != myrelid && (t->columns || list_member_oid(relids_with_collist, childrelid))) ereport(ERROR, (errcode(ERRCODE_DUPLICATE_OBJECT), errmsg("conflicting or redundant column lists for table \"%s\"", RelationGetRelationName(rel)))); continue; } /* find_all_inheritors already got lock */ rel = table_open(childrelid, NoLock); pub_rel = palloc(sizeof(PublicationRelInfo)); pub_rel->relation = rel; /* child inherits WHERE clause from parent */ pub_rel->whereClause = t->whereClause; /* child inherits column list from parent */ pub_rel->columns = t->columns; rels = lappend(rels, pub_rel); relids = lappend_oid(relids, childrelid); if (t->whereClause) relids_with_rf = lappend_oid(relids_with_rf, childrelid); if (t->columns) relids_with_collist = lappend_oid(relids_with_collist, childrelid); } } } list_free(relids); list_free(relids_with_rf); return rels; } /* * Close all relations in the list. */ static void CloseTableList(List *rels) { ListCell *lc; foreach(lc, rels) { PublicationRelInfo *pub_rel; pub_rel = (PublicationRelInfo *) lfirst(lc); table_close(pub_rel->relation, NoLock); } list_free_deep(rels); } /* * Lock the schemas specified in the schema list in AccessShareLock mode in * order to prevent concurrent schema deletion. */ static void LockSchemaList(List *schemalist) { ListCell *lc; foreach(lc, schemalist) { Oid schemaid = lfirst_oid(lc); /* Allow query cancel in case this takes a long time */ CHECK_FOR_INTERRUPTS(); LockDatabaseObject(NamespaceRelationId, schemaid, 0, AccessShareLock); /* * It is possible that by the time we acquire the lock on schema, * concurrent DDL has removed it. We can test this by checking the * existence of schema. */ if (!SearchSysCacheExists1(NAMESPACEOID, ObjectIdGetDatum(schemaid))) ereport(ERROR, errcode(ERRCODE_UNDEFINED_SCHEMA), errmsg("schema with OID %u does not exist", schemaid)); } } /* * Add listed tables to the publication. */ static void PublicationAddTables(Oid pubid, List *rels, bool if_not_exists, AlterPublicationStmt *stmt) { ListCell *lc; Assert(!stmt || !stmt->for_all_tables); foreach(lc, rels) { PublicationRelInfo *pub_rel = (PublicationRelInfo *) lfirst(lc); Relation rel = pub_rel->relation; ObjectAddress obj; /* Must be owner of the table or superuser. */ if (!object_ownercheck(RelationRelationId, RelationGetRelid(rel), GetUserId())) aclcheck_error(ACLCHECK_NOT_OWNER, get_relkind_objtype(rel->rd_rel->relkind), RelationGetRelationName(rel)); obj = publication_add_relation(pubid, pub_rel, if_not_exists); if (stmt) { EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress, (Node *) stmt); InvokeObjectPostCreateHook(PublicationRelRelationId, obj.objectId, 0); } } } /* * Remove listed tables from the publication. */ static void PublicationDropTables(Oid pubid, List *rels, bool missing_ok) { ObjectAddress obj; ListCell *lc; Oid prid; foreach(lc, rels) { PublicationRelInfo *pubrel = (PublicationRelInfo *) lfirst(lc); Relation rel = pubrel->relation; Oid relid = RelationGetRelid(rel); if (pubrel->columns) ereport(ERROR, errcode(ERRCODE_SYNTAX_ERROR), errmsg("column list must not be specified in ALTER PUBLICATION ... DROP")); prid = GetSysCacheOid2(PUBLICATIONRELMAP, Anum_pg_publication_rel_oid, ObjectIdGetDatum(relid), ObjectIdGetDatum(pubid)); if (!OidIsValid(prid)) { if (missing_ok) continue; ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("relation \"%s\" is not part of the publication", RelationGetRelationName(rel)))); } if (pubrel->whereClause) ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("cannot use a WHERE clause when removing a table from a publication"))); ObjectAddressSet(obj, PublicationRelRelationId, prid); performDeletion(&obj, DROP_CASCADE, 0); } } /* * Add listed schemas to the publication. */ static void PublicationAddSchemas(Oid pubid, List *schemas, bool if_not_exists, AlterPublicationStmt *stmt) { ListCell *lc; Assert(!stmt || !stmt->for_all_tables); foreach(lc, schemas) { Oid schemaid = lfirst_oid(lc); ObjectAddress obj; obj = publication_add_schema(pubid, schemaid, if_not_exists); if (stmt) { EventTriggerCollectSimpleCommand(obj, InvalidObjectAddress, (Node *) stmt); InvokeObjectPostCreateHook(PublicationNamespaceRelationId, obj.objectId, 0); } } } /* * Remove listed schemas from the publication. */ static void PublicationDropSchemas(Oid pubid, List *schemas, bool missing_ok) { ObjectAddress obj; ListCell *lc; Oid psid; foreach(lc, schemas) { Oid schemaid = lfirst_oid(lc); psid = GetSysCacheOid2(PUBLICATIONNAMESPACEMAP, Anum_pg_publication_namespace_oid, ObjectIdGetDatum(schemaid), ObjectIdGetDatum(pubid)); if (!OidIsValid(psid)) { if (missing_ok) continue; ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("tables from schema \"%s\" are not part of the publication", get_namespace_name(schemaid)))); } ObjectAddressSet(obj, PublicationNamespaceRelationId, psid); performDeletion(&obj, DROP_CASCADE, 0); } } /* * Internal workhorse for changing a publication owner */ static void AlterPublicationOwner_internal(Relation rel, HeapTuple tup, Oid newOwnerId) { Form_pg_publication form; form = (Form_pg_publication) GETSTRUCT(tup); if (form->pubowner == newOwnerId) return; if (!superuser()) { AclResult aclresult; /* Must be owner */ if (!object_ownercheck(PublicationRelationId, form->oid, GetUserId())) aclcheck_error(ACLCHECK_NOT_OWNER, OBJECT_PUBLICATION, NameStr(form->pubname)); /* Must be able to become new owner */ check_can_set_role(GetUserId(), newOwnerId); /* New owner must have CREATE privilege on database */ aclresult = object_aclcheck(DatabaseRelationId, MyDatabaseId, newOwnerId, ACL_CREATE); if (aclresult != ACLCHECK_OK) aclcheck_error(aclresult, OBJECT_DATABASE, get_database_name(MyDatabaseId)); if (form->puballtables && !superuser_arg(newOwnerId)) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied to change owner of publication \"%s\"", NameStr(form->pubname)), errhint("The owner of a FOR ALL TABLES publication must be a superuser."))); if (!superuser_arg(newOwnerId) && is_schema_publication(form->oid)) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("permission denied to change owner of publication \"%s\"", NameStr(form->pubname)), errhint("The owner of a FOR TABLES IN SCHEMA publication must be a superuser."))); } form->pubowner = newOwnerId; CatalogTupleUpdate(rel, &tup->t_self, tup); /* Update owner dependency reference */ changeDependencyOnOwner(PublicationRelationId, form->oid, newOwnerId); InvokeObjectPostAlterHook(PublicationRelationId, form->oid, 0); } /* * Change publication owner -- by name */ ObjectAddress AlterPublicationOwner(const char *name, Oid newOwnerId) { Oid subid; HeapTuple tup; Relation rel; ObjectAddress address; Form_pg_publication pubform; rel = table_open(PublicationRelationId, RowExclusiveLock); tup = SearchSysCacheCopy1(PUBLICATIONNAME, CStringGetDatum(name)); if (!HeapTupleIsValid(tup)) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("publication \"%s\" does not exist", name))); pubform = (Form_pg_publication) GETSTRUCT(tup); subid = pubform->oid; AlterPublicationOwner_internal(rel, tup, newOwnerId); ObjectAddressSet(address, PublicationRelationId, subid); heap_freetuple(tup); table_close(rel, RowExclusiveLock); return address; } /* * Change publication owner -- by OID */ void AlterPublicationOwner_oid(Oid subid, Oid newOwnerId) { HeapTuple tup; Relation rel; rel = table_open(PublicationRelationId, RowExclusiveLock); tup = SearchSysCacheCopy1(PUBLICATIONOID, ObjectIdGetDatum(subid)); if (!HeapTupleIsValid(tup)) ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("publication with OID %u does not exist", subid))); AlterPublicationOwner_internal(rel, tup, newOwnerId); heap_freetuple(tup); table_close(rel, RowExclusiveLock); }