From 968a55f1d87c1d002c8ec45398f454d86da7ffa8 Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Mon, 18 May 2026 10:34:43 +0000 Subject: [PATCH v37 02/10] Review comment fixes for "Add configurable conflict log table for Logical Replication" Review comment fixes for "Add configurable conflict log table for Logical Replication" --- src/backend/catalog/aclchk.c | 61 +++--- src/backend/catalog/catalog.c | 11 +- src/backend/catalog/heap.c | 33 +-- src/backend/catalog/namespace.c | 6 +- src/backend/catalog/pg_publication.c | 16 +- src/backend/commands/subscriptioncmds.c | 195 +----------------- src/backend/commands/tablecmds.c | 2 +- src/backend/executor/execMain.c | 7 +- src/backend/replication/logical/conflict.c | 216 +++++++++++++++++++- src/include/catalog/catalog.h | 2 +- src/include/catalog/pg_subscription.h | 16 +- src/include/commands/subscriptioncmds.h | 3 - src/include/replication/conflict.h | 21 +- src/test/regress/expected/subscription.out | 225 +++++++++++---------- src/test/regress/sql/subscription.sql | 38 ++-- 15 files changed, 448 insertions(+), 404 deletions(-) diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 84ef5304e22..e583187c7a6 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -3337,33 +3337,42 @@ pg_class_aclmask_ext(Oid table_oid, Oid roleid, AclMode mask, classForm = (Form_pg_class) GETSTRUCT(tuple); - /* - * Deny anyone permission to update a system catalog unless - * pg_authid.rolsuper is set. - * - * As of 7.4 we have some updatable system views; those shouldn't be - * protected in this way. Assume the view rules can take care of - * themselves. ACL_USAGE is if we ever have system sequences. - * - * For conflict log tables, we allow non-superusers to perform DELETE - * and TRUNCATE for maintenance, while still restricting INSERT, - * UPDATE, and USAGE. - */ - if ((mask & (ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE | ACL_USAGE)) && - IsConflictClass(classForm) && - !superuser_arg(roleid)) - mask &= ~(ACL_INSERT | ACL_UPDATE | ACL_USAGE); - else if ((mask & (ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE | ACL_USAGE)) && - IsSystemClass(table_oid, classForm) && - classForm->relkind != RELKIND_VIEW && - !superuser_arg(roleid)) - mask &= ~(ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE | ACL_USAGE); - - /* - * Otherwise, superusers bypass all permission-checking. - */ - if (superuser_arg(roleid)) + if (!superuser_arg(roleid)) + { + if (mask & (ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE | ACL_USAGE)) + { + if (IsConflictLogTableClass(classForm)) + { + /* + * For conflict log tables, allow non-superusers to perform + * DELETE and TRUNCATE for cleanup and maintenance. Also allow + * INSERT and UPDATE to pass ACL checks so that later checks + * can raise the dedicated "cannot modify or insert data into + * conflict log table" error instead of a generic permission + * denied error. Still restrict USAGE for non-superusers. + */ + mask &= ~(ACL_USAGE); + } + else if (IsSystemClass(table_oid, classForm) && + classForm->relkind != RELKIND_VIEW) + { + /* + * Deny anyone permission to update a system catalog unless + * pg_authid.rolsuper is set. + * + * As of 7.4 we have some updatable system views; those + * shouldn't be protected in this way. Assume the view rules + * can take care of themselves. ACL_USAGE is if we ever have + * system sequences. + */ + mask &= ~(ACL_INSERT | ACL_UPDATE | ACL_DELETE | ACL_TRUNCATE | + ACL_USAGE); + } + } + } + else { + /* Superusers bypass all permission-checking. */ ReleaseSysCache(tuple); return mask; } diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c index 4578cd07140..46d27ed02a9 100644 --- a/src/backend/catalog/catalog.c +++ b/src/backend/catalog/catalog.c @@ -88,7 +88,7 @@ IsSystemClass(Oid relid, Form_pg_class reltuple) /* IsCatalogRelationOid is a bit faster, so test that first */ return (IsCatalogRelationOid(relid) || IsToastClass(reltuple) || - IsConflictClass(reltuple)); + IsConflictLogTableClass(reltuple)); } /* @@ -233,11 +233,14 @@ IsToastClass(Form_pg_class reltuple) } /* - * IsConflictClass - Check if the given pg_class tuple belongs to the conflict - * namespace. + * IsConflictLogTableClass + * True iff Form_pg_class tuple represents a subscription-specific + * Conflict Log Table. + * + * Does not perform any catalog accesses. */ bool -IsConflictClass(Form_pg_class reltuple) +IsConflictLogTableClass(Form_pg_class reltuple) { Oid relnamespace = reltuple->relnamespace; diff --git a/src/backend/catalog/heap.c b/src/backend/catalog/heap.c index 0daf98a4405..3812caedb69 100644 --- a/src/backend/catalog/heap.c +++ b/src/backend/catalog/heap.c @@ -305,24 +305,31 @@ heap_create(const char *relname, Assert(OidIsValid(relid)); /* - * Don't allow creating relations in pg_catalog directly, even though it - * is allowed to move user defined relations there. Semantics with search - * paths including pg_catalog are too confusing for now. + * Don't allow creating relations in pg_catalog or pg_conflict directly, + * even though it is allowed to move user defined relations there. Semantics + * with search paths including pg_catalog are too confusing for now. * * But allow creating indexes on relations in pg_catalog even if * allow_system_table_mods = off, upper layers already guarantee it's on a * user defined relation, not a system one. */ - if (!allow_system_table_mods && - ((IsCatalogNamespace(relnamespace) && relkind != RELKIND_INDEX) || - IsToastNamespace(relnamespace) || - IsConflictNamespace(relnamespace)) && - IsNormalProcessingMode()) - ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), - errmsg("permission denied to create \"%s.%s\"", - get_namespace_name(relnamespace), relname), - errdetail("System catalog modifications are currently disallowed."))); + if (!allow_system_table_mods && IsNormalProcessingMode()) + { + if ((IsCatalogNamespace(relnamespace) && relkind != RELKIND_INDEX) || + IsToastNamespace(relnamespace)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied to create \"%s.%s\"", + get_namespace_name(relnamespace), relname), + errdetail("System catalog modifications are currently disallowed."))); + + if (IsConflictNamespace(relnamespace)) + ereport(ERROR, + (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + errmsg("permission denied to create \"%s.%s\"", + get_namespace_name(relnamespace), relname), + errdetail("Conflict schema modifications are currently disallowed."))); + } *relfrozenxid = InvalidTransactionId; *relminmxid = InvalidMultiXactId; diff --git a/src/backend/catalog/namespace.c b/src/backend/catalog/namespace.c index c35fcf57fd4..c4d3f5b6239 100644 --- a/src/backend/catalog/namespace.c +++ b/src/backend/catalog/namespace.c @@ -3524,7 +3524,7 @@ LookupCreationNamespace(const char *nspname) * Common checks on switching namespaces. * * We complain if either the old or new namespaces is a temporary schema, - * temporary toast schema, the TOAST schema, or the CONFLICT schema. + * temporary toast schema, the TOAST schema, or the pg_conflict schema. */ void CheckSetNamespace(Oid oldNspOid, Oid nspOid) @@ -3541,11 +3541,11 @@ CheckSetNamespace(Oid oldNspOid, Oid nspOid) (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("cannot move objects into or out of TOAST schema"))); - /* similarly for CONFLICT schema */ + /* similarly for pg_conflict schema */ if (nspOid == PG_CONFLICT_NAMESPACE || oldNspOid == PG_CONFLICT_NAMESPACE) ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), - errmsg("cannot move objects into or out of CONFLICT schema"))); + errmsg("cannot move objects into or out of the pg_conflict schema"))); } /* diff --git a/src/backend/catalog/pg_publication.c b/src/backend/catalog/pg_publication.c index c680356a10b..93791210e35 100644 --- a/src/backend/catalog/pg_publication.c +++ b/src/backend/catalog/pg_publication.c @@ -92,6 +92,13 @@ check_publication_add_relation(PublicationRelInfo *pri) errmsg(errormsg, relname), errdetail("This operation is not supported for system tables."))); + /* Can't be conflict log table */ + if (IsConflictNamespace(RelationGetNamespace(targetrel))) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg(errormsg, relname), + errdetail("This operation is not supported for conflict log tables."))); + /* UNLOGGED and TEMP relations cannot be part of publication. */ if (targetrel->rd_rel->relpersistence == RELPERSISTENCE_TEMP) ereport(ERROR, @@ -103,13 +110,6 @@ check_publication_add_relation(PublicationRelInfo *pri) (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg(errormsg, relname), errdetail("This operation is not supported for unlogged tables."))); - - /* Can't be conflict log table */ - if (IsConflictNamespace(RelationGetNamespace(targetrel))) - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg(errormsg, relname), - errdetail("This operation is not supported for conflict log tables."))); } /* @@ -165,7 +165,7 @@ is_publishable_class(Oid relid, Form_pg_class reltuple) reltuple->relkind == RELKIND_PARTITIONED_TABLE || reltuple->relkind == RELKIND_SEQUENCE) && !IsCatalogRelationOid(relid) && - !IsConflictClass(reltuple) && + !IsConflictLogTableClass(reltuple) && reltuple->relpersistence == RELPERSISTENCE_PERMANENT && relid >= FirstNormalObjectId; } diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index c10f6bf73b0..892b57a3da7 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -21,12 +21,10 @@ #include "access/xact.h" #include "catalog/catalog.h" #include "catalog/dependency.h" -#include "catalog/heap.h" #include "catalog/indexing.h" #include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/objectaddress.h" -#include "catalog/pg_am_d.h" #include "catalog/pg_authid_d.h" #include "catalog/pg_database_d.h" #include "catalog/pg_foreign_server.h" @@ -145,7 +143,7 @@ static List *merge_publications(List *oldpublist, List *newpublist, bool addpub, static void ReportSlotConnectionError(List *rstates, Oid subid, char *slotname, char *err); static void CheckAlterSubOption(Subscription *sub, const char *option, bool slot_needs_update, bool isTopLevel); -static Oid create_conflict_log_table(Oid subid, char *subname, Oid subowner); + /* * Common option parsing function for CREATE and ALTER SUBSCRIPTION commands. @@ -838,13 +836,11 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, values[Anum_pg_subscription_suborigin - 1] = CStringGetTextDatum(opts.origin); - /* Always set the destination, default will be 'log'. */ values[Anum_pg_subscription_subconflictlogdest - 1] = CStringGetTextDatum(ConflictLogDestNames[opts.conflictlogdest]); /* If logging to a table is required, physically create the table. */ - if (opts.conflictlogdest == CONFLICT_LOG_DEST_TABLE || - opts.conflictlogdest == CONFLICT_LOG_DEST_ALL) + if (CONFLICTS_LOGGED_TO_TABLE(opts.conflictlogdest)) logrelid = create_conflict_log_table(subid, stmt->subname, owner); /* Store table OID in the catalog. */ @@ -1805,10 +1801,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, if (opts.conflictlogdest != old_dest) { - bool want_table = (opts.conflictlogdest == CONFLICT_LOG_DEST_TABLE || - opts.conflictlogdest == CONFLICT_LOG_DEST_ALL); - bool has_oldtable = (old_dest == CONFLICT_LOG_DEST_TABLE || - old_dest == CONFLICT_LOG_DEST_ALL); + bool want_table = CONFLICTS_LOGGED_TO_TABLE(opts.conflictlogdest); + bool has_oldtable = CONFLICTS_LOGGED_TO_TABLE(old_dest); values[Anum_pg_subscription_subconflictlogdest - 1] = CStringGetTextDatum(ConflictLogDestNames[opts.conflictlogdest]); @@ -1828,25 +1822,8 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, } else if (!want_table && has_oldtable) { - ObjectAddress object; - - /* - * Conflict log tables are recorded as internal - * dependencies of the subscription. Drop the - * table if it is not required anymore to avoid - * stale or orphaned relations. - * - * XXX: At present, only conflict log tables are - * managed this way. In future if we introduce - * additional internal dependencies, we may need - * a targeted deletion to avoid deletion of any - * other objects. - */ - ObjectAddressSet(object, SubscriptionRelationId, - subid); - performDeletion(&object, DROP_CASCADE, - PERFORM_DELETION_INTERNAL | - PERFORM_DELETION_SKIP_ORIGINAL); + drop_conflict_log_table(sub->oid, sub->name, + sub->conflictlogrelid); values[Anum_pg_subscription_subconflictlogrelid - 1] = ObjectIdGetDatum(InvalidOid); @@ -2282,6 +2259,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) HeapTuple tup; Oid subid; Oid subowner; + Oid subconflictlogrelid; Datum datum; bool isnull; char *subname; @@ -2295,7 +2273,6 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) Form_pg_subscription form; List *rstates; bool must_use_password; - ObjectAddress object; /* * The launcher may concurrently start a new worker for this subscription. @@ -2328,6 +2305,7 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) form = (Form_pg_subscription) GETSTRUCT(tup); subid = form->oid; subowner = form->subowner; + subconflictlogrelid = form->subconflictlogrelid; must_use_password = !superuser_arg(subowner) && form->subpasswordrequired; /* must be owner */ @@ -2482,18 +2460,8 @@ DropSubscription(DropSubscriptionStmt *stmt, bool isTopLevel) deleteDependencyRecordsFor(SubscriptionRelationId, subid, false); deleteSharedDependencyRecordsFor(SubscriptionRelationId, subid, 0); - /* - * Conflict log tables are recorded as internal dependencies of the - * subscription. We must drop the dependent objects before the - * subscription itself is removed. By using - * PERFORM_DELETION_SKIP_ORIGINAL, we ensure that only the conflict log - * table is reaped while the subscription remains for the final deletion - * step. - */ - ObjectAddressSet(object, SubscriptionRelationId, subid); - performDeletion(&object, DROP_CASCADE, - PERFORM_DELETION_INTERNAL | - PERFORM_DELETION_SKIP_ORIGINAL); + if (OidIsValid(subconflictlogrelid)) + drop_conflict_log_table(subid, subname, subconflictlogrelid); /* Remove any associated relation synchronization states. */ RemoveSubscriptionRel(subid, InvalidOid); @@ -3534,146 +3502,3 @@ defGetStreamingMode(DefElem *def) def->defname))); return LOGICALREP_STREAM_OFF; /* keep compiler quiet */ } - -/* - * Builds the TupleDesc for the conflict log table. - */ -static TupleDesc -create_conflict_log_table_tupdesc(void) -{ - TupleDesc tupdesc; - - tupdesc = CreateTemplateTupleDesc(MAX_CONFLICT_ATTR_NUM); - - for (int i = 0; i < MAX_CONFLICT_ATTR_NUM; i++) - { - Oid type_oid = ConflictLogSchema[i].atttypid; - - /* - * Special handling for the JSON array type for proper - * TupleDescInitEntry call. - */ - if (type_oid == JSONARRAYOID) - type_oid = get_array_type(JSONOID); - - TupleDescInitEntry(tupdesc, i + 1, - ConflictLogSchema[i].attname, - type_oid, - -1, 0); - } - - TupleDescFinalize(tupdesc); - - return tupdesc; -} - -/* - * Create a structured conflict log table for a subscription. - * - * The table is created within the system-managed 'pg_conflict' namespace to - * prevent users from manually dropping or altering it. This also prevents - * accidental name collisions with user-created tables with the same name. - * - * The table name is generated automatically using the subscription's OID - * (e.g., "pg_conflict_log_") to ensure uniqueness within the cluster - * and to avoid collisions during subscription renames. - */ -static Oid -create_conflict_log_table(Oid subid, char *subname, Oid subowner) -{ - TupleDesc tupdesc; - Oid relid; - ObjectAddress myself; - ObjectAddress subaddr; - char relname[NAMEDATALEN]; - - snprintf(relname, NAMEDATALEN, "pg_conflict_log_%u", subid); - - /* - * Check for an existing table with the sname name in the pg_conflict namespace. - * A collision should not occur under normal operation, but we must handle cases - * where a table has been created manually. - */ - if (OidIsValid(get_relname_relid(relname, PG_CONFLICT_NAMESPACE))) - ereport(ERROR, - (errcode(ERRCODE_DUPLICATE_TABLE), - errmsg("conflict log table pg_conflict.\"%s\" already exists", relname), - errhint("A table with the same name already exists. " - "To proceed, drop the existing table and retry."))); - - /* Build the tuple descriptor for the new table. */ - tupdesc = create_conflict_log_table_tupdesc(); - - /* Create conflict log table. */ - relid = heap_create_with_catalog(relname, - PG_CONFLICT_NAMESPACE, - 0, /* tablespace */ - InvalidOid, /* relid */ - InvalidOid, /* reltypeid */ - InvalidOid, /* reloftypeid */ - subowner, - HEAP_TABLE_AM_OID, - tupdesc, - NIL, - RELKIND_RELATION, - RELPERSISTENCE_PERMANENT, - false, /* shared_relation */ - false, /* mapped_relation */ - ONCOMMIT_NOOP, - (Datum) 0, /* reloptions */ - false, /* use_user_acl */ - true, /* allow_system_table_mods */ - true, /* is_internal */ - InvalidOid, /* relrewrite */ - NULL); /* typaddress */ - - /* - * Establish an internal dependency between the conflict log table and - * the subscription. - * - * We use DEPENDENCY_INTERNAL to signify that the table's lifecycle is - * strictly tied to the subscription, similar to how a TOAST table relates - * to its main table or a sequence relates to an identity column. - * - * This ensures the conflict log table is automatically reaped during a - * DROP SUBSCRIPTION via performDeletion(). - */ - ObjectAddressSet(myself, RelationRelationId, relid); - ObjectAddressSet(subaddr, SubscriptionRelationId, subid); - recordDependencyOn(&myself, &subaddr, DEPENDENCY_INTERNAL); - - /* Release tuple descriptor memory. */ - FreeTupleDesc(tupdesc); - - ereport(NOTICE, - (errmsg("created conflict log table \"%s\" for subscription \"%s\"", - get_qualified_objname(PG_CONFLICT_NAMESPACE, relname), - subname))); - - return relid; -} - -/* - * GetLogDestination - * - * Convert string to enum by comparing against standardized labels. - */ -ConflictLogDest -GetLogDestination(const char *dest) -{ - /* Empty string or NULL defaults to LOG. */ - if (dest == NULL || dest[0] == '\0' || pg_strcasecmp(dest, "log") == 0) - return CONFLICT_LOG_DEST_LOG; - - if (pg_strcasecmp(dest, "table") == 0) - return CONFLICT_LOG_DEST_TABLE; - - if (pg_strcasecmp(dest, "all") == 0) - return CONFLICT_LOG_DEST_ALL; - - /* Unrecognized string. */ - ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unrecognized conflict_log_destination value: \"%s\"", dest), - errhint("Valid values are \"log\", \"table\", and \"all\"."))); -} diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index adf6b0f01d9..ee1687e8676 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -2461,7 +2461,7 @@ truncate_check_rel(Oid relid, Form_pg_class reltuple) * to permit users to manually prune these logs to manage disk space. */ if (!allowSystemTableMods && IsSystemClass(relid, reltuple) && - !IsConflictClass(reltuple) + !IsConflictLogTableClass(reltuple) && (!IsBinaryUpgrade || (relid != LargeObjectRelationId && relid != LargeObjectMetadataRelationId))) diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c index 345640fe41d..2c1a4d8ab71 100644 --- a/src/backend/executor/execMain.c +++ b/src/backend/executor/execMain.c @@ -1201,7 +1201,7 @@ CheckValidResultRel(ResultRelInfo *resultRelInfo, CmdType operation, if (IsConflictNamespace(RelationGetNamespace(resultRel)) && operation != CMD_DELETE) ereport(ERROR, - (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), + (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("cannot modify or insert data into conflict log table \"%s\"", RelationGetRelationName(resultRel)), errdetail("Conflict log tables are system-managed and only support cleanup via DELETE or TRUNCATE."))); @@ -1279,13 +1279,12 @@ CheckValidRowMarkRel(Relation rel, RowMarkType markType) /* * Conflict log tables are managed by the system to record logical - * replication conflicts. We do not allow locking rows in CONFLICT - * relations. + * replication conflicts. */ if (IsConflictNamespace(RelationGetNamespace(rel))) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("cannot lock rows in conflict log table \"%s\"", + errmsg("cannot lock rows in the conflict log table \"%s\"", RelationGetRelationName(rel)))); } diff --git a/src/backend/replication/logical/conflict.c b/src/backend/replication/logical/conflict.c index d038e265ca9..31fb195b20d 100644 --- a/src/backend/replication/logical/conflict.c +++ b/src/backend/replication/logical/conflict.c @@ -17,6 +17,11 @@ #include "access/commit_ts.h" #include "access/genam.h" #include "access/tableam.h" +#include "catalog/dependency.h" +#include "catalog/heap.h" +#include "catalog/pg_am.h" +#include "catalog/pg_namespace.h" +#include "commands/subscriptioncmds.h" #include "executor/executor.h" #include "pgstat.h" #include "replication/conflict.h" @@ -24,13 +29,35 @@ #include "storage/lmgr.h" #include "utils/lsyscache.h" +/* + * String representations for the supported conflict logging destinations. + */ const char *const ConflictLogDestNames[] = { [CONFLICT_LOG_DEST_LOG] = "log", [CONFLICT_LOG_DEST_TABLE] = "table", [CONFLICT_LOG_DEST_ALL] = "all" }; -const ConflictLogColumnDef ConflictLogSchema[] = { +StaticAssertDecl(lengthof(ConflictLogDestNames) == 3, + "ConflictLogDestNames length mismatch"); + + +/* Structure to hold metadata for one column of the conflict log table */ +typedef struct ConflictLogColumnDef +{ + const char *attname; /* Column name */ + Oid atttypid; /* Data type OID */ +} ConflictLogColumnDef; + +/* + * Schema definition for conflict log tables. + * + * Defines the fixed schema of the per-subscription conflict log table created + * in the pg_conflict namespace. Each entry specifies the column name and its + * type OID; the table is created in this column order by + * create_conflict_log_table(). + */ +static const ConflictLogColumnDef ConflictLogSchema[] = { { .attname = "relid", .atttypid = OIDOID }, { .attname = "schemaname", .atttypid = TEXTOID }, { .attname = "relname", .atttypid = TEXTOID }, @@ -39,15 +66,12 @@ const ConflictLogColumnDef ConflictLogSchema[] = { { .attname = "remote_commit_lsn",.atttypid = LSNOID }, { .attname = "remote_commit_ts", .atttypid = TIMESTAMPTZOID }, { .attname = "remote_origin", .atttypid = TEXTOID }, - { .attname = "replica_identity", .atttypid = JSONOID }, { .attname = "remote_tuple", .atttypid = JSONOID }, + { .attname = "replica_identity", .atttypid = JSONOID }, { .attname = "local_conflicts", .atttypid = JSONARRAYOID } }; -StaticAssertDecl(lengthof(ConflictLogSchema) == MAX_CONFLICT_ATTR_NUM, - "ConflictLogSchema length mismatch"); -StaticAssertDecl(lengthof(ConflictLogDestNames) == 3, - "ConflictLogDestNames length mismatch"); +#define NUM_CONFLICT_ATTRS lengthof(ConflictLogSchema) static const char *const ConflictTypeNames[] = { [CT_INSERT_EXISTS] = "insert_exists", @@ -79,6 +103,186 @@ static void get_tuple_desc(EState *estate, ResultRelInfo *relinfo, static char *build_index_value_desc(EState *estate, Relation localrel, TupleTableSlot *slot, Oid indexoid); +/* + * Builds the TupleDesc for the conflict log table. + */ +static TupleDesc +create_conflict_log_table_tupdesc(void) +{ + TupleDesc tupdesc; + + tupdesc = CreateTemplateTupleDesc(NUM_CONFLICT_ATTRS); + + for (int i = 0; i < NUM_CONFLICT_ATTRS; i++) + { + Oid type_oid = ConflictLogSchema[i].atttypid; + + /* + * Special handling for the JSON array type for proper + * TupleDescInitEntry call. + */ + if (type_oid == JSONARRAYOID) + type_oid = get_array_type(JSONOID); + + TupleDescInitEntry(tupdesc, i + 1, + ConflictLogSchema[i].attname, + type_oid, + -1, 0); + } + + TupleDescFinalize(tupdesc); + + return tupdesc; +} + +/* + * Create a structured conflict log table for a subscription. + * + * The table is created within the system-managed 'pg_conflict' namespace to + * prevent users from manually dropping or altering it. This also prevents + * accidental name collisions with user-created tables with the same name. + * + * The table name is generated automatically using the subscription's OID + * (e.g., "pg_conflict_log_for_subid_") to ensure uniqueness within the + * cluster and to avoid collisions during subscription renames. + */ +Oid +create_conflict_log_table(Oid subid, char *subname, Oid subowner) +{ + TupleDesc tupdesc; + Oid relid; + ObjectAddress myself; + ObjectAddress subaddr; + char relname[NAMEDATALEN]; + + snprintf(relname, NAMEDATALEN, "pg_conflict_log_for_subid_%u", subid); + + /* + * Check for an existing table with the same name in the pg_conflict namespace. + * A collision should not occur under normal operation, but we must handle cases + * where a table has been created manually when allow_system_tables_mods is + * ON. + */ + if (OidIsValid(get_relname_relid(relname, PG_CONFLICT_NAMESPACE))) + ereport(ERROR, + (errcode(ERRCODE_DUPLICATE_TABLE), + errmsg("conflict log table pg_conflict.\"%s\" already exists", relname), + errhint("To proceed, drop the existing table and retry."))); + + /* Build the tuple descriptor for the new table. */ + tupdesc = create_conflict_log_table_tupdesc(); + + /* Create conflict log table. */ + relid = heap_create_with_catalog(relname, + PG_CONFLICT_NAMESPACE, + 0, /* tablespace */ + InvalidOid, /* relid */ + InvalidOid, /* reltypeid */ + InvalidOid, /* reloftypeid */ + subowner, + HEAP_TABLE_AM_OID, + tupdesc, + NIL, + RELKIND_RELATION, + RELPERSISTENCE_PERMANENT, + false, /* shared_relation */ + false, /* mapped_relation */ + ONCOMMIT_NOOP, + (Datum) 0, /* reloptions */ + false, /* use_user_acl */ + true, /* allow_system_table_mods */ + true, /* is_internal */ + InvalidOid, /* relrewrite */ + NULL); /* typaddress */ + Assert(OidIsValid(relid)); + + /* + * Establish an internal dependency between the conflict log table and + * the subscription. + * + * We use DEPENDENCY_INTERNAL to signify that the table's lifecycle is + * strictly tied to the subscription, similar to how a TOAST table relates + * to its main table or a sequence relates to an identity column. + * + * This ensures the conflict log table is automatically reaped during a + * DROP SUBSCRIPTION via performDeletion(). + */ + ObjectAddressSet(myself, RelationRelationId, relid); + ObjectAddressSet(subaddr, SubscriptionRelationId, subid); + recordDependencyOn(&myself, &subaddr, DEPENDENCY_INTERNAL); + + /* Release tuple descriptor memory. */ + FreeTupleDesc(tupdesc); + + ereport(NOTICE, + (errmsg("created conflict log table \"%s\" for subscription \"%s\"", + get_qualified_objname(PG_CONFLICT_NAMESPACE, relname), + subname))); + + return relid; +} + +/* + * drop_conflict_log_table + * Drop the conflict log table associated with a subscription. + * + * The conflict log table is registered as an internal dependency of the + * subscription. This function removes the dependency by performing a + * cascading deletion on the subscription object, which in turn drops the + * associated conflict log table. + * + * This is used to clean up conflict log tables that are no longer required, + * preventing accumulation of stale or orphaned relations. + * + * NOTE: + * Only conflict log tables are currently managed via this internal dependency + * mechanism. If additional internal dependencies are introduced in future, + * this function may require refinement to avoid unintended deletions. + */ +void +drop_conflict_log_table(Oid subid, char *subname, Oid subconflictlogrelid) +{ + ObjectAddress object; + char *conflictrelname; + + conflictrelname = get_rel_name(subconflictlogrelid); + + ObjectAddressSet(object, SubscriptionRelationId, subid); + performDeletion(&object, DROP_CASCADE, + PERFORM_DELETION_INTERNAL | + PERFORM_DELETION_SKIP_ORIGINAL); + + ereport(NOTICE, + errmsg("dropped conflict log table \"%s\" for subscription \"%s\"", + get_qualified_objname(PG_CONFLICT_NAMESPACE, conflictrelname), + subname)); +} + +/* + * GetLogDestination + * + * Convert string to enum by comparing against standardized labels. + */ +ConflictLogDest +GetLogDestination(const char *dest) +{ + /* Empty string or NULL defaults to LOG. */ + if (dest == NULL || dest[0] == '\0' || pg_strcasecmp(dest, "log") == 0) + return CONFLICT_LOG_DEST_LOG; + + if (pg_strcasecmp(dest, "table") == 0) + return CONFLICT_LOG_DEST_TABLE; + + if (pg_strcasecmp(dest, "all") == 0) + return CONFLICT_LOG_DEST_ALL; + + /* Unrecognized string. */ + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("unrecognized conflict_log_destination value: \"%s\"", dest), + errhint("Valid values are \"log\", \"table\", and \"all\"."))); +} + /* * Get the xmin and commit timestamp data (origin and timestamp) associated * with the provided local row. diff --git a/src/include/catalog/catalog.h b/src/include/catalog/catalog.h index 8193229f2e2..cd05974b86c 100644 --- a/src/include/catalog/catalog.h +++ b/src/include/catalog/catalog.h @@ -25,7 +25,7 @@ extern bool IsInplaceUpdateRelation(Relation relation); extern bool IsSystemClass(Oid relid, Form_pg_class reltuple); extern bool IsToastClass(Form_pg_class reltuple); -extern bool IsConflictClass(Form_pg_class reltuple); +extern bool IsConflictLogTableClass(Form_pg_class reltuple); extern bool IsCatalogRelationOid(Oid relid); extern bool IsCatalogTextUniqueIndexOid(Oid relid); diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index 5f214d3586b..cc31b4d00bc 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -97,6 +97,14 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW Oid subconflictlogrelid; /* Relid of the conflict log table. */ #ifdef CATALOG_VARLEN /* variable-length fields start here */ + /* + * Strategy for logging replication conflicts: + * 'log' - server log only, + * 'table' - conflict log table only, + * 'all' - both log and table. + */ + text subconflictlogdest BKI_FORCE_NOT_NULL; + /* Connection string to the publisher */ text subconninfo; /* Set if connecting with connection string */ @@ -112,14 +120,6 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW /* List of publications subscribed to */ text subpublications[1] BKI_FORCE_NOT_NULL; - /* - * Strategy for logging replication conflicts: - * 'log' - server log only, - * 'table' - conflict log table only, - * 'all' - both log and table. - */ - text subconflictlogdest BKI_FORCE_NOT_NULL; - /* Only publish data originating from the specified origin */ text suborigin BKI_DEFAULT(LOGICALREP_ORIGIN_ANY); #endif diff --git a/src/include/commands/subscriptioncmds.h b/src/include/commands/subscriptioncmds.h index a895127f8fe..63504232a14 100644 --- a/src/include/commands/subscriptioncmds.h +++ b/src/include/commands/subscriptioncmds.h @@ -17,7 +17,6 @@ #include "catalog/objectaddress.h" #include "parser/parse_node.h" -#include "replication/conflict.h" extern ObjectAddress CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, bool isTopLevel); @@ -37,6 +36,4 @@ extern void CheckSubDeadTupleRetention(bool check_guc, bool sub_disabled, bool retention_active, bool max_retention_set); -extern ConflictLogDest GetLogDestination(const char *dest); - #endif /* SUBSCRIPTIONCMDS_H */ diff --git a/src/include/replication/conflict.h b/src/include/replication/conflict.h index 00a9cbec264..39a94441984 100644 --- a/src/include/replication/conflict.h +++ b/src/include/replication/conflict.h @@ -94,23 +94,20 @@ typedef enum ConflictLogDest CONFLICT_LOG_DEST_ALL /* Both log and table */ } ConflictLogDest; +#define CONFLICTS_LOGGED_TO_TABLE(dest) \ + ((dest == CONFLICT_LOG_DEST_TABLE) || (dest == CONFLICT_LOG_DEST_ALL)) +#define CONFLICTS_LOGGED_TO_FILE(dest) \ + ((dest == CONFLICT_LOG_DEST_LOG) || (dest == CONFLICT_LOG_DEST_ALL)) + /* * Array mapping for converting internal enum to string. */ extern PGDLLIMPORT const char *const ConflictLogDestNames[]; -/* Structure to hold metadata for one column of the conflict log table */ -typedef struct ConflictLogColumnDef -{ - const char *attname; /* Column name */ - Oid atttypid; /* Data type OID */ -} ConflictLogColumnDef; - -/* The single source of truth for the conflict log table schema */ -extern PGDLLIMPORT const ConflictLogColumnDef ConflictLogSchema[]; - -#define MAX_CONFLICT_ATTR_NUM 11 - +extern Oid create_conflict_log_table(Oid subid, char *subname, Oid subowner); +extern void drop_conflict_log_table(Oid subid, char *subname, + Oid subconflictlogrelid); +extern ConflictLogDest GetLogDestination(const char *dest); extern bool GetTupleTransactionInfo(TupleTableSlot *localslot, TransactionId *xmin, ReplOriginId *localorigin, diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out index 85f9c60f449..6ac5ed312f4 100644 --- a/src/test/regress/expected/subscription.out +++ b/src/test/regress/expected/subscription.out @@ -124,18 +124,18 @@ CREATE SUBSCRIPTION regress_testsub4 CONNECTION 'dbname=regress_doesnotexist' PU WARNING: subscription was created, but is not connected HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ regress_testsub4 - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table -------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub4 SET (origin = any); \dRs+ regress_testsub4 - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table -------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub3; @@ -200,10 +200,10 @@ ALTER SUBSCRIPTION regress_testsub CONNECTION 'foobar'; ERROR: invalid connection string syntax: missing "=" after "foobar" in connection info string \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | test subscription | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | test subscription (1 row) ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false); @@ -212,10 +212,10 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = 'newname'); ALTER SUBSCRIPTION regress_testsub SET (password_required = false); ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+-------------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription (1 row) ALTER SUBSCRIPTION regress_testsub SET (password_required = true); @@ -231,10 +231,10 @@ ERROR: unrecognized subscription parameter: "create_slot" -- ok ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345'); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+-------------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00012345 | test subscription | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00012345 | test subscription (1 row) -- ok - with lsn = NONE @@ -243,10 +243,10 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = NONE); ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/0'); ERROR: invalid WAL location (LSN): 0/0 \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+-------------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist2 | -1 | 0/00000000 | test subscription (1 row) BEGIN; @@ -282,10 +282,10 @@ ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = '80s'); ALTER SUBSCRIPTION regress_testsub_foo SET (wal_receiver_timeout = 'foobar'); ERROR: invalid value for parameter "wal_receiver_timeout": "foobar" \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ----------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+-------------------+--------------------------+-------------------- - regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | local | dbname=regress_doesnotexist2 | 80s | 0/00000000 | test subscription | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+------------------------------+------------------+------------+------------------- + regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | | f | 0 | f | local | dbname=regress_doesnotexist2 | 80s | 0/00000000 | test subscription (1 row) -- rename back to keep the rest simple @@ -314,19 +314,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB WARNING: subscription was created, but is not connected HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (binary = false); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub; @@ -338,27 +338,27 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB WARNING: subscription was created, but is not connected HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = false); ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- fail - publication already exists @@ -373,10 +373,10 @@ ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refr ALTER SUBSCRIPTION regress_testsub ADD PUBLICATION testpub1, testpub2 WITH (refresh = false); ERROR: publication "testpub1" is already in subscription "regress_testsub" \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- fail - publication used more than once @@ -391,10 +391,10 @@ ERROR: publication "testpub3" is not in subscription "regress_testsub" -- ok - delete publications ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (refresh = false); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) DROP SUBSCRIPTION regress_testsub; @@ -430,19 +430,19 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB WARNING: subscription was created, but is not connected HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- we can alter streaming when two_phase enabled ALTER SUBSCRIPTION regress_testsub SET (streaming = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -452,10 +452,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB WARNING: subscription was created, but is not connected HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -468,18 +468,18 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB WARNING: subscription was created, but is not connected HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -492,10 +492,10 @@ CREATE SUBSCRIPTION regress_testsub CONNECTION 'dbname=regress_doesnotexist' PUB WARNING: subscription was created, but is not connected HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -509,19 +509,19 @@ NOTICE: max_retention_duration is ineffective when retain_dead_tuples is disabl WARNING: subscription was created, but is not connected HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 1000 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 1000 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) -- ok ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = 0); \dRs+ - List of subscriptions - Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description | Conflict log destination | Conflict log table ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+-------------+--------------------------+-------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | | log | - + List of subscriptions + Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Server | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Receiver timeout | Skip LSN | Description +-----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------------+------------+------------- + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | | f | 0 | f | off | dbname=regress_doesnotexist | -1 | 0/00000000 | (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -585,7 +585,7 @@ SET client_min_messages = WARNING; CREATE SUBSCRIPTION regress_conflict_fail CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'invalid'); ERROR: unrecognized conflict_log_destination value: "invalid" HINT: Valid values are "log", "table", and "all". --- verify subconflictlogdest is 'log' and relid is 0 (InvalidOid) for default case +-- verify subconflictlogdest is 'log' and subconflictlogrelid is 0 (InvalidOid) for default case CREATE SUBSCRIPTION regress_conflict_log_default CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false); WARNING: subscription was created, but is not connected HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. @@ -607,11 +607,11 @@ FROM pg_subscription WHERE subname = 'regress_conflict_empty_str'; regress_conflict_empty_str | log | 0 (1 row) --- this should generate an internal conflict log table named pg_conflict_log_$subid$ +-- this should generate an internal conflict log table named pg_conflict_log_for_subid_$subid$ CREATE SUBSCRIPTION regress_conflict_test1 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'table'); WARNING: subscription was created, but is not connected HINT: To initiate replication, you must manually create the replication slot, enable the subscription, and alter the subscription to refresh publications. --- check metadata in pg_subscription: destination should be 'table' and relid valid +-- check metadata in pg_subscription: destination should be 'table' and subconflictlogrelid valid SELECT subname, subconflictlogdest, subconflictlogrelid > 0 AS has_relid FROM pg_subscription WHERE subname = 'regress_conflict_test1'; subname | subconflictlogdest | has_relid @@ -623,7 +623,7 @@ FROM pg_subscription WHERE subname = 'regress_conflict_test1'; -- and it is located in the 'pg_conflict' namespace SELECT n.nspname, (c.oid = s.subconflictlogrelid) AS "oid_matches" FROM pg_class c -JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid +JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid JOIN pg_namespace n ON c.relnamespace = n.oid WHERE s.subname = 'regress_conflict_test1'; nspname | oid_matches @@ -635,7 +635,7 @@ WHERE s.subname = 'regress_conflict_test1'; SELECT a.attnum, a.attname FROM pg_attribute a JOIN pg_class c ON a.attrelid = c.oid -JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid +JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_test1' AND a.attnum > 0 ORDER BY a.attnum; attnum | attname @@ -648,8 +648,8 @@ WHERE s.subname = 'regress_conflict_test1' AND a.attnum > 0 6 | remote_commit_lsn 7 | remote_commit_ts 8 | remote_origin - 9 | replica_identity - 10 | remote_tuple + 9 | remote_tuple + 10 | replica_identity 11 | local_conflicts (11 rows) @@ -686,7 +686,7 @@ FROM pg_subscription WHERE subname = 'regress_conflict_test2'; (1 row) -- transition from 'table' to 'log' --- should drop the table and clear relid +-- should drop the table and clear subconflictlogrelid ALTER SUBSCRIPTION regress_conflict_test2 SET (conflict_log_destination = 'log'); SELECT subconflictlogdest, subconflictlogrelid FROM pg_subscription WHERE subname = 'regress_conflict_test2'; @@ -698,7 +698,7 @@ FROM pg_subscription WHERE subname = 'regress_conflict_test2'; -- verify the physical table is gone SELECT count(*) FROM pg_class c -JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid +JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_test2'; count ------- @@ -738,7 +738,7 @@ ALTER SUBSCRIPTION regress_conflict_test1 SET (conflict_log_destination = 'table SET client_min_messages = NOTICE; DO $$ BEGIN - EXECUTE 'DROP TABLE ' || (SELECT 'pg_conflict.pg_conflict_log_' || oid FROM pg_subscription WHERE subname = 'regress_conflict_test1'); + EXECUTE 'DROP TABLE ' || (SELECT 'pg_conflict.pg_conflict_log_for_subid_' || oid FROM pg_subscription WHERE subname = 'regress_conflict_test1'); EXCEPTION WHEN insufficient_privilege THEN RAISE NOTICE 'captured expected error: insufficient_privilege'; END $$; @@ -747,7 +747,8 @@ NOTICE: captured expected error: insufficient_privilege ALTER SUBSCRIPTION regress_conflict_test1 DISABLE; ALTER SUBSCRIPTION regress_conflict_test1 SET (slot_name = NONE); -- Verify the table OID for reap check -SELECT 'pg_conflict_log_' || oid AS internal_tablename FROM pg_subscription WHERE subname = 'regress_conflict_test1' \gset +SELECT 'pg_conflict_log_for_subid_' || oid AS internal_tablename FROM pg_subscription WHERE subname = 'regress_conflict_test1' \gset +SET client_min_messages = WARNING; DROP SUBSCRIPTION regress_conflict_test1; -- should return NULL, meaning the conflict log table was reaped via dependency SELECT to_regclass(:'internal_tablename'); @@ -759,7 +760,6 @@ SELECT to_regclass(:'internal_tablename'); -- -- Additional Namespace and Table Protection Tests -- -SET client_min_messages = WARNING; -- Setup: Ensure we have a subscription with a conflict log table CREATE SUBSCRIPTION regress_conflict_protection_test CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'table'); @@ -774,7 +774,7 @@ DECLARE tab_name text; BEGIN SELECT 'pg_conflict.' || relname INTO tab_name - FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid + FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_protection_test'; RAISE NOTICE 'Attempting ALTER TABLE on internal conflict log table'; @@ -792,14 +792,14 @@ DECLARE tab_name text; BEGIN SELECT 'pg_conflict.' || relname INTO tab_name - FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid + FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_protection_test'; EXECUTE 'INSERT INTO ' || tab_name || ' (relname) VALUES (''mytest'')'; -EXCEPTION WHEN insufficient_privilege THEN - RAISE NOTICE 'captured expected error: insufficient_privilege during INSERT'; +EXCEPTION WHEN wrong_object_type THEN + RAISE NOTICE 'captured expected error: wrong_object_type during INSERT'; END $$; -NOTICE: captured expected error: insufficient_privilege during INSERT +NOTICE: captured expected error: wrong_object_type during INSERT -- Test Manual UPDATE on conflict log table -- This should fail because the table is system-managed -- Hiding the OID in the error message by catching the exception @@ -808,19 +808,19 @@ DECLARE tab_name text; BEGIN SELECT 'pg_conflict.' || relname INTO tab_name - FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid + FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_protection_test'; EXECUTE 'UPDATE ' || tab_name || ' SET relname = ''mytest'' '; -EXCEPTION WHEN insufficient_privilege THEN - RAISE NOTICE 'captured expected error: insufficient_privilege during UPDATE'; +EXCEPTION WHEN wrong_object_type THEN + RAISE NOTICE 'captured expected error: wrong_object_type during UPDATE'; END $$; -NOTICE: captured expected error: insufficient_privilege during UPDATE +NOTICE: captured expected error: wrong_object_type during UPDATE -- Trying to perform TRUNCATE/DELETE on the internal conflict log table -- This should be allowed so that user can perform cleanup SELECT 'pg_conflict.' || relname AS conflict_tab FROM pg_class c -JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid +JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_protection_test' \gset TRUNCATE :conflict_tab; DELETE FROM :conflict_tab; @@ -828,13 +828,14 @@ DELETE FROM :conflict_tab; -- This should fail as the namespace is reserved for conflict logs tables CREATE TABLE pg_conflict.manual_table (id int); ERROR: permission denied to create "pg_conflict.manual_table" -DETAIL: System catalog modifications are currently disallowed. +DETAIL: Conflict schema modifications are currently disallowed. -- Moving an existing table into the pg_conflict namespace -- Users should not be able to move their own tables within this namespace CREATE TABLE public.test_move (id int); ALTER TABLE public.test_move SET SCHEMA pg_conflict; -ERROR: cannot move objects into or out of CONFLICT schema +ERROR: cannot move objects into or out of the pg_conflict schema DROP TABLE public.test_move; +SET client_min_messages = WARNING; -- Clean up remaining test subscription ALTER SUBSCRIPTION regress_conflict_log_default DISABLE; ALTER SUBSCRIPTION regress_conflict_log_default SET (slot_name = NONE); diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql index d155f24fdbb..76c07f64ef3 100644 --- a/src/test/regress/sql/subscription.sql +++ b/src/test/regress/sql/subscription.sql @@ -442,7 +442,7 @@ SET client_min_messages = WARNING; -- fail - unrecognized parameter value CREATE SUBSCRIPTION regress_conflict_fail CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'invalid'); --- verify subconflictlogdest is 'log' and relid is 0 (InvalidOid) for default case +-- verify subconflictlogdest is 'log' and subconflictlogrelid is 0 (InvalidOid) for default case CREATE SUBSCRIPTION regress_conflict_log_default CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false); SELECT subname, subconflictlogdest, subconflictlogrelid FROM pg_subscription WHERE subname = 'regress_conflict_log_default'; @@ -452,10 +452,10 @@ CREATE SUBSCRIPTION regress_conflict_empty_str CONNECTION 'dbname=regress_doesno SELECT subname, subconflictlogdest, subconflictlogrelid FROM pg_subscription WHERE subname = 'regress_conflict_empty_str'; --- this should generate an internal conflict log table named pg_conflict_log_$subid$ +-- this should generate an internal conflict log table named pg_conflict_log_for_subid_$subid$ CREATE SUBSCRIPTION regress_conflict_test1 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'table'); --- check metadata in pg_subscription: destination should be 'table' and relid valid +-- check metadata in pg_subscription: destination should be 'table' and subconflictlogrelid valid SELECT subname, subconflictlogdest, subconflictlogrelid > 0 AS has_relid FROM pg_subscription WHERE subname = 'regress_conflict_test1'; @@ -463,7 +463,7 @@ FROM pg_subscription WHERE subname = 'regress_conflict_test1'; -- and it is located in the 'pg_conflict' namespace SELECT n.nspname, (c.oid = s.subconflictlogrelid) AS "oid_matches" FROM pg_class c -JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid +JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid JOIN pg_namespace n ON c.relnamespace = n.oid WHERE s.subname = 'regress_conflict_test1'; @@ -471,7 +471,7 @@ WHERE s.subname = 'regress_conflict_test1'; SELECT a.attnum, a.attname FROM pg_attribute a JOIN pg_class c ON a.attrelid = c.oid -JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid +JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_test1' AND a.attnum > 0 ORDER BY a.attnum; @@ -499,7 +499,7 @@ SELECT subconflictlogdest, subconflictlogrelid = :old_relid AS relid_unchanged FROM pg_subscription WHERE subname = 'regress_conflict_test2'; -- transition from 'table' to 'log' --- should drop the table and clear relid +-- should drop the table and clear subconflictlogrelid ALTER SUBSCRIPTION regress_conflict_test2 SET (conflict_log_destination = 'log'); SELECT subconflictlogdest, subconflictlogrelid FROM pg_subscription WHERE subname = 'regress_conflict_test2'; @@ -507,7 +507,7 @@ FROM pg_subscription WHERE subname = 'regress_conflict_test2'; -- verify the physical table is gone SELECT count(*) FROM pg_class c -JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid +JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_test2'; -- @@ -541,7 +541,7 @@ ALTER SUBSCRIPTION regress_conflict_test1 SET (conflict_log_destination = 'table SET client_min_messages = NOTICE; DO $$ BEGIN - EXECUTE 'DROP TABLE ' || (SELECT 'pg_conflict.pg_conflict_log_' || oid FROM pg_subscription WHERE subname = 'regress_conflict_test1'); + EXECUTE 'DROP TABLE ' || (SELECT 'pg_conflict.pg_conflict_log_for_subid_' || oid FROM pg_subscription WHERE subname = 'regress_conflict_test1'); EXCEPTION WHEN insufficient_privilege THEN RAISE NOTICE 'captured expected error: insufficient_privilege'; END $$; @@ -551,8 +551,9 @@ ALTER SUBSCRIPTION regress_conflict_test1 DISABLE; ALTER SUBSCRIPTION regress_conflict_test1 SET (slot_name = NONE); -- Verify the table OID for reap check -SELECT 'pg_conflict_log_' || oid AS internal_tablename FROM pg_subscription WHERE subname = 'regress_conflict_test1' \gset +SELECT 'pg_conflict_log_for_subid_' || oid AS internal_tablename FROM pg_subscription WHERE subname = 'regress_conflict_test1' \gset +SET client_min_messages = WARNING; DROP SUBSCRIPTION regress_conflict_test1; -- should return NULL, meaning the conflict log table was reaped via dependency @@ -562,7 +563,6 @@ SELECT to_regclass(:'internal_tablename'); -- Additional Namespace and Table Protection Tests -- -SET client_min_messages = WARNING; -- Setup: Ensure we have a subscription with a conflict log table CREATE SUBSCRIPTION regress_conflict_protection_test CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'table'); @@ -577,7 +577,7 @@ DECLARE tab_name text; BEGIN SELECT 'pg_conflict.' || relname INTO tab_name - FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid + FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_protection_test'; RAISE NOTICE 'Attempting ALTER TABLE on internal conflict log table'; @@ -594,12 +594,12 @@ DECLARE tab_name text; BEGIN SELECT 'pg_conflict.' || relname INTO tab_name - FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid + FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_protection_test'; EXECUTE 'INSERT INTO ' || tab_name || ' (relname) VALUES (''mytest'')'; -EXCEPTION WHEN insufficient_privilege THEN - RAISE NOTICE 'captured expected error: insufficient_privilege during INSERT'; +EXCEPTION WHEN wrong_object_type THEN + RAISE NOTICE 'captured expected error: wrong_object_type during INSERT'; END $$; -- Test Manual UPDATE on conflict log table @@ -610,19 +610,19 @@ DECLARE tab_name text; BEGIN SELECT 'pg_conflict.' || relname INTO tab_name - FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid + FROM pg_class c JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_protection_test'; EXECUTE 'UPDATE ' || tab_name || ' SET relname = ''mytest'' '; -EXCEPTION WHEN insufficient_privilege THEN - RAISE NOTICE 'captured expected error: insufficient_privilege during UPDATE'; +EXCEPTION WHEN wrong_object_type THEN + RAISE NOTICE 'captured expected error: wrong_object_type during UPDATE'; END $$; -- Trying to perform TRUNCATE/DELETE on the internal conflict log table -- This should be allowed so that user can perform cleanup SELECT 'pg_conflict.' || relname AS conflict_tab FROM pg_class c -JOIN pg_subscription s ON c.relname = 'pg_conflict_log_' || s.oid +JOIN pg_subscription s ON c.relname = 'pg_conflict_log_for_subid_' || s.oid WHERE s.subname = 'regress_conflict_protection_test' \gset TRUNCATE :conflict_tab; DELETE FROM :conflict_tab; @@ -637,6 +637,8 @@ CREATE TABLE public.test_move (id int); ALTER TABLE public.test_move SET SCHEMA pg_conflict; DROP TABLE public.test_move; +SET client_min_messages = WARNING; + -- Clean up remaining test subscription ALTER SUBSCRIPTION regress_conflict_log_default DISABLE; ALTER SUBSCRIPTION regress_conflict_log_default SET (slot_name = NONE); -- 2.53.0