From e6a1c0a6f88f421f66a93a2483ca585f2a298c46 Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Wed, 24 Dec 2025 15:47:01 +0530 Subject: [PATCH v15 6/6] logical replication: allow combined conflict_log_destination settings Extend conflict_log_destination handling to support combined destination specifications. Previously, only log, table, or all were accepted. This change allows combinations of them like log, table and all, log, table etc --- src/backend/catalog/pg_subscription.c | 2 +- src/backend/commands/subscriptioncmds.c | 95 +++++++++++++++------- src/backend/replication/logical/conflict.c | 9 +- src/bin/pg_dump/pg_dump.c | 42 ++++++---- src/bin/pg_dump/t/002_pg_dump.pl | 4 +- src/include/catalog/pg_subscription.h | 4 +- src/include/commands/subscriptioncmds.h | 2 +- src/include/replication/conflict.h | 23 +++--- src/test/regress/expected/subscription.out | 72 +++++++++------- src/test/regress/sql/subscription.sql | 11 ++- src/tools/pgindent/typedefs.list | 3 +- 11 files changed, 170 insertions(+), 97 deletions(-) diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c index dae44c659f8..c1b8c2870c5 100644 --- a/src/backend/catalog/pg_subscription.c +++ b/src/backend/catalog/pg_subscription.c @@ -147,7 +147,7 @@ GetSubscription(Oid subid, bool missing_ok) datum = SysCacheGetAttrNotNull(SUBSCRIPTIONOID, tup, Anum_pg_subscription_sublogdestination); - sub->logdestination = TextDatumGetCString(datum); + sub->logdestination = textarray_to_stringlist(DatumGetArrayTypeP(datum)); /* Is the subscription owner a superuser? */ sub->ownersuperuser = superuser_arg(sub->owner); diff --git a/src/backend/commands/subscriptioncmds.c b/src/backend/commands/subscriptioncmds.c index 1fbe0d474cf..9809b8b56f4 100644 --- a/src/backend/commands/subscriptioncmds.c +++ b/src/backend/commands/subscriptioncmds.c @@ -59,6 +59,7 @@ #include "utils/pg_lsn.h" #include "utils/regproc.h" #include "utils/syscache.h" +#include "utils/varlena.h" /* * Options that can be specified by the user in CREATE/ALTER SUBSCRIPTION @@ -417,23 +418,22 @@ parse_subscription_options(ParseState *pstate, List *stmt_options, else if (IsSet(supported_opts, SUBOPT_CONFLICT_LOG_DESTINATION) && strcmp(defel->defname, "conflict_log_destination") == 0) { - char *val; - ConflictLogDest dest; + char *val; + List *dest; if (IsSet(opts->specified_opts, SUBOPT_CONFLICT_LOG_DESTINATION)) errorConflictingDefElem(defel, pstate); val = defGetString(defel); - dest = GetLogDestination(val); - - if (dest == CONFLICT_LOG_DEST_INVALID) + if (!SplitIdentifierString(val, ',', &dest)) ereport(ERROR, - (errcode(ERRCODE_INVALID_PARAMETER_VALUE), - errmsg("unrecognized conflict_log_destination value: \"%s\"", val), - errhint("Valid values are \"log\", \"table\", and \"all\"."))); + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("invalid list syntax in parameter \"%s\"", + "conflict_log_destination")); + + opts->logdest = GetLogDestination(dest, false); - opts->logdest = dest; opts->specified_opts |= SUBOPT_CONFLICT_LOG_DESTINATION; } else @@ -613,6 +613,30 @@ publicationListToArray(List *publist) return PointerGetDatum(arr); } +/* + * Build a text[] array representing the conflict_log_destination flags. + */ +static Datum +ConflictLogDestFlagsToArray(ConflictLogDest logdest) +{ + Datum datums[3]; + int ndatums = 0; + + if (CONFLICT_LOG_DEST_ALL_ENABLED(logdest)) + datums[ndatums++] = CStringGetTextDatum("all"); + else + { + if (CONFLICT_LOG_DEST_LOG_ENABLED(logdest)) + datums[ndatums++] = CStringGetTextDatum("log"); + + if (CONFLICT_LOG_DEST_TABLE_ENABLED(logdest)) + datums[ndatums++] = CStringGetTextDatum("table"); + } + + return PointerGetDatum( + construct_array_builtin(datums, ndatums, TEXTOID)); +} + /* * Create new subscription. */ @@ -784,7 +808,7 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, /* Always set the destination, default will be log. */ values[Anum_pg_subscription_sublogdestination - 1] = - CStringGetTextDatum(ConflictLogDestLabels[opts.logdest]); + ConflictLogDestFlagsToArray(opts.logdest); /* * If the conflict log destination includes 'table', generate an internal @@ -793,8 +817,8 @@ CreateSubscription(ParseState *pstate, CreateSubscriptionStmt *stmt, * format in the pg_subscription catalog tuple., then physically create * the table. */ - if (opts.logdest == CONFLICT_LOG_DEST_TABLE || - opts.logdest == CONFLICT_LOG_DEST_ALL) + if (CONFLICT_LOG_DEST_TABLE_ENABLED(opts.logdest) || + CONFLICT_LOG_DEST_ALL_ENABLED(opts.logdest)) { char conflict_table_name[NAMEDATALEN]; Oid namespaceId, logrelid; @@ -1424,16 +1448,16 @@ AlterSubscriptionConflictLogDestination(Subscription *sub, ConflictLogDest logdest, Oid *conflicttablerelid) { - ConflictLogDest old_dest = GetLogDestination(sub->logdestination); + ConflictLogDest old_dest = GetLogDestination(sub->logdestination, true); bool want_table; bool has_oldtable; bool update_relid = false; Oid relid = InvalidOid; - want_table = (logdest == CONFLICT_LOG_DEST_TABLE || - logdest == CONFLICT_LOG_DEST_ALL); - has_oldtable = (old_dest == CONFLICT_LOG_DEST_TABLE || - old_dest == CONFLICT_LOG_DEST_ALL); + want_table = (CONFLICT_LOG_DEST_TABLE_ENABLED(logdest) || + CONFLICT_LOG_DEST_ALL_ENABLED(logdest)); + has_oldtable = (CONFLICT_LOG_DEST_TABLE_ENABLED(old_dest) || + CONFLICT_LOG_DEST_ALL_ENABLED(old_dest)); if (want_table && !has_oldtable) { @@ -1856,7 +1880,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, if (IsSet(opts.specified_opts, SUBOPT_CONFLICT_LOG_DESTINATION)) { ConflictLogDest old_dest = - GetLogDestination(sub->logdestination); + GetLogDestination(sub->logdestination, true); if (opts.logdest != old_dest) { @@ -1864,7 +1888,7 @@ AlterSubscription(ParseState *pstate, AlterSubscriptionStmt *stmt, Oid relid = InvalidOid; values[Anum_pg_subscription_sublogdestination - 1] = - CStringGetTextDatum(ConflictLogDestLabels[opts.logdest]); + ConflictLogDestFlagsToArray(opts.logdest); replaces[Anum_pg_subscription_sublogdestination - 1] = true; update_relid = AlterSubscriptionConflictLogDestination(sub, opts.logdest, &relid); if (update_relid) @@ -3544,23 +3568,38 @@ GetConflictLogTableName(char *dest, Oid subid) /* * GetLogDestination * - * Convert string to enum by comparing against standardized labels. + * Convert log destination List of strings to enums. */ ConflictLogDest -GetLogDestination(const char *dest) +GetLogDestination(List *destlist, bool strnodelist) { - /* Empty string or NULL defaults to LOG. */ - if (dest == NULL || dest[0] == '\0') + ConflictLogDest logdest = CONFLICT_LOG_DEST_INVALID; + ListCell *cell; + + if (destlist == NIL) return CONFLICT_LOG_DEST_LOG; - for (int i = CONFLICT_LOG_DEST_LOG; i <= CONFLICT_LOG_DEST_ALL; i++) + foreach(cell, destlist) { - if (pg_strcasecmp(dest, ConflictLogDestLabels[i]) == 0) - return (ConflictLogDest) i; + char *name; + + name = (strnodelist) ? strVal(lfirst(cell)) : (char *) lfirst(cell); + + if (pg_strcasecmp(name, "log") == 0) + logdest |= CONFLICT_LOG_DEST_LOG; + else if (pg_strcasecmp(name, "table") == 0) + logdest |= CONFLICT_LOG_DEST_TABLE; + else if (pg_strcasecmp(name, "all") == 0) + logdest |= CONFLICT_LOG_DEST_ALL; + else + ereport(ERROR, + errcode(ERRCODE_SYNTAX_ERROR), + errmsg("unrecognized value for subscription parameter \"%s\": \"%s\"", + "conflict_log_destination", name), + errhint("Valid values are \"log\", \"table\", and \"all\".")); } - /* Unrecognized string. */ - return CONFLICT_LOG_DEST_INVALID; + return logdest; } /* diff --git a/src/backend/replication/logical/conflict.c b/src/backend/replication/logical/conflict.c index 0b6e3f4a2c8..1c7ac2da6f5 100644 --- a/src/backend/replication/logical/conflict.c +++ b/src/backend/replication/logical/conflict.c @@ -159,8 +159,8 @@ ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel, /* Insert to table if destination is 'table' or 'all' */ if (conflictlogrel) { - Assert(dest == CONFLICT_LOG_DEST_TABLE || - dest == CONFLICT_LOG_DEST_ALL); + Assert(CONFLICT_LOG_DEST_TABLE_ENABLED(dest) || + CONFLICT_LOG_DEST_ALL_ENABLED(dest)); if (ValidateConflictLogTable(conflictlogrel)) { @@ -187,7 +187,8 @@ ReportApplyConflict(EState *estate, ResultRelInfo *relinfo, int elevel, pgstat_report_subscription_conflict(MySubscription->oid, type); /* Decide what detail to show in server logs. */ - if (dest == CONFLICT_LOG_DEST_LOG || dest == CONFLICT_LOG_DEST_ALL) + if (CONFLICT_LOG_DEST_LOG_ENABLED(dest) || + CONFLICT_LOG_DEST_ALL_ENABLED(dest)) { /* Standard reporting with full internal details. */ ereport(elevel, @@ -263,7 +264,7 @@ GetConflictLogTableInfo(ConflictLogDest *log_dest) * Convert the text log destination to the internal enum. MySubscription * already contains the data from pg_subscription. */ - *log_dest = GetLogDestination(MySubscription->logdestination); + *log_dest = GetLogDestination(MySubscription->logdestination, true); conflictlogrelid = MySubscription->conflictrelid; /* If destination is 'log' only, no table to open. */ diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c index d2477cfb5a1..85b2f3a9a47 100644 --- a/src/bin/pg_dump/pg_dump.c +++ b/src/bin/pg_dump/pg_dump.c @@ -5522,10 +5522,10 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo) DumpOptions *dopt = fout->dopt; PQExpBuffer delq; PQExpBuffer query; - PQExpBuffer publications; + PQExpBuffer namebuf; char *qsubname; - char **pubnames = NULL; - int npubnames = 0; + char **names = NULL; + int nnames = 0; int i; /* Do nothing if not dumping schema */ @@ -5545,19 +5545,22 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo) appendStringLiteralAH(query, subinfo->subconninfo, fout); /* Build list of quoted publications and append them to query. */ - if (!parsePGArray(subinfo->subpublications, &pubnames, &npubnames)) + if (!parsePGArray(subinfo->subpublications, &names, &nnames)) pg_fatal("could not parse %s array", "subpublications"); - publications = createPQExpBuffer(); - for (i = 0; i < npubnames; i++) + namebuf = createPQExpBuffer(); + for (i = 0; i < nnames; i++) { if (i > 0) - appendPQExpBufferStr(publications, ", "); + appendPQExpBufferStr(namebuf, ", "); - appendPQExpBufferStr(publications, fmtId(pubnames[i])); + appendPQExpBufferStr(namebuf, fmtId(names[i])); } - appendPQExpBuffer(query, " PUBLICATION %s WITH (connect = false, slot_name = ", publications->data); + appendPQExpBuffer(query, " PUBLICATION %s WITH (connect = false, slot_name = ", namebuf->data); + resetPQExpBuffer(namebuf); + free(names); + if (subinfo->subslotname) appendStringLiteralAH(query, subinfo->subslotname, fout); else @@ -5610,11 +5613,22 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo) tableInfo->dobj.namespace->dobj.name); } + /* Build list of quoted conflict log destinations and append them to query. */ + if (!parsePGArray(subinfo->sublogdestination, &names, &nnames)) + pg_fatal("could not parse %s array", "conflict_log_destination"); + + for (i = 0; i < nnames; i++) + { + if (i > 0) + appendPQExpBufferStr(namebuf, ", "); + + appendPQExpBuffer(namebuf, "%s", names[i]); + } + appendPQExpBuffer(query, - "\n\nALTER SUBSCRIPTION %s SET (conflict_log_destination = %s);\n", + "\n\nALTER SUBSCRIPTION %s SET (conflict_log_destination = '%s');\n", qsubname, - subinfo->sublogdestination); - + namebuf->data); if (subinfo->subconflictlogrelid) appendPQExpBufferStr(query, "\n\nSELECT pg_catalog.set_config('search_path', '', false);\n"); @@ -5675,8 +5689,8 @@ dumpSubscription(Archive *fout, const SubscriptionInfo *subinfo) NULL, subinfo->rolname, subinfo->dobj.catId, 0, subinfo->dobj.dumpId); - destroyPQExpBuffer(publications); - free(pubnames); + destroyPQExpBuffer(namebuf); + free(names); destroyPQExpBuffer(delq); destroyPQExpBuffer(query); diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl index 8ec7b0069dd..e0859dddf4f 100644 --- a/src/bin/pg_dump/t/002_pg_dump.pl +++ b/src/bin/pg_dump/t/002_pg_dump.pl @@ -3204,11 +3204,11 @@ my %tests = ( create_order => 50, create_sql => 'CREATE SUBSCRIPTION sub3 CONNECTION \'dbname=doesnotexist\' PUBLICATION pub1 - WITH (connect = false, origin = any, streaming = on, conflict_log_destination= table);', + WITH (connect = false, origin = any, streaming = on, conflict_log_destination= \'log,table\');', regexp => qr/^ \QCREATE SUBSCRIPTION sub3 CONNECTION 'dbname=doesnotexist' PUBLICATION pub1 WITH (connect = false, slot_name = 'sub3', streaming = on);\E\n\n\n \QSELECT pg_catalog.set_config('search_path', 'public', false);\E\n\n\n - \QALTER SUBSCRIPTION sub3 SET (conflict_log_destination = table);\E\n\n\n + \QALTER SUBSCRIPTION sub3 SET (conflict_log_destination = 'log, table');\E\n\n\n \QSELECT pg_catalog.set_config('search_path', '', false);\E /xm, like => { %full_runs, section_post_data => 1, }, diff --git a/src/include/catalog/pg_subscription.h b/src/include/catalog/pg_subscription.h index 46c446eaf8b..b3b2c29b96d 100644 --- a/src/include/catalog/pg_subscription.h +++ b/src/include/catalog/pg_subscription.h @@ -110,7 +110,7 @@ CATALOG(pg_subscription,6100,SubscriptionRelationId) BKI_SHARED_RELATION BKI_ROW * table - internal table only, * all - both log and table. */ - text sublogdestination; + text sublogdestination[1] BKI_FORCE_NULL; /* Only publish data originating from the specified origin */ text suborigin BKI_DEFAULT(LOGICALREP_ORIGIN_ANY); @@ -169,7 +169,7 @@ typedef struct Subscription List *publications; /* List of publication names to subscribe to */ char *origin; /* Only publish data originating from the * specified origin */ - char *logdestination; /* Conflict log destination */ + List *logdestination; /* Conflict log destination */ } Subscription; #ifdef EXPOSE_TO_CLIENT_CODE diff --git a/src/include/commands/subscriptioncmds.h b/src/include/commands/subscriptioncmds.h index 255e1e241b8..aa0bc503847 100644 --- a/src/include/commands/subscriptioncmds.h +++ b/src/include/commands/subscriptioncmds.h @@ -38,7 +38,7 @@ extern void CheckSubDeadTupleRetention(bool check_guc, bool sub_disabled, bool max_retention_set); extern void GetConflictLogTableName(char *dest, Oid subid); -extern ConflictLogDest GetLogDestination(const char *dest); +extern ConflictLogDest GetLogDestination(List *destlist, bool strnodelist); extern bool IsConflictLogTable(Oid relid); #endif /* SUBSCRIPTIONCMDS_H */ diff --git a/src/include/replication/conflict.h b/src/include/replication/conflict.h index 5f313b7a976..92b7e619eb8 100644 --- a/src/include/replication/conflict.h +++ b/src/include/replication/conflict.h @@ -89,19 +89,20 @@ typedef struct ConflictTupleInfo typedef enum ConflictLogDest { CONFLICT_LOG_DEST_INVALID = 0, - CONFLICT_LOG_DEST_LOG, /* "log" (default) */ - CONFLICT_LOG_DEST_TABLE, /* "table" */ - CONFLICT_LOG_DEST_ALL /* "all" */ + CONFLICT_LOG_DEST_LOG = 1 << 0, /* 0x00000001 */ + CONFLICT_LOG_DEST_TABLE = 1 << 1, /* 0x00000002 */ + CONFLICT_LOG_DEST_ALL = 1 << 2 /* 0x00000004 */ } ConflictLogDest; -/* - * Array mapping for converting internal enum to string. - */ -static const char *const ConflictLogDestLabels[] = { - [CONFLICT_LOG_DEST_LOG] = "log", - [CONFLICT_LOG_DEST_TABLE] = "table", - [CONFLICT_LOG_DEST_ALL] = "all" -}; +/* Conflict log destination flags */ +#define CONFLICT_LOG_DEST_LOG_ENABLED(dest) \ + ((dest) & CONFLICT_LOG_DEST_LOG) + +#define CONFLICT_LOG_DEST_TABLE_ENABLED(dest) \ + ((dest) & CONFLICT_LOG_DEST_TABLE) + +#define CONFLICT_LOG_DEST_ALL_ENABLED(dest) \ + ((dest) & CONFLICT_LOG_DEST_ALL) /* Structure to hold metadata for one column of the conflict log table */ typedef struct ConflictLogColumnDef diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out index 92423c83197..471821167a0 100644 --- a/src/test/regress/expected/subscription.out +++ b/src/test/regress/expected/subscription.out @@ -119,7 +119,7 @@ HINT: To initiate replication, you must manually create the replication slot, e List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | none | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub4 SET (origin = any); @@ -127,7 +127,7 @@ ALTER SUBSCRIPTION regress_testsub4 SET (origin = any); List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination ------------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub4 | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) DROP SUBSCRIPTION regress_testsub3; @@ -148,7 +148,7 @@ ERROR: invalid connection string syntax: missing "=" after "foobar" in connecti List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET PUBLICATION testpub2, testpub3 WITH (refresh = false); @@ -160,7 +160,7 @@ ALTER SUBSCRIPTION regress_testsub SET (run_as_owner = true); List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | f | 0 | f | off | dbname=regress_doesnotexist2 | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | f | t | f | f | 0 | f | off | dbname=regress_doesnotexist2 | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET (password_required = true); @@ -179,7 +179,7 @@ ALTER SUBSCRIPTION regress_testsub SKIP (lsn = '0/12345'); List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist2 | 0/00012345 | log + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist2 | 0/00012345 | {log} (1 row) -- ok - with lsn = NONE @@ -191,7 +191,7 @@ ERROR: invalid WAL location (LSN): 0/0 List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist2 | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist2 | 0/00000000 | {log} (1 row) BEGIN; @@ -226,7 +226,7 @@ HINT: Available values: local, remote_write, remote_apply, on, off. List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination ---------------------+---------------------------+---------+---------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+------------------------------+------------+-------------------------- - regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | local | dbname=regress_doesnotexist2 | 0/00000000 | log + regress_testsub_foo | regress_subscription_user | f | {testpub2,testpub3} | f | parallel | d | f | any | t | f | f | f | 0 | f | local | dbname=regress_doesnotexist2 | 0/00000000 | {log} (1 row) -- rename back to keep the rest simple @@ -258,7 +258,7 @@ HINT: To initiate replication, you must manually create the replication slot, e List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | t | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET (binary = false); @@ -267,7 +267,7 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) DROP SUBSCRIPTION regress_testsub; @@ -282,7 +282,7 @@ HINT: To initiate replication, you must manually create the replication slot, e List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | on | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel); @@ -290,7 +290,7 @@ ALTER SUBSCRIPTION regress_testsub SET (streaming = parallel); List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET (streaming = false); @@ -299,7 +299,7 @@ ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) -- fail - publication already exists @@ -317,7 +317,7 @@ ERROR: publication "testpub1" is already in subscription "regress_testsub" List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-----------------------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub,testpub1,testpub2} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) -- fail - publication used more than once @@ -335,7 +335,7 @@ ALTER SUBSCRIPTION regress_testsub DROP PUBLICATION testpub1, testpub2 WITH (ref List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | off | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) DROP SUBSCRIPTION regress_testsub; @@ -374,7 +374,7 @@ HINT: To initiate replication, you must manually create the replication slot, e List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) -- we can alter streaming when two_phase enabled @@ -383,7 +383,7 @@ ALTER SUBSCRIPTION regress_testsub SET (streaming = true); List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -396,7 +396,7 @@ HINT: To initiate replication, you must manually create the replication slot, e List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | on | p | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -412,7 +412,7 @@ HINT: To initiate replication, you must manually create the replication slot, e List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true); @@ -420,7 +420,7 @@ ALTER SUBSCRIPTION regress_testsub SET (disable_on_error = true); List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | t | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -436,7 +436,7 @@ HINT: To initiate replication, you must manually create the replication slot, e List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -453,7 +453,7 @@ HINT: To initiate replication, you must manually create the replication slot, e List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 1000 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 1000 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) -- ok @@ -462,7 +462,7 @@ ALTER SUBSCRIPTION regress_testsub SET (max_retention_duration = 0); List of subscriptions Name | Owner | Enabled | Publication | Binary | Streaming | Two-phase commit | Disable on error | Origin | Password required | Run as owner? | Failover | Retain dead tuples | Max retention duration | Retention active | Synchronous commit | Conninfo | Skip LSN | Conflict log destination -----------------+---------------------------+---------+-------------+--------+-----------+------------------+------------------+--------+-------------------+---------------+----------+--------------------+------------------------+------------------+--------------------+-----------------------------+------------+-------------------------- - regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | log + regress_testsub | regress_subscription_user | f | {testpub} | f | parallel | d | f | any | t | f | f | f | 0 | f | off | dbname=regress_doesnotexist | 0/00000000 | {log} (1 row) ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE); @@ -523,7 +523,7 @@ DROP SUBSCRIPTION regress_testsub; SET SESSION AUTHORIZATION 'regress_subscription_user'; -- fail - unrecognized format value 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" +ERROR: unrecognized value for subscription parameter "conflict_log_destination": "invalid" HINT: Valid values are "log", "table", and "all". -- verify sublogdestination is 'log' and relid is 0 (InvalidOid) for default case CREATE SUBSCRIPTION regress_conflict_log_default CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false); @@ -533,7 +533,7 @@ SELECT subname, sublogdestination, subconflictlogrelid FROM pg_subscription WHERE subname = 'regress_conflict_log_default'; subname | sublogdestination | subconflictlogrelid ------------------------------+-------------------+--------------------- - regress_conflict_log_default | log | 0 + regress_conflict_log_default | {log} | 0 (1 row) -- verify empty string defaults to 'log' @@ -544,11 +544,11 @@ SELECT subname, sublogdestination, subconflictlogrelid FROM pg_subscription WHERE subname = 'regress_conflict_empty_str'; subname | sublogdestination | subconflictlogrelid ----------------------------+-------------------+--------------------- - regress_conflict_empty_str | log | 0 + regress_conflict_empty_str | {log} | 0 (1 row) -- this should generate an internal table named conflict_log_table_$subid$ -CREATE SUBSCRIPTION regress_conflict_test1 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'table'); +CREATE SUBSCRIPTION regress_conflict_test1 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'log, 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 @@ -556,7 +556,7 @@ SELECT subname, sublogdestination, subconflictlogrelid > 0 AS has_relid FROM pg_subscription WHERE subname = 'regress_conflict_test1'; subname | sublogdestination | has_relid ------------------------+-------------------+----------- - regress_conflict_test1 | table | t + regress_conflict_test1 | {log,table} | t (1 row) -- verify the physical table exists and its OID matches subconflictlogrelid @@ -581,17 +581,27 @@ WHERE s.subname = 'regress_conflict_test1' AND a.attnum > 0; (1 row) -- ALTER: State transitions --- transition from 'log' to 'all' +-- transition from 'log' to 'log, table' CREATE SUBSCRIPTION regress_conflict_test2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'log'); 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. +ALTER SUBSCRIPTION regress_conflict_test2 SET (conflict_log_destination = 'log, table'); +-- verify metadata after ALTER (destination should be 'log, table') +SELECT subname, sublogdestination, subconflictlogrelid > 0 AS has_relid +FROM pg_subscription WHERE subname = 'regress_conflict_test2'; + subname | sublogdestination | has_relid +------------------------+-------------------+----------- + regress_conflict_test2 | {log,table} | t +(1 row) + +-- transition from 'log, table' to 'all' ALTER SUBSCRIPTION regress_conflict_test2 SET (conflict_log_destination = 'all'); -- verify metadata after ALTER (destination should be 'all') SELECT subname, sublogdestination, subconflictlogrelid > 0 AS has_relid FROM pg_subscription WHERE subname = 'regress_conflict_test2'; subname | sublogdestination | has_relid ------------------------+-------------------+----------- - regress_conflict_test2 | all | t + regress_conflict_test2 | {all} | t (1 row) -- transition from 'all' to 'table' (should NOT drop the table, only change destination string) @@ -601,7 +611,7 @@ SELECT sublogdestination, subconflictlogrelid = :old_relid AS relid_unchanged FROM pg_subscription WHERE subname = 'regress_conflict_test2'; sublogdestination | relid_unchanged -------------------+----------------- - table | t + {table} | t (1 row) -- transition from 'table' to 'log' (should drop the table and clear relid) @@ -610,7 +620,7 @@ SELECT sublogdestination, subconflictlogrelid FROM pg_subscription WHERE subname = 'regress_conflict_test2'; sublogdestination | subconflictlogrelid -------------------+--------------------- - log | 0 + {log} | 0 (1 row) -- verify the physical table is gone diff --git a/src/test/regress/sql/subscription.sql b/src/test/regress/sql/subscription.sql index b4b98c9a178..b437c383a4e 100644 --- a/src/test/regress/sql/subscription.sql +++ b/src/test/regress/sql/subscription.sql @@ -385,7 +385,7 @@ SELECT subname, sublogdestination, subconflictlogrelid FROM pg_subscription WHERE subname = 'regress_conflict_empty_str'; -- this should generate an internal table named conflict_log_table_$subid$ -CREATE SUBSCRIPTION regress_conflict_test1 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'table'); +CREATE SUBSCRIPTION regress_conflict_test1 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'log, table'); -- check metadata in pg_subscription: destination should be 'table' and relid valid SELECT subname, sublogdestination, subconflictlogrelid > 0 AS has_relid @@ -405,8 +405,15 @@ JOIN pg_subscription s ON c.relname = 'conflict_log_table_' || s.oid WHERE s.subname = 'regress_conflict_test1' AND a.attnum > 0; -- ALTER: State transitions --- transition from 'log' to 'all' +-- transition from 'log' to 'log, table' CREATE SUBSCRIPTION regress_conflict_test2 CONNECTION 'dbname=regress_doesnotexist' PUBLICATION testpub WITH (connect = false, conflict_log_destination = 'log'); +ALTER SUBSCRIPTION regress_conflict_test2 SET (conflict_log_destination = 'log, table'); + +-- verify metadata after ALTER (destination should be 'log, table') +SELECT subname, sublogdestination, subconflictlogrelid > 0 AS has_relid +FROM pg_subscription WHERE subname = 'regress_conflict_test2'; + +-- transition from 'log, table' to 'all' ALTER SUBSCRIPTION regress_conflict_test2 SET (conflict_log_destination = 'all'); -- verify metadata after ALTER (destination should be 'all') diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 21826be5bd7..65ba5074331 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -501,8 +501,9 @@ ConditionalStack ConfigData ConfigVariable ConflictLogColumnDef +ConflictLogDest ConflictTupleInfo -ConflictType +ConflictTyp ConnCacheEntry ConnCacheKey ConnParams -- 2.43.0