Here is an updated patch that addresses some of your concerns.

I've split it up into the decoding support and the pgoutput replication
support.

The problem I see now is that when we WAL-log a truncate, we include all
the relids in one record.  That seems useful.  But during decoding we
split this into one change per relid.  So at the receiving end, truncate
can only process one relation at a time, which will fail if there are
foreign keys involved.  The solution that had been proposed here was to
ignore foreign keys on the subscriber, which is clearly problematic.

I wonder why this approach was chosen.  If we go through the trouble of
WAL-logging all the relations together, why not present them to the
output plugin as one so that they can be applied together?  This will
clearly make questions of filtering and replication set membership and
so on more difficult, but that's just the nature of the thing.  If you
are connecting tables by foreign keys and only replicate some of them,
then that's going to have confusing effects no matter what you do.

(That's perhaps another reason why having the option of replicating
truncate separately from delete could be useful.)

I'm going to try to hack up an alternative approach and see how it works
out.


On 4/1/18 16:01, Andres Freund wrote:
> On 2018-01-25 14:21:15 +0100, Marco Nenciarini wrote:
>> +    if (SessionReplicationRole != SESSION_REPLICATION_ROLE_REPLICA)
>> +    {
>> + 
>> +            /*
>> +             * Check foreign key references.  In CASCADE mode, this should 
>> be
>> +             * unnecessary since we just pulled in all the references; but 
>> as a
>> +             * cross-check, do it anyway if in an Assert-enabled build.
>> +             */
>>   #ifdef USE_ASSERT_CHECKING
>>              heap_truncate_check_FKs(rels, false);
>> + #else
>> +            if (stmt->behavior == DROP_RESTRICT)
>> +                    heap_truncate_check_FKs(rels, false);
>>   #endif
>> +    }
> 
> That *can't* be right.

This is actually existing code that just looks funny in the diff after
being indented.

But I'd rather skip this patch altogether and find a different solution;
see above.

> I know this has been discussed in the thread already, but it really
> strikes me as wrong to basically do some mini DDL replication feature
> via per-command WAL records.

I think TRUNCATE is special in some ways and it's reasonable to treat it
specially.  Real DDL is being worked on in the 2PC decoding thread.
What we are discussing here isn't going to be applicable there and vice
versa, I think.  In fact, one of the reasons for this effort is that in
BDR TRUNCATE is currently handled like a DDL command, which doesn't work
well.

>> +          <para>
>> +            <command>TRUNCATE</command> is treated as a form of
>> +            <command>DELETE</command> for the purpose of deciding whether
>> +            to publish, or not.
>> +          </para>
>>           </listitem>
>>          </varlistentry>
>>         </variablelist>
> 
> Why is this a good idea?

I have changed this by making this a separate property.

> Hm, it seems logicaldecoding.sgml hasn't been updated?

I re-checked but didn't find anything suitable to update.

>> + void
>> + ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
>> +                                                    DropBehavior behavior, 
>> bool restart_seqs)
>> + {
>> +    List       *rels = list_copy(explicit_rels);
> 
> Why is this copied?

Because it is overwritten later.  I have moved it down a bit to make
that a bit clearer.

>> +     * Write a WAL record to allow this set of actions to be logically 
>> decoded.
>> +     * We could optimize this away when !RelationIsLogicallyLogged(rel)
>> +     * but that doesn't save much space or time.
> 
> What you're saying isn't that you're not logging anything, but that
> you're allocating the header regardless? Because this certainly sounds
> like you unconditionally log a WAL record.

> I'm confused. Why do we need the resizing here, when we know the max
> upfront?

I have rewritten this a bit and removed the logging of the sequence
relids, which isn't used anywhere after, to make the code a bit simpler.

-- 
Peter Eisentraut              http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
From 72577e567725526543e5493e235c1059610f3467 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <pete...@gmx.net>
Date: Tue, 3 Apr 2018 22:12:00 -0400
Subject: [PATCH v18 1/2] Logical decoding of TRUNCATE

---
 contrib/test_decoding/expected/ddl.out        | 10 ++-
 contrib/test_decoding/sql/ddl.sql             |  3 +
 contrib/test_decoding/test_decoding.c         | 14 +++
 src/backend/access/heap/heapam.c              |  7 ++
 src/backend/access/rmgrdesc/heapdesc.c        | 14 +++
 src/backend/commands/tablecmds.c              | 87 +++++++++++++++++--
 src/backend/replication/logical/decode.c      | 46 ++++++++++
 .../replication/logical/reorderbuffer.c       | 10 +++
 src/include/access/heapam_xlog.h              | 23 ++++-
 src/include/commands/tablecmds.h              |  2 +
 src/include/replication/reorderbuffer.h       | 16 +++-
 11 files changed, 219 insertions(+), 13 deletions(-)

diff --git a/contrib/test_decoding/expected/ddl.out 
b/contrib/test_decoding/expected/ddl.out
index b7c76469fc..89c9dc5466 100644
--- a/contrib/test_decoding/expected/ddl.out
+++ b/contrib/test_decoding/expected/ddl.out
@@ -547,6 +547,8 @@ UPDATE table_with_unique_not_null SET data = 3 WHERE data = 
2;
 UPDATE table_with_unique_not_null SET id = -id;
 UPDATE table_with_unique_not_null SET id = -id;
 DELETE FROM table_with_unique_not_null WHERE data = 3;
+TRUNCATE table_with_unique_not_null;
+TRUNCATE table_with_unique_not_null, table_with_unique_not_null RESTART 
IDENTITY CASCADE;
 -- check toast support
 BEGIN;
 CREATE SEQUENCE toasttable_rand_seq START 79 INCREMENT 1499; -- portable 
"random"
@@ -664,6 +666,12 @@ SELECT data FROM 
pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc
  table public.table_with_unique_not_null: DELETE: id[integer]:4
  COMMIT
  BEGIN
+ table public.table_with_unique_not_null: TRUNCATE: (no-flags)
+ COMMIT
+ BEGIN
+ table public.table_with_unique_not_null: TRUNCATE: restart_seqs cascade
+ COMMIT
+ BEGIN
  table public.toasttable: INSERT: id[integer]:1 
toasted_col1[text]:'12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000'
 rand1[double precision]:79 toasted_col2[text]:null rand2[double precision]:1578
  COMMIT
  BEGIN
@@ -672,7 +680,7 @@ SELECT data FROM 
pg_logical_slot_get_changes('regression_slot', NULL, NULL, 'inc
  BEGIN
  table public.toasttable: UPDATE: id[integer]:1 
toasted_col1[text]:'12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000'
 rand1[double precision]:79 toasted_col2[text]:null rand2[double precision]:1578
  COMMIT
-(103 rows)
+(109 rows)
 
 INSERT INTO toasttable(toasted_col1) SELECT string_agg(g.i::text, '') FROM 
generate_series(1, 2000) g(i);
 -- update of second column, first column unchanged
diff --git a/contrib/test_decoding/sql/ddl.sql 
b/contrib/test_decoding/sql/ddl.sql
index c4b10a4cf9..2724d36b79 100644
--- a/contrib/test_decoding/sql/ddl.sql
+++ b/contrib/test_decoding/sql/ddl.sql
@@ -332,6 +332,9 @@ CREATE TABLE table_with_unique_not_null(id serial unique, 
data int);
 UPDATE table_with_unique_not_null SET id = -id;
 DELETE FROM table_with_unique_not_null WHERE data = 3;
 
+TRUNCATE table_with_unique_not_null;
+TRUNCATE table_with_unique_not_null, table_with_unique_not_null RESTART 
IDENTITY CASCADE;
+
 -- check toast support
 BEGIN;
 CREATE SEQUENCE toasttable_rand_seq START 79 INCREMENT 1499; -- portable 
"random"
diff --git a/contrib/test_decoding/test_decoding.c 
b/contrib/test_decoding/test_decoding.c
index a94aeeae29..8c18821886 100644
--- a/contrib/test_decoding/test_decoding.c
+++ b/contrib/test_decoding/test_decoding.c
@@ -470,6 +470,20 @@ pg_decode_change(LogicalDecodingContext *ctx, 
ReorderBufferTXN *txn,
                                                                        
&change->data.tp.oldtuple->tuple,
                                                                        true);
                        break;
+               case REORDER_BUFFER_CHANGE_TRUNCATE:
+                       appendStringInfoString(ctx->out, " TRUNCATE:");
+
+                       if (change->data.truncate_msg.restart_seqs
+                               || change->data.truncate_msg.cascade)
+                       {
+                               if (change->data.truncate_msg.restart_seqs)
+                                       appendStringInfo(ctx->out, " 
restart_seqs");
+                               if (change->data.truncate_msg.cascade)
+                                       appendStringInfo(ctx->out, " cascade");
+                       }
+                       else
+                               appendStringInfoString(ctx->out, " (no-flags)");
+                       break;
                default:
                        Assert(false);
        }
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index d7279248e7..8fc36fa83d 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -9256,6 +9256,13 @@ heap_redo(XLogReaderState *record)
                case XLOG_HEAP_UPDATE:
                        heap_xlog_update(record, false);
                        break;
+               case XLOG_HEAP_TRUNCATE:
+                       /*
+                        * TRUNCATE is a no-op because the actions are already 
logged as
+                        * SMGR WAL records.  TRUNCATE WAL record only exists 
for logical
+                        * decoding.
+                        */
+                       break;
                case XLOG_HEAP_HOT_UPDATE:
                        heap_xlog_update(record, true);
                        break;
diff --git a/src/backend/access/rmgrdesc/heapdesc.c 
b/src/backend/access/rmgrdesc/heapdesc.c
index b00c071cb6..349feb6510 100644
--- a/src/backend/access/rmgrdesc/heapdesc.c
+++ b/src/backend/access/rmgrdesc/heapdesc.c
@@ -75,6 +75,17 @@ heap_desc(StringInfo buf, XLogReaderState *record)
                                                 xlrec->new_offnum,
                                                 xlrec->new_xmax);
        }
