diff --git a/doc/src/sgml/logical-replication.sgml b/doc/src/sgml/logical-replication.sgml index 92ec175af1..fa8ae536d9 100644 --- a/doc/src/sgml/logical-replication.sgml +++ b/doc/src/sgml/logical-replication.sgml @@ -110,11 +110,29 @@ Publications can choose to limit the changes they produce to any combination of INSERT, UPDATE, and DELETE, similar to how triggers are fired by - particular event types. If a table without a REPLICA - IDENTITY is added to a publication that - replicates UPDATE or DELETE - operations then subsequent UPDATE - or DELETE operations will fail on the publisher. + particular event types. By default, all operation types are replicated. + + + + A published table must have a replica identity configured in + order to be able to replicate UPDATE + and DELETE operations, so that appropriate rows to + update or delete can be identified on the subscriber side. By default, + this is the primary key, if there is one. Another unique index (with + certain additional requirements) can also be set to be the replica + identity. If the table does not have any suitable key, then it can be set + to replica identity full, which means the entire row becomes + the key. This, however, is very inefficient and should only be used as a + fallback if no other solution is possible. If a replica identity other + than full is set on the publisher side, a replica identity + comprising the same or fewer columns must also be set on the subscriber + side. See for details on + how to set the replica identity. If a table without a replica identity is + added to a publication that replicates UPDATE + or DELETE operations then + subsequent UPDATE or DELETE + operations will cause an error on the publisher. INSERT + operations can proceed regardless of any replica identity. diff --git a/src/backend/executor/execReplication.c b/src/backend/executor/execReplication.c index 6dae79a8f0..59f14e997f 100644 --- a/src/backend/executor/execReplication.c +++ b/src/backend/executor/execReplication.c @@ -24,12 +24,14 @@ #include "parser/parsetree.h" #include "storage/bufmgr.h" #include "storage/lmgr.h" +#include "utils/builtins.h" #include "utils/datum.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" #include "utils/snapmgr.h" #include "utils/syscache.h" +#include "utils/typcache.h" #include "utils/tqual.h" @@ -224,13 +226,15 @@ tuple_equals_slot(TupleDesc desc, HeapTuple tup, TupleTableSlot *slot) Datum values[MaxTupleAttributeNumber]; bool isnull[MaxTupleAttributeNumber]; int attrnum; - Form_pg_attribute att; heap_deform_tuple(tup, desc, values, isnull); /* Check equality of the attributes. */ for (attrnum = 0; attrnum < desc->natts; attrnum++) { + Form_pg_attribute att; + TypeCacheEntry *typentry; + /* * If one value is NULL and other is not, then they are certainly not * equal @@ -245,8 +249,17 @@ tuple_equals_slot(TupleDesc desc, HeapTuple tup, TupleTableSlot *slot) continue; att = desc->attrs[attrnum]; - if (!datumIsEqual(values[attrnum], slot->tts_values[attrnum], - att->attbyval, att->attlen)) + + typentry = lookup_type_cache(att->atttypid, TYPECACHE_EQ_OPR_FINFO); + if (!OidIsValid(typentry->eq_opr_finfo.fn_oid)) + ereport(ERROR, + (errcode(ERRCODE_UNDEFINED_FUNCTION), + errmsg("could not identify an equality operator for type %s", + format_type_be(att->atttypid)))); + + if (!DatumGetBool(FunctionCall2(&typentry->eq_opr_finfo, + values[attrnum], + slot->tts_values[attrnum]))) return false; } diff --git a/src/test/subscription/t/001_rep_changes.pl b/src/test/subscription/t/001_rep_changes.pl index f9cf5e4392..a63c679848 100644 --- a/src/test/subscription/t/001_rep_changes.pl +++ b/src/test/subscription/t/001_rep_changes.pl @@ -3,7 +3,7 @@ use strict; use warnings; use PostgresNode; use TestLib; -use Test::More tests => 15; +use Test::More tests => 16; # Initialize publisher node my $node_publisher = get_new_node('publisher'); @@ -22,6 +22,10 @@ $node_publisher->safe_psql('postgres', "CREATE TABLE tab_ins AS SELECT generate_series(1,1002) AS a"); $node_publisher->safe_psql('postgres', "CREATE TABLE tab_full AS SELECT generate_series(1,10) AS a"); +$node_publisher->safe_psql('postgres', + "CREATE TABLE tab_full2 (x text)"); +$node_publisher->safe_psql('postgres', + "INSERT INTO tab_full2 VALUES ('a'), ('b'), ('b')"); $node_publisher->safe_psql('postgres', "CREATE TABLE tab_rep (a int primary key)"); $node_publisher->safe_psql('postgres', @@ -33,6 +37,7 @@ $node_publisher->safe_psql('postgres', $node_subscriber->safe_psql('postgres', "CREATE TABLE tab_notrep (a int)"); $node_subscriber->safe_psql('postgres', "CREATE TABLE tab_ins (a int)"); $node_subscriber->safe_psql('postgres', "CREATE TABLE tab_full (a int)"); +$node_subscriber->safe_psql('postgres', "CREATE TABLE tab_full2 (x text)"); $node_subscriber->safe_psql('postgres', "CREATE TABLE tab_rep (a int primary key)"); # different column count and order than on publisher @@ -45,7 +50,7 @@ $node_publisher->safe_psql('postgres', "CREATE PUBLICATION tap_pub"); $node_publisher->safe_psql('postgres', "CREATE PUBLICATION tap_pub_ins_only WITH (publish = insert)"); $node_publisher->safe_psql('postgres', - "ALTER PUBLICATION tap_pub ADD TABLE tab_rep, tab_full, tab_mixed"); + "ALTER PUBLICATION tap_pub ADD TABLE tab_rep, tab_full, tab_full2, tab_mixed"); $node_publisher->safe_psql('postgres', "ALTER PUBLICATION tap_pub_ins_only ADD TABLE tab_ins"); @@ -108,13 +113,18 @@ $node_publisher->safe_psql('postgres', "ALTER TABLE tab_full REPLICA IDENTITY FULL"); $node_subscriber->safe_psql('postgres', "ALTER TABLE tab_full REPLICA IDENTITY FULL"); +$node_publisher->safe_psql('postgres', + "ALTER TABLE tab_full2 REPLICA IDENTITY FULL"); +$node_subscriber->safe_psql('postgres', + "ALTER TABLE tab_full2 REPLICA IDENTITY FULL"); $node_publisher->safe_psql('postgres', "ALTER TABLE tab_ins REPLICA IDENTITY FULL"); $node_subscriber->safe_psql('postgres', "ALTER TABLE tab_ins REPLICA IDENTITY FULL"); -# and do the update +# and do the updates $node_publisher->safe_psql('postgres', "UPDATE tab_full SET a = a * a"); +$node_publisher->safe_psql('postgres', "UPDATE tab_full2 SET x = 'bb' WHERE x = 'b'"); # Wait for subscription to catch up $node_publisher->poll_query_until('postgres', $caughtup_query) @@ -125,6 +135,13 @@ $result = $node_subscriber->safe_psql('postgres', is($result, qq(20|1|100), 'update works with REPLICA IDENTITY FULL and duplicate tuples'); +$result = $node_subscriber->safe_psql('postgres', + "SELECT x FROM tab_full2 ORDER BY 1"); +is($result, qq(a +bb +bb), + 'update works with REPLICA IDENTITY FULL and text datums'); + # check that change of connection string and/or publication list causes # restart of subscription workers. Not all of these are registered as tests # as we need to poll for a change but the test suite will fail none the less