+       else if (info == XLOG_HEAP_TRUNCATE)
+       {
+               xl_heap_truncate *xlrec = (xl_heap_truncate *) rec;
+
+               if (xlrec->flags & XLH_TRUNCATE_CASCADE)
+                       appendStringInfo(buf, "cascade ");
+               if (xlrec->flags & XLH_TRUNCATE_RESTART_SEQS)
+                       appendStringInfo(buf, "restart_seqs ");
+               appendStringInfo(buf, "nrelids %u", xlrec->nrelids);
+               /* skip the list of relids */
+       }
        else if (info == XLOG_HEAP_CONFIRM)
        {
                xl_heap_confirm *xlrec = (xl_heap_confirm *) rec;
@@ -186,6 +197,9 @@ heap_identify(uint8 info)
                case XLOG_HEAP_HOT_UPDATE | XLOG_HEAP_INIT_PAGE:
                        id = "HOT_UPDATE+INIT";
                        break;
+               case XLOG_HEAP_TRUNCATE:
+                       id = "TRUNCATE";
+                       break;
                case XLOG_HEAP_CONFIRM:
                        id = "HEAP_CONFIRM";
                        break;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index c8da82217d..840373f641 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -16,6 +16,7 @@
 
 #include "access/genam.h"
 #include "access/heapam.h"
+#include "access/heapam_xlog.h"
 #include "access/multixact.h"
 #include "access/reloptions.h"
 #include "access/relscan.h"
@@ -1316,11 +1317,7 @@ ExecuteTruncate(TruncateStmt *stmt)
 {
        List       *rels = NIL;
        List       *relids = NIL;
-       List       *seq_relids = NIL;
-       EState     *estate;
-       ResultRelInfo *resultRelInfos;
-       ResultRelInfo *resultRelInfo;
-       SubTransactionId mySubid;
+       List       *relids_logged = NIL;
        ListCell   *cell;
 
        /*
@@ -1344,6 +1341,9 @@ ExecuteTruncate(TruncateStmt *stmt)
                truncate_check_rel(rel);
                rels = lappend(rels, rel);
                relids = lappend_oid(relids, myrelid);
+               /* Log this relation only if needed for logical decoding */
+               if (RelationIsLogicallyLogged(rel))
+                       relids_logged = lappend_oid(relids_logged, myrelid);
 
                if (recurse)
                {
@@ -1364,6 +1364,9 @@ ExecuteTruncate(TruncateStmt *stmt)
                                truncate_check_rel(rel);
                                rels = lappend(rels, rel);
                                relids = lappend_oid(relids, childrelid);
+                               /* Log this relation only if needed for logical 
decoding */
+                               if (RelationIsLogicallyLogged(rel))
+                                       relids_logged = 
lappend_oid(relids_logged, childrelid);
                        }
                }
                else if (rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
@@ -1373,7 +1376,34 @@ ExecuteTruncate(TruncateStmt *stmt)
                                         errhint("Do not specify the ONLY 
keyword, or use TRUNCATE ONLY on the partitions directly.")));
        }
 
+       ExecuteTruncateGuts(rels, relids, relids_logged,
+                                               stmt->behavior, 
stmt->restart_seqs);
+
+       /* And close the rels */
+       foreach(cell, rels)
+       {
+               Relation        rel = (Relation) lfirst(cell);
+
+               heap_close(rel, NoLock);
+       }
+}
+
+void
+ExecuteTruncateGuts(List *explicit_rels, List *relids, List *relids_logged,
+                                                       DropBehavior behavior, 
bool restart_seqs)
+{
+       List       *rels;
+       List       *seq_relids = NIL;
+       EState     *estate;
+       ResultRelInfo *resultRelInfos;
+       ResultRelInfo *resultRelInfo;
+       SubTransactionId mySubid;
+       ListCell   *cell;
+       Oid                *logrelids;
+
        /*
+        * Open, exclusive-lock, and check all the explicitly-specified 
relations
+        *
         * In CASCADE mode, suck in all referencing relations as well.  This
         * requires multiple iterations to find indirectly-dependent relations. 
At
         * each phase, we need to exclusive-lock new rels before looking for 
their
@@ -1381,7 +1411,8 @@ ExecuteTruncate(TruncateStmt *stmt)
         * soon as we open it, to avoid a faux pas such as holding lock for a 
long
         * time on a rel we have no permissions for.
         */
-       if (stmt->behavior == DROP_CASCADE)
+       rels = list_copy(explicit_rels);
+       if (behavior == DROP_CASCADE)
        {
                for (;;)
                {
@@ -1403,6 +1434,9 @@ ExecuteTruncate(TruncateStmt *stmt)
                                truncate_check_rel(rel);
                                rels = lappend(rels, rel);
                                relids = lappend_oid(relids, relid);
+                               /* Log this relation only if needed for logical 
decoding */
+                               if (RelationIsLogicallyLogged(rel))
+                                       relids_logged = 
lappend_oid(relids_logged, relid);
                        }
                }
        }
@@ -1415,7 +1449,7 @@ ExecuteTruncate(TruncateStmt *stmt)
 #ifdef USE_ASSERT_CHECKING
        heap_truncate_check_FKs(rels, false);
 #else
-       if (stmt->behavior == DROP_RESTRICT)
+       if (behavior == DROP_RESTRICT)
                heap_truncate_check_FKs(rels, false);
 #endif
 
@@ -1425,7 +1459,7 @@ ExecuteTruncate(TruncateStmt *stmt)
         * We want to do this early since it's pointless to do all the 
truncation
         * work only to fail on sequence permissions.
         */
-       if (stmt->restart_seqs)
+       if (restart_seqs)
        {
                foreach(cell, rels)
                {
@@ -1580,6 +1614,38 @@ ExecuteTruncate(TruncateStmt *stmt)
                ResetSequence(seq_relid);
        }
 
+       /*
+        * Write a WAL record to allow this set of actions to be logically 
decoded.
+        *
+        * Assemble an array of relids so we can write a single WAL record for 
the
+        * whole action.
+        */
+       if (list_length(relids_logged) > 0)
+       {
+               xl_heap_truncate xlrec;
+               int                     i = 0;
+
+               logrelids = palloc(list_length(relids_logged) * sizeof(Oid));
+               foreach (cell, relids_logged)
+                       logrelids[i++] = lfirst_oid(cell);
+
+               xlrec.dbId = MyDatabaseId;
+               xlrec.nrelids = list_length(relids_logged);
+               xlrec.flags = 0;
+               if (behavior == DROP_CASCADE)
+                       xlrec.flags |= XLH_TRUNCATE_CASCADE;
+               if (restart_seqs)
+                       xlrec.flags |= XLH_TRUNCATE_RESTART_SEQS;
+
+               XLogBeginInsert();
+               XLogRegisterData((char *) &xlrec, SizeOfHeapTruncate);
+               XLogRegisterData((char *) logrelids, list_length(relids_logged) 
* sizeof(Oid));
+
+               XLogSetRecordFlags(XLOG_INCLUDE_ORIGIN);
+
+               (void) XLogInsert(RM_HEAP_ID, XLOG_HEAP_TRUNCATE);
+       }
+
        /*
         * Process all AFTER STATEMENT TRUNCATE triggers.
         */
@@ -1597,7 +1663,10 @@ ExecuteTruncate(TruncateStmt *stmt)
        /* We can clean up the EState now */
        FreeExecutorState(estate);
 
-       /* And close the rels (can't do this while EState still holds refs) */
+       /* And close the eventual rels opened by CASCADE
+        * (can't do this while EState still holds refs)
+        */
+       rels = list_difference_ptr(rels, explicit_rels);
        foreach(cell, rels)
        {
                Relation        rel = (Relation) lfirst(cell);
diff --git a/src/backend/replication/logical/decode.c 
b/src/backend/replication/logical/decode.c
index 6eb0d5527e..fb02db6857 100644
--- a/src/backend/replication/logical/decode.c
+++ b/src/backend/replication/logical/decode.c
@@ -65,6 +65,7 @@ static void DecodeLogicalMsgOp(LogicalDecodingContext *ctx, 
XLogRecordBuffer *bu
 static void DecodeInsert(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
 static void DecodeUpdate(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
 static void DecodeDelete(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
+static void DecodeTruncate(LogicalDecodingContext *ctx, XLogRecordBuffer *buf);
 static void DecodeMultiInsert(LogicalDecodingContext *ctx, XLogRecordBuffer 
*buf);
 static void DecodeSpecConfirm(LogicalDecodingContext *ctx, XLogRecordBuffer 
*buf);
 
@@ -449,6 +450,11 @@ DecodeHeapOp(LogicalDecodingContext *ctx, XLogRecordBuffer 
*buf)
                                DecodeDelete(ctx, buf);
                        break;
 
+               case XLOG_HEAP_TRUNCATE:
+                       if (SnapBuildProcessChange(builder, xid, buf->origptr))
+                               DecodeTruncate(ctx, buf);
+                       break;
+
                case XLOG_HEAP_INPLACE:
 
                        /*
@@ -825,6 +831,46 @@ DecodeDelete(LogicalDecodingContext *ctx, XLogRecordBuffer 
*buf)
        ReorderBufferQueueChange(ctx->reorder, XLogRecGetXid(r), buf->origptr, 
change);
 }
 
+/*
+ * Parse XLOG_HEAP_TRUNCATE from wal
+ */
+static void
+DecodeTruncate(LogicalDecodingContext *ctx, XLogRecordBuffer *buf)
+{
+       XLogReaderState *r = buf->record;
+       xl_heap_truncate *xlrec;
+       ReorderBufferChange *change;
+       int     i;
+
+       xlrec = (xl_heap_truncate *) XLogRecGetData(r);
+
+       /* only interested in our database */
+       if (xlrec->dbId != ctx->slot->data.database)
+               return;
+
+       /* output plugin doesn't look for this origin, no need to queue */
+       if (FilterByOrigin(ctx, XLogRecGetOrigin(r)))
+               return;
+
+       change = ReorderBufferGetChange(ctx->reorder);
+       change->action = REORDER_BUFFER_CHANGE_TRUNCATE;
+       change->origin_id = XLogRecGetOrigin(r);
+       if (xlrec->flags & XLH_TRUNCATE_CASCADE)
+               change->data.truncate_msg.cascade = true;
+       if (xlrec->flags & XLH_TRUNCATE_RESTART_SEQS)
+               change->data.truncate_msg.restart_seqs = true;
+
+       /*
+        * Queue up one change per relation
+        */
+       for (i = 0; i < xlrec->nrelids; i++)
+       {
+               change->data.truncate_msg.relid = xlrec->relids[i];
+               ReorderBufferQueueChange(ctx->reorder, XLogRecGetXid(r),
+                                                                buf->origptr, 
change);
+       }
+}
+
 /*
  * Decode XLOG_HEAP2_MULTI_INSERT_insert record into multiple tuplebufs.
  *
diff --git a/src/backend/replication/logical/reorderbuffer.c 
b/src/backend/replication/logical/reorderbuffer.c
index b4016ed52b..0869e30bae 100644
--- a/src/backend/replication/logical/reorderbuffer.c
+++ b/src/backend/replication/logical/reorderbuffer.c
@@ -415,6 +415,7 @@ ReorderBufferReturnChange(ReorderBuffer *rb, 
ReorderBufferChange *change)
                case REORDER_BUFFER_CHANGE_INTERNAL_SPEC_CONFIRM:
                case REORDER_BUFFER_CHANGE_INTERNAL_COMMAND_ID:
                case REORDER_BUFFER_CHANGE_INTERNAL_TUPLECID:
+               case REORDER_BUFFER_CHANGE_TRUNCATE:
                        break;
        }
 
@@ -1355,6 +1356,13 @@ ReorderBufferCommit(ReorderBuffer *rb, TransactionId xid,
 
                        switch (change->action)
                        {
+                               case REORDER_BUFFER_CHANGE_TRUNCATE:
+                                       reloid = 
change->data.truncate_msg.relid;
+                                       relation = 
RelationIdGetRelation(reloid);
+                                       rb->apply_change(rb, txn, relation, 
change);
+                                       RelationClose(relation);
+                                       break;
+
                                case 
REORDER_BUFFER_CHANGE_INTERNAL_SPEC_CONFIRM:
 
                                        /*
@@ -2255,6 +2263,7 @@ ReorderBufferSerializeChange(ReorderBuffer *rb, 
ReorderBufferTXN *txn,
                                }
                                break;
                        }
+               case REORDER_BUFFER_CHANGE_TRUNCATE:
                case REORDER_BUFFER_CHANGE_INTERNAL_SPEC_CONFIRM:
                case REORDER_BUFFER_CHANGE_INTERNAL_COMMAND_ID:
                case REORDER_BUFFER_CHANGE_INTERNAL_TUPLECID:
@@ -2534,6 +2543,7 @@ ReorderBufferRestoreChange(ReorderBuffer *rb, 
ReorderBufferTXN *txn,
                                break;
                        }
                        /* the base struct contains all the data, easy peasy */
+               case REORDER_BUFFER_CHANGE_TRUNCATE:
                case REORDER_BUFFER_CHANGE_INTERNAL_SPEC_CONFIRM:
                case REORDER_BUFFER_CHANGE_INTERNAL_COMMAND_ID:
                case REORDER_BUFFER_CHANGE_INTERNAL_TUPLECID:
diff --git a/src/include/access/heapam_xlog.h b/src/include/access/heapam_xlog.h
index 700e25c36a..0052e4c569 100644
--- a/src/include/access/heapam_xlog.h
+++ b/src/include/access/heapam_xlog.h
@@ -32,7 +32,7 @@
 #define XLOG_HEAP_INSERT               0x00
 #define XLOG_HEAP_DELETE               0x10
 #define XLOG_HEAP_UPDATE               0x20
-/* 0x030 is free, was XLOG_HEAP_MOVE */
+#define XLOG_HEAP_TRUNCATE             0x30
 #define XLOG_HEAP_HOT_UPDATE   0x40
 #define XLOG_HEAP_CONFIRM              0x50
 #define XLOG_HEAP_LOCK                 0x60
@@ -109,6 +109,27 @@ typedef struct xl_heap_delete
 
 #define SizeOfHeapDelete       (offsetof(xl_heap_delete, flags) + 
sizeof(uint8))
 
+/*
+ * xl_heap_delete flag values, 8 bits are available.
+ */
+#define XLH_TRUNCATE_CASCADE                                   (1<<0)
+#define XLH_TRUNCATE_RESTART_SEQS                              (1<<1)
+
+/*
+ * For truncate we list all truncated relids in an array, followed by all
+ * sequence relids that need to be restarted, if any.
+ * All rels are always within the same database, so we just list dbid once.
+ */
+typedef struct xl_heap_truncate
+{
+       Oid                     dbId;
+       uint32          nrelids;
+       uint8           flags;
+       Oid relids[FLEXIBLE_ARRAY_MEMBER];
+} xl_heap_truncate;
+
+#define SizeOfHeapTruncate     (offsetof(xl_heap_truncate, relids))
+
 /*
  * We don't store the whole fixed part (HeapTupleHeaderData) of an inserted
  * or updated tuple in WAL; we can save a few bytes by reconstructing the
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index 06e5180a30..ceabcc14d8 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -54,6 +54,8 @@ extern void AlterRelationNamespaceInternal(Relation classRel, 
Oid relOid,
 extern void CheckTableNotInUse(Relation rel, const char *stmt);
 
 extern void ExecuteTruncate(TruncateStmt *stmt);
+extern void ExecuteTruncateGuts(List *explicit_rels, List *relids, List 
*relids_logged,
+                                                               DropBehavior 
behavior, bool restart_seqs);
 
 extern void SetRelationHasSubclass(Oid relationId, bool relhassubclass);
 
diff --git a/src/include/replication/reorderbuffer.h 
b/src/include/replication/reorderbuffer.h
index aa430c843c..7cb3b0f26a 100644
--- a/src/include/replication/reorderbuffer.h
+++ b/src/include/replication/reorderbuffer.h
@@ -59,7 +59,8 @@ enum ReorderBufferChangeType
        REORDER_BUFFER_CHANGE_INTERNAL_COMMAND_ID,
        REORDER_BUFFER_CHANGE_INTERNAL_TUPLECID,
        REORDER_BUFFER_CHANGE_INTERNAL_SPEC_INSERT,
-       REORDER_BUFFER_CHANGE_INTERNAL_SPEC_CONFIRM
+       REORDER_BUFFER_CHANGE_INTERNAL_SPEC_CONFIRM,
+       REORDER_BUFFER_CHANGE_TRUNCATE
 };
 
 /*
@@ -128,7 +129,18 @@ typedef struct ReorderBufferChange
                        CommandId       cmax;
                        CommandId       combocid;
                }                       tuplecid;
-       }                       data;
+
+               /*
+                * Truncate data for REORDER_BUFFER_CHANGE_TRUNCATE representing
+                * one relation to be truncated.
+                */
+               struct
+               {
+                       Oid                     relid;
+                       bool            cascade;
+                       bool            restart_seqs;
+               }       truncate_msg;
+       }       data;
 
        /*
         * While in use this is how a change is linked into a transactions,

base-commit: ed69864350a59c51c8570900601ebd335956b638
-- 
2.17.0

From e2773f54d42fb7ee41562d720861e5ede9fecc6f Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <pete...@gmx.net>
Date: Tue, 3 Apr 2018 22:13:07 -0400
Subject: [PATCH v18 2/2] Logical replication support for TRUNCATE

---
 doc/src/sgml/catalogs.sgml                  |   8 ++
 doc/src/sgml/logical-replication.sgml       |  13 +--
 doc/src/sgml/protocol.sgml                  |  46 +++++++++
 doc/src/sgml/ref/create_publication.sgml    |  10 +-
 src/backend/catalog/pg_publication.c        |   1 +
 src/backend/commands/publicationcmds.c      |  20 +++-
 src/backend/replication/logical/proto.c     |  45 +++++++++
 src/backend/replication/logical/worker.c    |  57 +++++++++++
 src/backend/replication/pgoutput/pgoutput.c |  16 ++-
 src/backend/utils/cache/relcache.c          |   3 +-
 src/bin/pg_dump/pg_dump.c                   |  33 ++++--
 src/bin/pg_dump/pg_dump.h                   |   1 +
 src/bin/pg_dump/t/002_pg_dump.pl            |   2 +-
 src/bin/psql/describe.c                     |  26 ++++-
 src/include/catalog/pg_publication.h        |   7 +-
 src/include/replication/logicalproto.h      |   4 +
 src/test/regress/expected/publication.out   |  84 ++++++++--------
 src/test/subscription/t/010_truncate.pl     | 106 ++++++++++++++++++++
 18 files changed, 406 insertions(+), 76 deletions(-)
 create mode 100644 src/test/subscription/t/010_truncate.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index d6a9d8c580..e8efa13e8d 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -5518,6 +5518,14 @@ <title><structname>pg_publication</structname> 
Columns</title>
       <entry>If true, <command>DELETE</command> operations are replicated for
        tables in the publication.</entry>
      </row>
+
+     <row>
+      <entry><structfield>pubtruncate</structfield></entry>
+      <entry><type>bool</type></entry>
+      <entry></entry>
+      <entry>If true, <command>TRUNCATE</command> operations are replicated for
+       tables in the publication.</entry>
+     </row>
     </tbody>
    </tgroup>
   </table>
diff --git a/doc/src/sgml/logical-replication.sgml 
b/doc/src/sgml/logical-replication.sgml
index 75551d8ee1..151e773fc2 100644
--- a/doc/src/sgml/logical-replication.sgml
+++ b/doc/src/sgml/logical-replication.sgml
@@ -108,8 +108,8 @@ <title>Publication</title>
 
   <para>
    Publications can choose to limit the changes they produce to
-   any combination of <command>INSERT</command>, <command>UPDATE</command>, and
-   <command>DELETE</command>, similar to how triggers are fired by
+   any combination of <command>INSERT</command>, <command>UPDATE</command>,
+   <command>DELETE</command>, and <command>TRUNCATE</command>, similar to how 
triggers are fired by
    particular event types.  By default, all operation types are replicated.
   </para>
 
@@ -364,15 +364,6 @@ <title>Restrictions</title>
     </para>
    </listitem>
 
-   <listitem>
-    <para>
-     <command>TRUNCATE</command> commands are not replicated.  This can, of
-     course, be worked around by using <command>DELETE</command> instead.  To
-     avoid accidental <command>TRUNCATE</command> invocations, you can revoke
-     the <literal>TRUNCATE</literal> privilege from tables.
-    </para>
-   </listitem>
-
    <listitem>
     <para>
      Large objects (see <xref linkend="largeobjects"/>) are not replicated.
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 8c488506fa..83fe7804d0 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -6763,6 +6763,52 @@ <title>Logical Replication Message Formats</title>
 </listitem>
 </varlistentry>
 
+<varlistentry>
+<term>
+Truncate
+</term>
+<listitem>
+<para>
+
+<variablelist>
+<varlistentry>
+<term>
+        Byte1('T')
+</term>
+<listitem>
+<para>
+                Identifies the message as a truncate message.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        Int32
+</term>
+<listitem>
+<para>
+                ID of the relation corresponding to the ID in the relation
+                message.
+</para>
+</listitem>
+</varlistentry>
+<varlistentry>
+<term>
+        Int8
+</term>
+<listitem>
+<para>
+                Option bits for <command>TRUNCATE</command>:
+                1 for <literal>CASCADE</literal>, 2 for <literal>RESTART 
IDENTITY</literal>
+</para>
+</listitem>
+</varlistentry>
+
+</variablelist>
+</para>
+</listitem>
+</varlistentry>
+
 </variablelist>
 
 <para>
diff --git a/doc/src/sgml/ref/create_publication.sgml 
b/doc/src/sgml/ref/create_publication.sgml
index bfe12d5f41..99f87ca393 100644
--- a/doc/src/sgml/ref/create_publication.sgml
+++ b/doc/src/sgml/ref/create_publication.sgml
@@ -106,10 +106,11 @@ <title>Parameters</title>
           This parameter determines which DML operations will be published by
           the new publication to the subscribers.  The value is
           comma-separated list of operations.  The allowed operations are
-          <literal>insert</literal>, <literal>update</literal>, and
-          <literal>delete</literal>.  The default is to publish all actions,
+          <literal>insert</literal>, <literal>update</literal>,
+          <literal>delete</literal>, and <literal>truncate</literal>.
+          The default is to publish all actions,
           and so the default value for this option is
-          <literal>'insert, update, delete'</literal>.
+          <literal>'insert, update, delete, truncate'</literal>.
          </para>
         </listitem>
        </varlistentry>
@@ -168,8 +169,7 @@ <title>Notes</title>
   </para>
 
   <para>
-   <command>TRUNCATE</command> and <acronym>DDL</acronym> operations
-   are not published.
+   <acronym>DDL</acronym> operations are not published.
   </para>
  </refsect1>
 
diff --git a/src/backend/catalog/pg_publication.c 
b/src/backend/catalog/pg_publication.c
index ba18258ebb..ec3bd1d22d 100644
--- a/src/backend/catalog/pg_publication.c
+++ b/src/backend/catalog/pg_publication.c
@@ -376,6 +376,7 @@ GetPublication(Oid pubid)
        pub->pubactions.pubinsert = pubform->pubinsert;
        pub->pubactions.pubupdate = pubform->pubupdate;
        pub->pubactions.pubdelete = pubform->pubdelete;
+       pub->pubactions.pubtruncate = pubform->pubtruncate;
 
        ReleaseSysCache(tup);
 
diff --git a/src/backend/commands/publicationcmds.c 
b/src/backend/commands/publicationcmds.c
index 9c5aa9ebc2..29992d4a0e 100644
--- a/src/backend/commands/publicationcmds.c
+++ b/src/backend/commands/publicationcmds.c
@@ -62,7 +62,8 @@ parse_publication_options(List *options,
                                                  bool *publish_given,
                                                  bool *publish_insert,
                                                  bool *publish_update,
-                                                 bool *publish_delete)
+                                                 bool *publish_delete,
+                                                 bool *publish_truncate)
 {
        ListCell   *lc;
 
@@ -72,6 +73,7 @@ parse_publication_options(List *options,
        *publish_insert = true;
        *publish_update = true;
        *publish_delete = true;
+       *publish_truncate = true;
 
        /* Parse options */
        foreach(lc, options)
@@ -96,6 +98,7 @@ parse_publication_options(List *options,
                        *publish_insert = false;
                        *publish_update = false;
                        *publish_delete = false;
+                       *publish_truncate = false;
 
                        *publish_given = true;
                        publish = defGetString(defel);
@@ -116,6 +119,8 @@ parse_publication_options(List *options,
                                        *publish_update = true;
                                else if (strcmp(publish_opt, "delete") == 0)
                                        *publish_delete = true;
+                               else if (strcmp(publish_opt, "truncate") == 0)
+                                       *publish_truncate = true;
                                else
                                        ereport(ERROR,
                                                        
(errcode(ERRCODE_SYNTAX_ERROR),
@@ -145,6 +150,7 @@ CreatePublication(CreatePublicationStmt *stmt)
        bool            publish_insert;
        bool            publish_update;
        bool            publish_delete;
+       bool            publish_truncate;
        AclResult       aclresult;
 
        /* must have CREATE privilege on database */
@@ -181,7 +187,8 @@ CreatePublication(CreatePublicationStmt *stmt)
 
        parse_publication_options(stmt->options,
                                                          &publish_given, 
&publish_insert,
-                                                         &publish_update, 
&publish_delete);
+                                                         &publish_update, 
&publish_delete,
+                                                         &publish_truncate);
 
        values[Anum_pg_publication_puballtables - 1] =
                BoolGetDatum(stmt->for_all_tables);
@@ -191,6 +198,8 @@ CreatePublication(CreatePublicationStmt *stmt)
                BoolGetDatum(publish_update);
        values[Anum_pg_publication_pubdelete - 1] =
                BoolGetDatum(publish_delete);
+       values[Anum_pg_publication_pubtruncate - 1] =
+               BoolGetDatum(publish_truncate);
 
        tup = heap_form_tuple(RelationGetDescr(rel), values, nulls);
 
@@ -237,11 +246,13 @@ AlterPublicationOptions(AlterPublicationStmt *stmt, 
Relation rel,
        bool            publish_insert;
        bool            publish_update;
        bool            publish_delete;
+       bool            publish_truncate;
        ObjectAddress obj;
 
        parse_publication_options(stmt->options,
                                                          &publish_given, 
&publish_insert,
-                                                         &publish_update, 
&publish_delete);
+                                                         &publish_update, 
&publish_delete,
+                                                         &publish_truncate);
 
        /* Everything ok, form a new tuple. */
        memset(values, 0, sizeof(values));
@@ -258,6 +269,9 @@ AlterPublicationOptions(AlterPublicationStmt *stmt, 
Relation rel,
 
                values[Anum_pg_publication_pubdelete - 1] = 
BoolGetDatum(publish_delete);
                replaces[Anum_pg_publication_pubdelete - 1] = true;
+
+               values[Anum_pg_publication_pubtruncate - 1] = 
BoolGetDatum(publish_truncate);
+               replaces[Anum_pg_publication_pubtruncate - 1] = true;
        }
 
        tup = heap_modify_tuple(tup, RelationGetDescr(rel), values, nulls,
diff --git a/src/backend/replication/logical/proto.c 
b/src/backend/replication/logical/proto.c
index 948343e4ae..2fa6f8393d 100644
--- a/src/backend/replication/logical/proto.c
+++ b/src/backend/replication/logical/proto.c
@@ -26,6 +26,9 @@
  */
 #define LOGICALREP_IS_REPLICA_IDENTITY 1
 
+#define TRUNCATE_CASCADE               (1<<0)
+#define TRUNCATE_RESTART_SEQS  (1<<1)
+
 static void logicalrep_write_attrs(StringInfo out, Relation rel);
 static void logicalrep_write_tuple(StringInfo out, Relation rel,
                                           HeapTuple tuple);
@@ -292,6 +295,48 @@ logicalrep_read_delete(StringInfo in, LogicalRepTupleData 
*oldtup)
        return relid;
 }
 
+/*
+ * Write TRUNCATE to the output stream.
+ */
+void
+logicalrep_write_truncate(StringInfo out, Relation rel,
+                                                 bool cascade, bool 
restart_seqs)
+{
+       uint8 flags = 0;
+
+       pq_sendbyte(out, 'T');          /* action TRUNCATE */
+
+       /* use Oid as relation identifier */
+       pq_sendint32(out, RelationGetRelid(rel));
+
+       /* encode and send truncate flags */
+       if (cascade)
+               flags |= TRUNCATE_CASCADE;
+       if (restart_seqs)
+               flags |= TRUNCATE_RESTART_SEQS;
+       pq_sendint8(out, flags);
+}
+
+/*
+ * Read TRUNCATE from stream.
+ */
+LogicalRepRelId
+logicalrep_read_truncate(StringInfo in, bool *cascade, bool *restart_seqs)
+{
+       LogicalRepRelId relid;
+       uint8 flags;
+
+       /* read the relation id */
+       relid = pq_getmsgint(in, 4);
+
+       /* read and decode truncate flags */
+       flags = pq_getmsgint(in, 1);
+       *cascade = (flags & TRUNCATE_CASCADE) > 0;
+       *restart_seqs = (flags & TRUNCATE_RESTART_SEQS) > 0;
+
+       return relid;
+}
+
 /*
  * Write relation description to the output stream.
  */
diff --git a/src/backend/replication/logical/worker.c 
b/src/backend/replication/logical/worker.c
index fdace7eea2..7c9c24ae9f 100644
--- a/src/backend/replication/logical/worker.c
+++ b/src/backend/replication/logical/worker.c
@@ -30,10 +30,12 @@
 #include "access/xact.h"
 #include "access/xlog_internal.h"
 
+#include "catalog/catalog.h"
 #include "catalog/namespace.h"
 #include "catalog/pg_subscription.h"
 #include "catalog/pg_subscription_rel.h"
 
+#include "commands/tablecmds.h"
 #include "commands/trigger.h"
 
 #include "executor/executor.h"
@@ -83,6 +85,7 @@
 #include "utils/inval.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
+#include "utils/rel.h"
 #include "utils/timeout.h"
 #include "utils/tqual.h"
 #include "utils/syscache.h"
@@ -888,6 +891,56 @@ apply_handle_delete(StringInfo s)
        CommandCounterIncrement();
 }
 
+/*
+ * Handle TRUNCATE message.
+ *
+ * TODO: FDW support
+ */
+static void
+apply_handle_truncate(StringInfo s)
+{
+       LogicalRepRelMapEntry *rel;
+       LogicalRepRelId relid;
+       bool     cascade = false;
+       bool     restart_seqs = false;
+       List    *rels = NIL;
+       List    *relids = NIL;
+       List    *relids_logged = NIL;
+
+       ensure_transaction();
+
+       relid = logicalrep_read_truncate(s, &cascade, &restart_seqs);
+       rel = logicalrep_rel_open(relid, RowExclusiveLock);
+       if (!should_apply_changes_for_rel(rel))
+       {
+               /*
+                * The relation can't become interesting in the middle of the
+                * transaction so it's safe to unlock it.
+                */
+               logicalrep_rel_close(rel, RowExclusiveLock);
+               return;
+       }
+
+       /* Check if we can do the truncate. */
+       check_relation_updatable(rel);
+
+       rels = lappend(rels, rel->localrel);
+       relids = lappend_oid(relids, rel->localreloid);
+       if (RelationIsLogicallyLogged(rel->localrel))
+               relids_logged = lappend_oid(relids, rel->localreloid);
+
+       /*
+        * Even if we used CASCADE on the upstream master we explicitly
+        * default to replaying changes without further cascading.
+        * This might be later changeable with a user specified option.
+        */
+       ExecuteTruncateGuts(rels, relids, relids_logged, DROP_RESTRICT, 
restart_seqs);
+
+       logicalrep_rel_close(rel, NoLock);
+
+       CommandCounterIncrement();
+}
+
 
 /*
  * Logical replication protocol message dispatcher.
@@ -919,6 +972,10 @@ apply_dispatch(StringInfo s)
                case 'D':
                        apply_handle_delete(s);
                        break;
+                       /* TRUNCATE */
+               case 'T':
+                       apply_handle_truncate(s);
+                       break;
                        /* RELATION */
                case 'R':
                        apply_handle_relation(s);
diff --git a/src/backend/replication/pgoutput/pgoutput.c 
b/src/backend/replication/pgoutput/pgoutput.c
index aa9cf5b54e..27d28259f2 100644
--- a/src/backend/replication/pgoutput/pgoutput.c
+++ b/src/backend/replication/pgoutput/pgoutput.c
@@ -281,6 +281,10 @@ pgoutput_change(LogicalDecodingContext *ctx, 
ReorderBufferTXN *txn,
                        if (!relentry->pubactions.pubdelete)
                                return;
                        break;
+               case REORDER_BUFFER_CHANGE_TRUNCATE:
+                       if (!relentry->pubactions.pubtruncate)
+                               return;
+                       break;
                default:
                        Assert(false);
        }
@@ -354,6 +358,13 @@ pgoutput_change(LogicalDecodingContext *ctx, 
ReorderBufferTXN *txn,
                        else
                                elog(DEBUG1, "didn't send DELETE change because 
of missing oldtuple");
                        break;
+               case REORDER_BUFFER_CHANGE_TRUNCATE:
+                       OutputPluginPrepareWrite(ctx, true);
+                       logicalrep_write_truncate(ctx->out, relation,
+                                                                         
change->data.truncate_msg.cascade,
+                                                                         
change->data.truncate_msg.restart_seqs);
+                       OutputPluginWrite(ctx, true);
+                       break;
                default:
                        Assert(false);
        }
@@ -504,7 +515,7 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
                 * we only need to consider ones that the subscriber requested.
                 */
                entry->pubactions.pubinsert = entry->pubactions.pubupdate =
-                       entry->pubactions.pubdelete = false;
+                       entry->pubactions.pubdelete = 
entry->pubactions.pubtruncate = false;
 
                foreach(lc, data->publications)
                {
@@ -515,10 +526,11 @@ get_rel_sync_entry(PGOutputData *data, Oid relid)
                                entry->pubactions.pubinsert |= 
pub->pubactions.pubinsert;
                                entry->pubactions.pubupdate |= 
pub->pubactions.pubupdate;
                                entry->pubactions.pubdelete |= 
pub->pubactions.pubdelete;
+                               entry->pubactions.pubtruncate |= 
pub->pubactions.pubtruncate;
                        }
 
                        if (entry->pubactions.pubinsert && 
entry->pubactions.pubupdate &&
-                               entry->pubactions.pubdelete)
+                               entry->pubactions.pubdelete && 
entry->pubactions.pubtruncate)
                                break;
                }
 
diff --git a/src/backend/utils/cache/relcache.c 
b/src/backend/utils/cache/relcache.c
index 69a2114a10..6a67c185b0 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -5339,6 +5339,7 @@ GetRelationPublicationActions(Relation relation)
                pubactions->pubinsert |= pubform->pubinsert;
                pubactions->pubupdate |= pubform->pubupdate;
                pubactions->pubdelete |= pubform->pubdelete;
+               pubactions->pubtruncate |= pubform->pubtruncate;
 
                ReleaseSysCache(tup);
 
@@ -5347,7 +5348,7 @@ GetRelationPublicationActions(Relation relation)
                 * other publications.
                 */
                if (pubactions->pubinsert && pubactions->pubupdate &&
-                       pubactions->pubdelete)
+                       pubactions->pubdelete && pubactions->pubtruncate)
                        break;
        }
 
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index b8d65a9ee3..f5286f890e 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -3712,6 +3712,7 @@ getPublications(Archive *fout)
        int                     i_pubinsert;
        int                     i_pubupdate;
        int                     i_pubdelete;
+       int                     i_pubtruncate;
        int                     i,
                                ntups;
 
@@ -3723,12 +3724,20 @@ getPublications(Archive *fout)
        resetPQExpBuffer(query);
 
        /* Get the publications. */
-       appendPQExpBuffer(query,
-                                         "SELECT p.tableoid, p.oid, p.pubname, 
"
-                                         "(%s p.pubowner) AS rolname, "
-                                         "p.puballtables, p.pubinsert, 
p.pubupdate, p.pubdelete "
-                                         "FROM pg_publication p",
-                                         username_subquery);
+       if (fout->remoteVersion >= 110000)
+               appendPQExpBuffer(query,
+                                                 "SELECT p.tableoid, p.oid, 
p.pubname, "
+                                                 "(%s p.pubowner) AS rolname, "
+                                                 "p.puballtables, p.pubinsert, 
p.pubupdate, p.pubdelete, p.pubtruncate "
+                                                 "FROM pg_publication p",
+                                                 username_subquery);
+       else
+               appendPQExpBuffer(query,
+                                                 "SELECT p.tableoid, p.oid, 
p.pubname, "
+                                                 "(%s p.pubowner) AS rolname, "
+                                                 "p.puballtables, p.pubinsert, 
p.pubupdate, p.pubdelete, false AS pubtruncate "
+                                                 "FROM pg_publication p",
+                                                 username_subquery);
 
        res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
 
@@ -3742,6 +3751,7 @@ getPublications(Archive *fout)
        i_pubinsert = PQfnumber(res, "pubinsert");
        i_pubupdate = PQfnumber(res, "pubupdate");
        i_pubdelete = PQfnumber(res, "pubdelete");
+       i_pubtruncate = PQfnumber(res, "pubtruncate");
 
        pubinfo = pg_malloc(ntups * sizeof(PublicationInfo));
 
@@ -3762,6 +3772,8 @@ getPublications(Archive *fout)
                        (strcmp(PQgetvalue(res, i, i_pubupdate), "t") == 0);
                pubinfo[i].pubdelete =
                        (strcmp(PQgetvalue(res, i, i_pubdelete), "t") == 0);
+               pubinfo[i].pubtruncate =
+                       (strcmp(PQgetvalue(res, i, i_pubtruncate), "t") == 0);
 
                if (strlen(pubinfo[i].rolname) == 0)
                        write_msg(NULL, "WARNING: owner of publication \"%s\" 
appears to be invalid\n",
@@ -3829,6 +3841,15 @@ dumpPublication(Archive *fout, PublicationInfo *pubinfo)
                first = false;
        }
 
+       if (pubinfo->pubtruncate)
+       {
+               if (!first)
+                       appendPQExpBufferStr(query, ", ");
+
+               appendPQExpBufferStr(query, "truncate");
+               first = false;
+       }
+
        appendPQExpBufferStr(query, "');\n");
 
        ArchiveEntry(fout, pubinfo->dobj.catId, pubinfo->dobj.dumpId,
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index a4d6d926a8..c2314758de 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -595,6 +595,7 @@ typedef struct _PublicationInfo
        bool            pubinsert;
        bool            pubupdate;
        bool            pubdelete;
+       bool            pubtruncate;
 } PublicationInfo;
 
 /*
diff --git a/src/bin/pg_dump/t/002_pg_dump.pl b/src/bin/pg_dump/t/002_pg_dump.pl
index 1bea6ae81d..c14664ddaf 100644
--- a/src/bin/pg_dump/t/002_pg_dump.pl
+++ b/src/bin/pg_dump/t/002_pg_dump.pl
@@ -4401,7 +4401,7 @@
                create_order => 50,
                create_sql   => 'CREATE PUBLICATION pub1;',
                regexp       => qr/^
-                       \QCREATE PUBLICATION pub1 WITH (publish = 'insert, 
update, delete');\E
+                       \QCREATE PUBLICATION pub1 WITH (publish = 'insert, 
update, delete, truncate');\E
                        /xm,
                like => {
                        binary_upgrade           => 1,
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index 0c3be1f504..75a1e42cee 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -5187,7 +5187,7 @@ listPublications(const char *pattern)
        PQExpBufferData buf;
        PGresult   *res;
        printQueryOpt myopt = pset.popt;
-       static const bool translate_columns[] = {false, false, false, false, 
false, false};
+       static const bool translate_columns[] = {false, false, false, false, 
false, false, false};
 
        if (pset.sversion < 100000)
        {
@@ -5207,13 +5207,17 @@ listPublications(const char *pattern)
                                          "  puballtables AS \"%s\",\n"
                                          "  pubinsert AS \"%s\",\n"
                                          "  pubupdate AS \"%s\",\n"
-                                         "  pubdelete AS \"%s\"\n",
+                                         "  pubdelete AS \"%s\"",
                                          gettext_noop("Name"),
                                          gettext_noop("Owner"),
                                          gettext_noop("All tables"),
                                          gettext_noop("Inserts"),
                                          gettext_noop("Updates"),
                                          gettext_noop("Deletes"));
+       if (pset.sversion >= 110000)
+               appendPQExpBuffer(&buf,
+                                                 ",\n  pubtruncate AS \"%s\"",
+                                                 gettext_noop("Truncates"));
 
        appendPQExpBufferStr(&buf,
                                                 "\nFROM 
pg_catalog.pg_publication\n");
@@ -5254,6 +5258,7 @@ describePublications(const char *pattern)
        PQExpBufferData buf;
        int                     i;
        PGresult   *res;
+       bool            has_pubtruncate;
 
        if (pset.sversion < 100000)
        {
@@ -5265,13 +5270,19 @@ describePublications(const char *pattern)
                return true;
        }
 
+       has_pubtruncate = (pset.sversion >= 110000);
+
        initPQExpBuffer(&buf);
 
        printfPQExpBuffer(&buf,
                                          "SELECT oid, pubname,\n"
                                          "  
pg_catalog.pg_get_userbyid(pubowner) AS owner,\n"
-                                         "  puballtables, pubinsert, 
pubupdate, pubdelete\n"
-                                         "FROM pg_catalog.pg_publication\n");
+                                         "  puballtables, pubinsert, 
pubupdate, pubdelete");
+       if (has_pubtruncate)
+               appendPQExpBuffer(&buf,
+                                                 ", pubtruncate");
+       appendPQExpBuffer(&buf,
+                                         "\nFROM pg_catalog.pg_publication\n");
 
        processSQLNamePattern(pset.db, &buf, pattern, false, false,
                                                  NULL, "pubname", NULL,
@@ -5317,6 +5328,9 @@ describePublications(const char *pattern)
                printTableOpt myopt = pset.popt.topt;
                printTableContent cont;
 
+               if (has_pubtruncate)
+                       ncols++;
+
                initPQExpBuffer(&title);
                printfPQExpBuffer(&title, _("Publication %s"), pubname);
                printTableInit(&cont, &myopt, title.data, ncols, nrows);
@@ -5326,12 +5340,16 @@ describePublications(const char *pattern)
                printTableAddHeader(&cont, gettext_noop("Inserts"), true, 
align);
                printTableAddHeader(&cont, gettext_noop("Updates"), true, 
align);
                printTableAddHeader(&cont, gettext_noop("Deletes"), true, 
align);
+               if (has_pubtruncate)
+                       printTableAddHeader(&cont, gettext_noop("Truncates"), 
true, align);
 
                printTableAddCell(&cont, PQgetvalue(res, i, 2), false, false);
                printTableAddCell(&cont, PQgetvalue(res, i, 3), false, false);
                printTableAddCell(&cont, PQgetvalue(res, i, 4), false, false);
                printTableAddCell(&cont, PQgetvalue(res, i, 5), false, false);
                printTableAddCell(&cont, PQgetvalue(res, i, 6), false, false);
+               if (has_pubtruncate)
+                       printTableAddCell(&cont, PQgetvalue(res, i, 7), false, 
false);
 
                if (!puballtables)
                {
diff --git a/src/include/catalog/pg_publication.h 
b/src/include/catalog/pg_publication.h
index 37e77b8be7..b643c489cd 100644
--- a/src/include/catalog/pg_publication.h
+++ b/src/include/catalog/pg_publication.h
@@ -49,6 +49,9 @@ CATALOG(pg_publication,6104)
        /* true if deletes are published */
        bool            pubdelete;
 
+       /* true if truncates are published */
+       bool            pubtruncate;
+
 } FormData_pg_publication;
 
 /* ----------------
@@ -63,19 +66,21 @@ typedef FormData_pg_publication *Form_pg_publication;
  * ----------------
  */
 
-#define Natts_pg_publication                           6
+#define Natts_pg_publication                           7
 #define Anum_pg_publication_pubname                    1
 #define Anum_pg_publication_pubowner           2
 #define Anum_pg_publication_puballtables       3
 #define Anum_pg_publication_pubinsert          4
 #define Anum_pg_publication_pubupdate          5
 #define Anum_pg_publication_pubdelete          6
+#define Anum_pg_publication_pubtruncate                7
 
 typedef struct PublicationActions
 {
        bool            pubinsert;
        bool            pubupdate;
        bool            pubdelete;
+       bool            pubtruncate;
 } PublicationActions;
 
 typedef struct Publication
diff --git a/src/include/replication/logicalproto.h 
b/src/include/replication/logicalproto.h
index 116f16f42d..f635ed82c1 100644
--- a/src/include/replication/logicalproto.h
+++ b/src/include/replication/logicalproto.h
@@ -97,6 +97,10 @@ extern void logicalrep_write_delete(StringInfo out, Relation 
rel,
                                                HeapTuple oldtuple);
 extern LogicalRepRelId logicalrep_read_delete(StringInfo in,
                                           LogicalRepTupleData *oldtup);
+extern void logicalrep_write_truncate(StringInfo out, Relation rel,
+                                               bool cascade, bool 
restart_seqs);
+extern LogicalRepRelId logicalrep_read_truncate(StringInfo in,
+                                               bool *cascade, bool 
*restart_seqs);
 extern void logicalrep_write_rel(StringInfo out, Relation rel);
 extern LogicalRepRelation *logicalrep_read_rel(StringInfo in);
 extern void logicalrep_write_typ(StringInfo out, Oid typoid);
diff --git a/src/test/regress/expected/publication.out 
b/src/test/regress/expected/publication.out
index 0c86c647bc..afbbdd543d 100644
--- a/src/test/regress/expected/publication.out
+++ b/src/test/regress/expected/publication.out
@@ -21,20 +21,20 @@ ERROR:  unrecognized publication parameter: foo
 CREATE PUBLICATION testpub_xxx WITH (publish = 'cluster, vacuum');
 ERROR:  unrecognized "publish" value: "cluster"
 \dRp
-                                   List of publications
-        Name        |          Owner           | All tables | Inserts | 
Updates | Deletes 
---------------------+--------------------------+------------+---------+---------+---------
- testpib_ins_trunct | regress_publication_user | f          | t       | f      
 | f
- testpub_default    | regress_publication_user | f          | f       | t      
 | f
+                                         List of publications
+        Name        |          Owner           | All tables | Inserts | 
Updates | Deletes | Truncates 
+--------------------+--------------------------+------------+---------+---------+---------+-----------
+ testpib_ins_trunct | regress_publication_user | f          | t       | f      
 | f       | f
+ testpub_default    | regress_publication_user | f          | f       | t      
 | f       | f
 (2 rows)
 
 ALTER PUBLICATION testpub_default SET (publish = 'insert, update, delete');
 \dRp
-                                   List of publications
-        Name        |          Owner           | All tables | Inserts | 
Updates | Deletes 
---------------------+--------------------------+------------+---------+---------+---------
- testpib_ins_trunct | regress_publication_user | f          | t       | f      
 | f
- testpub_default    | regress_publication_user | f          | t       | t      
 | t
+                                         List of publications
+        Name        |          Owner           | All tables | Inserts | 
Updates | Deletes | Truncates 
+--------------------+--------------------------+------------+---------+---------+---------+-----------
+ testpib_ins_trunct | regress_publication_user | f          | t       | f      
 | f       | f
+ testpub_default    | regress_publication_user | f          | t       | t      
 | t       | f
 (2 rows)
 
 --- adding tables
@@ -76,10 +76,10 @@ Publications:
     "testpub_foralltables"
 
 \dRp+ testpub_foralltables
-                  Publication testpub_foralltables
-          Owner           | All tables | Inserts | Updates | Deletes 
---------------------------+------------+---------+---------+---------
- regress_publication_user | t          | t       | t       | f
+                        Publication testpub_foralltables
+          Owner           | All tables | Inserts | Updates | Deletes | 
Truncates 
+--------------------------+------------+---------+---------+---------+-----------
+ regress_publication_user | t          | t       | t       | f       | f
 (1 row)
 
 DROP TABLE testpub_tbl2;
@@ -89,19 +89,19 @@ CREATE TABLE testpub_tbl3a (b text) INHERITS (testpub_tbl3);
 CREATE PUBLICATION testpub3 FOR TABLE testpub_tbl3;
 CREATE PUBLICATION testpub4 FOR TABLE ONLY testpub_tbl3;
 \dRp+ testpub3
-                        Publication testpub3
-          Owner           | All tables | Inserts | Updates | Deletes 
---------------------------+------------+---------+---------+---------
- regress_publication_user | f          | t       | t       | t
+                              Publication testpub3
+          Owner           | All tables | Inserts | Updates | Deletes | 
Truncates 
+--------------------------+------------+---------+---------+---------+-----------
+ regress_publication_user | f          | t       | t       | t       | t
 Tables:
     "public.testpub_tbl3"
     "public.testpub_tbl3a"
 
 \dRp+ testpub4
-                        Publication testpub4
-          Owner           | All tables | Inserts | Updates | Deletes 
---------------------------+------------+---------+---------+---------
- regress_publication_user | f          | t       | t       | t
+                              Publication testpub4
+          Owner           | All tables | Inserts | Updates | Deletes | 
Truncates 
+--------------------------+------------+---------+---------+---------+-----------
+ regress_publication_user | f          | t       | t       | t       | t
 Tables:
     "public.testpub_tbl3"
 
@@ -119,10 +119,10 @@ ERROR:  relation "testpub_tbl1" is already member of 
publication "testpub_fortbl
 CREATE PUBLICATION testpub_fortbl FOR TABLE testpub_tbl1;
 ERROR:  publication "testpub_fortbl" already exists
 \dRp+ testpub_fortbl
-                     Publication testpub_fortbl
-          Owner           | All tables | Inserts | Updates | Deletes 
---------------------------+------------+---------+---------+---------
- regress_publication_user | f          | t       | t       | t
+                           Publication testpub_fortbl
+          Owner           | All tables | Inserts | Updates | Deletes | 
Truncates 
+--------------------------+------------+---------+---------+---------+-----------
+ regress_publication_user | f          | t       | t       | t       | t
 Tables:
     "pub_test.testpub_nopk"
     "public.testpub_tbl1"
@@ -165,10 +165,10 @@ Publications:
     "testpub_fortbl"
 
 \dRp+ testpub_default
-                     Publication testpub_default
-          Owner           | All tables | Inserts | Updates | Deletes 
---------------------------+------------+---------+---------+---------
- regress_publication_user | f          | t       | t       | t
+                           Publication testpub_default
+          Owner           | All tables | Inserts | Updates | Deletes | 
Truncates 
+--------------------------+------------+---------+---------+---------+-----------
+ regress_publication_user | f          | t       | t       | t       | f
 Tables:
     "pub_test.testpub_nopk"
     "public.testpub_tbl1"
@@ -210,10 +210,10 @@ DROP TABLE testpub_parted;
 DROP VIEW testpub_view;
 DROP TABLE testpub_tbl1;
 \dRp+ testpub_default
-                     Publication testpub_default
-          Owner           | All tables | Inserts | Updates | Deletes 
---------------------------+------------+---------+---------+---------
- regress_publication_user | f          | t       | t       | t
+                           Publication testpub_default
+          Owner           | All tables | Inserts | Updates | Deletes | 
Truncates 
+--------------------------+------------+---------+---------+---------+-----------
+ regress_publication_user | f          | t       | t       | t       | f
 (1 row)
 
 -- fail - must be owner of publication
@@ -223,20 +223,20 @@ ERROR:  must be owner of publication testpub_default
 RESET ROLE;
 ALTER PUBLICATION testpub_default RENAME TO testpub_foo;
 \dRp testpub_foo
-                               List of publications
-    Name     |          Owner           | All tables | Inserts | Updates | 
Deletes 
--------------+--------------------------+------------+---------+---------+---------
- testpub_foo | regress_publication_user | f          | t       | t       | t
+                                     List of publications
+    Name     |          Owner           | All tables | Inserts | Updates | 
Deletes | Truncates 
+-------------+--------------------------+------------+---------+---------+---------+-----------
+ testpub_foo | regress_publication_user | f          | t       | t       | t   
    | f
 (1 row)
 
 -- rename back to keep the rest simple
 ALTER PUBLICATION testpub_foo RENAME TO testpub_default;
 ALTER PUBLICATION testpub_default OWNER TO regress_publication_user2;
 \dRp testpub_default
-                                  List of publications
-      Name       |           Owner           | All tables | Inserts | Updates 
| Deletes 
------------------+---------------------------+------------+---------+---------+---------
- testpub_default | regress_publication_user2 | f          | t       | t       
| t
+                                        List of publications
+      Name       |           Owner           | All tables | Inserts | Updates 
| Deletes | Truncates 
+-----------------+---------------------------+------------+---------+---------+---------+-----------
+ testpub_default | regress_publication_user2 | f          | t       | t       
| t       | f
 (1 row)
 
 DROP PUBLICATION testpub_default;
diff --git a/src/test/subscription/t/010_truncate.pl 
b/src/test/subscription/t/010_truncate.pl
new file mode 100644
index 0000000000..864135f007
--- /dev/null
+++ b/src/test/subscription/t/010_truncate.pl
@@ -0,0 +1,106 @@
+# Test TRUNCATE
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 5;
+
+# setup
+
+my $node_publisher = get_new_node('publisher');
+$node_publisher->init(allows_streaming => 'logical');
+$node_publisher->start;
+
+my $node_subscriber = get_new_node('subscriber');
+$node_subscriber->init(allows_streaming => 'logical');
+$node_subscriber->start;
+
+my $publisher_connstr = $node_publisher->connstr . ' dbname=postgres';
+
+$node_publisher->safe_psql('postgres',
+       "CREATE TABLE tab1 (a int PRIMARY KEY)");
+
+$node_subscriber->safe_psql('postgres',
+       "CREATE TABLE tab1 (a int PRIMARY KEY)");
+
+$node_publisher->safe_psql('postgres',
+       "CREATE TABLE tab2 (a int PRIMARY KEY)");
+
+$node_subscriber->safe_psql('postgres',
+       "CREATE TABLE tab2 (a int PRIMARY KEY)");
+
+$node_subscriber->safe_psql('postgres',
+       "CREATE SEQUENCE seq1 OWNED BY tab1.a"
+);
+$node_subscriber->safe_psql('postgres',
+       "ALTER SEQUENCE seq1 START 101"
+);
+
+$node_publisher->safe_psql('postgres',
+       "CREATE PUBLICATION pub1 FOR TABLE tab1");
+$node_publisher->safe_psql('postgres',
+       "CREATE PUBLICATION pub2 FOR TABLE tab2 WITH (publish = insert)");
+$node_subscriber->safe_psql('postgres',
+       "CREATE SUBSCRIPTION sub1 CONNECTION '$publisher_connstr 
application_name=sub1' PUBLICATION pub1");
+$node_subscriber->safe_psql('postgres',
+       "CREATE SUBSCRIPTION sub2 CONNECTION '$publisher_connstr 
application_name=sub2' PUBLICATION pub2");
+
+$node_publisher->wait_for_catchup('sub1');
+
+# insert data to truncate
+
+$node_subscriber->safe_psql('postgres', "INSERT INTO tab1 VALUES (1), (2), 
(3)");
+
+$node_publisher->wait_for_catchup('sub1');
+
+# truncate and check
+
+$node_publisher->safe_psql('postgres', "TRUNCATE tab1");
+
+$node_publisher->wait_for_catchup('sub1');
+
+my $result = $node_subscriber->safe_psql('postgres',
+       "SELECT count(*), min(a), max(a) FROM tab1");
+is($result, qq(0||),
+       'truncate replicated');
+
+$result = $node_subscriber->safe_psql('postgres',
+       "SELECT nextval('seq1')");
+is($result, qq(1),
+       'sequence not restarted');
+
+# truncate with restart identity
+
+$node_publisher->safe_psql('postgres', "TRUNCATE tab1 RESTART IDENTITY");
+
+$node_publisher->wait_for_catchup('sub1');
+
+$result = $node_subscriber->safe_psql('postgres',
+       "SELECT nextval('seq1')");
+is($result, qq(101),
+       'truncate restarted identities');
+
+# test publication that does not replicate truncate
+
+$node_subscriber->safe_psql('postgres', "INSERT INTO tab2 VALUES (1), (2), 
(3)");
+
+$node_publisher->safe_psql('postgres', "TRUNCATE tab2");
+
+$node_publisher->wait_for_catchup('sub2');
+
+$result = $node_subscriber->safe_psql('postgres',
+       "SELECT count(*), min(a), max(a) FROM tab2");
+is($result, qq(3|1|3),
+       'truncate not replicated');
+
+$node_publisher->safe_psql('postgres',
+       "ALTER PUBLICATION pub2 SET (publish = 'insert, truncate')");
+
+$node_publisher->safe_psql('postgres', "TRUNCATE tab2");
+
+$node_publisher->wait_for_catchup('sub2');
+
+$result = $node_subscriber->safe_psql('postgres',
+       "SELECT count(*), min(a), max(a) FROM tab2");
+is($result, qq(0||),
+       'truncate replicated after publication change');
-- 
2.17.0

Reply via email to