pglogical - logical replication contrib module

Started by Petr Jelinekabout 10 years ago30 messages
#1Petr Jelinek
petr@2ndquadrant.com
1 attachment(s)

Hi,

I'd like to submit the replication solution which is based on the
pglogical_output [1]https://commitfest.postgresql.org/8/418/ module (which is obviously needed for this to compile).

The pglogical contrib module provides extension which does the
master-slave logical replication based on the logical decoding.

The basic documentation is in README.md, I didn't bother making sgml
docs yet since I expect that there will be ongoing changes happening and
it's easier for me to update the markdown docs than sgml. I will do the
conversion once we start approaching committable state.

What it implements
- logical replication
- partial replication (replication sets)
- multiple sources for single subscriber
- origin filtering (so that if replication is setup both ways, there is
no cyclic replication)

It currently doesn't do multi-master or automatic DDL. I think DDL
should be relatively easy if somebody finishes the deparse extension as
the infrastructure for replicating arbitrary commands is present in this
patch.

It's rather large patch so I will just go very briefly over high level
overview of how it works, the details need to be discussed separately IMHO:
Catalogs:
- node - stores information about "nodes" (postgresql databases)
- node_interface - represents connection string(s) to nodes, we
separate interfaces to different catalog mainly to allow for setups
where different subscribers see different address of the provider server
- local_node - stores exactly one tuple which points to the nodes
catalog tuple that represents the local node of the current database
- subscription - represents subscription between two nodes, it
includes configuration of the subscription like replication set and
origin filters

Upstream:
- basically just implements the pglogical_output hooks according to
the catalogs

Downstream:
- several background workers
- supervisor is worker which manages all the other workers
- manager is per database worker which manages individual database
(node) and its subscriptions
- apply does the actual replication, one apply process is started per
subscription, connects to the walsender on the other side and applies
the changes received from there

[1]: https://commitfest.postgresql.org/8/418/

--
Petr Jelinek http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

0001-pglogical-v1.patch.gzapplication/gzip; name=0001-pglogical-v1.patch.gzDownload
����V0001-pglogical-v1.patch�\{s�F��;�S��$��zK9_Y�(G�LiI��R�C1�x��>��_w�0x����U�vW&������{F{x3v��FGg�q>>��������y8j_���O�����������l�}�9c��%������-��d�<
�O��]�����������������j7F�/�m`7X��d���"�����3Vow��� �����}z�z���W�Oob���^:�Z���1�s��|0>���p�������5dB��5�#{�zABge���#�����^�|��f��	H�.~��������aY��9���Q
?�h�
�##�M
)���S�j[vm�5I��#�{%���EnN�y{�
<�yr\.���F������J�9VB.���7M}~��J����uK�.�,������F�m�cI����p�l��JD�i�����a;�)�oW�f��93���3#�l�r�yv������)7?k�|�vK����"�G��~C�;�Ubr�Eh����yF�,��t	���f��j��/N����M�|j�o%��p������srh�N�w'NK�e���C���E�k��S���
?%���a�F���)���f�q�����1/�����Q��We����q�\r��M=�s��v�^a#)xf���:M��|��]�������y"2�agG�D�^�V}|x���H�4?��_���<��i;�K@�s�U��;��3k����9h�&����'�L3����iNC7�2�Ra�k��0���Q�^��l�f�N��q<�7?)X�Q���m@�D����9���Ys1�d#�����;�
���@l��d8B�Y�/�q� �0�O_
M����Fj��&%�K��"���n��pK�LV�Q��Y����dFaIK|=)�^\$���y0�����x�x=�����~��!`��sx$�2l�?���B(^&���[���%S���J��R>�$a@�����K
R
(s���-��0M��{�9^����7A�������E����^0��CfN
w��V|�G�vC�����
aq�����>Lp#�l1�R��_�0��O��/�T'����|3�*���S��8�!P�+�+��@Up���=�mpI`k�hT�k���P��%4mT'��	T��t����B��U�-�:B�*��U�P����C�s��6�-fJ��XY�W�%%}e�����U������U�O��[�������C��U����K��P��%�v����b|[��3���U��2|Ct���K
�*�|	^���
UW���wX�����+�
�L�]	'�����������
(����J��J�L�]YRaW@kev�f��1k6��e�����
�j6��W6�O:��V�����}h����N�^���z��5���3V���6����?����-����;�Cpj��;�h2��1(R%����o�|f��m�C����`P�K)���!��~�?��C���h��ad��h����q7
�z��w0�T�emMb�]
����o�����i�N����X����_��qr�QJ���h�z����Z=v�?��G�"����/,�V��d��a�B��A��������:A�(aI�e!tN�����
���N)�����U]�z������}w8�'����Z��O������}������{�\���T=�L}�}'��z��i�e�~�����Z�G��$�^��*s���^Q���Y��$I���8J�2� +�T��hOE>�?���� }�(i�'��y(�����������(����=��_�a`B��_�~���ax��������w�����,��,	^��e�������Cf9�������2���2���a��0Av_�;���d��v��y�����@�(,���`��?��bD�u�j���k:1X��=���H,1�!��`)�����M1�5�,��C\��F�4���h��7���KR`;s�G��p��dx���S��"�h��	�.�b��RA�,��^���g{�iKd^����^{��N�^���5������7x���g��K���"
(5f���4��4�N�66Q�K�����&o�[��z�����%[�������[�SM���8����c�d���Nik/�	���U��p�OS��vb�
��H5L\pc�lw���������_��z��������A"8I���"f�h���@!����`0P�a���F�|#�-�������>r�'��4�����C��V`�Pm{���Z���3i*���xs�(��,�H~,#2����9w�����&�����#�c�N�T��ul�x�B[�%�s�C
�R��L�^a���C�($vy$
������|��FA1}M����d��r�
J�E�bH�����I:�l����1��wM��1h|�B��������a�*���s�������"��f1bW�E��N8��IY�n�"��u�������f����T�-��p��W$7��U����I` _j�g�o0�\Z��&�<E�#�.���#�[��tS3����v��gy3�da�CL�> �T��3L��������Bk �������(�8�
Y@�;�����:y�L@��A�`>��t�s���!���b��;���������
$l�>��)����b/!t�pn��g�q����)7,5CJv6�$NAC!��F)}�%�#�$��_a�'f��Id%J��	�s�D�@<'�-��o5X
J�$x/<��i2��9�'@?��]�iXy-�g /,�(���b2(�dL�@��prz�^�s����"���L	�j�p}�����W;u�����,��@6��s>|>�<���O�v�U�!����,P�%yh��c�0��|
�������Yw

��8�M�My�a�E�����tK���VBX��&6���\�e��!��������\/�g.��L<���	!7�-�L��AN/�pfh�`Z�����*������\d3�q�N!N�y?�Is*��@:?v�~F+b���
��{xb������<2i�Z}��
:8��7�8NlfC����4�#�����%���Q�g�Hd��F!8w+
G$�t�hf�A��H����=�#C%8I_�-��� ��+{�B1*t?�1^�/���b�d�T���
.)�b�m����T���#g72
L
e� H��H�"�l*���X����_�D�xm%�O�t���v����U��XPCf�1�-�����*�rk���L�sAk�OJ�ktr�J�U�u�H���R	G�:,���
������N8=����x�5��40�$��l��(��y	1(�0��cD����@������`L;��L�6���0���%"
)����d|'FCJ]X�$���ys����$$��t���OZwEi8h/��:D����"��m�C����SP��j~a��oO���5Bo���>1zNW'�9��g*y�g��Q$/��
����\��t9����0����{���n�]��W�|�3��*X{��'K]@p�,�p ��r���l�I.	��\U������9��O���_�^i��h�]Loc����������#�Mp���
���3r������������#�DB���CPRQ^0d�>�J�/W���fJ��b����W;��B�.�>`��'C���i������q��=�~We�#p�������'��?.��fD��y��1bH���RwF� :,����ZcH��AD��`�p= �8)�E�:;6L�'55��Q�T�W�v=�����]�H0*�9i��FC�<�D�sB��1��f��
jPR�P�``[��GIx@B��]�1`����HP� �s��"JS(����F2��]��,�$�6G�F��Q]D5��m����*���R�����
���9Bt708�e4�	�H��U4��jBk�� .x�L�U����9w��ey\� 
R��?F�<-W*9��Y�,S��9�H+c-�k�C.b�����
���>,����
������KUaU-h�u;#�,�a�BY��e|y�]CzBe���*F9���!*:<+�ij��I�V��Q�u@�H���_b��X����,C��bG�Ps���8G����F^C��D��zd8 Uk!uKW����/
��P\��g���D�f#O�����0��{���T+p��f�v/
B)Wh5%��
�����TE�Hf��g��eF��E(
e�U�m(�$7�-]�Tk��pb.�5>���$83���<w7i�@Y�6��<�e[8H0��9��7�~��;x���(���DO�R'iI��xa�4�9l2;���4�(�s���2�l��$�b���3I�e���n������N<#I��-q<�\�����i�Tgz�I�P�.�L���R���ea�1�G���3	�Y�^c�zn�R��oL?��R��0����F�P���p�����������
�������A>�����������Y1_����-�ZF7'JNL=�1��a:�%6�T��1��=5���|�I��r�l���0���M2V��Nv��j�F�1�����B�_oV��p�2�9��8|����b��:�j�>qY�y���~�����Q�R��kJ4�#�oP�����"d��e�(m�|�
� T(r�9��(��Ql 2^(������0�L�#<����(��K�!��+�*?�3uF,�y����V�cuH���sA�H�c�n[����.$�v#��?�*��'6�ru�j�d�B�we��%�/4�{�����(�Yu#I�����#�S�x��#k�����3����r���+��o#���h�DL�YF�����C�YK��%�t�����g���&��4h#�i�IE����X�S9
@y!���D���Y�F���;QF.`r�NEOB��]o��?��
�������k����}?'�O���kz��S�hvi���2B�f�
;�\\(��si�Y�V+���%op�s�8-�G��B1i0�^�^��b�n�.�@l_�?bY�g��+�Gi�����8v#�����^��teg@������'����Zj�������������2Ypf!�@�+��Z������H�T��t:�/'dB&����a2����O�@������/���H��I;��u����M/h���`f �h�T���#]�M^��On(�W��k�({���l*��u~.�5��Gz*�HGJ�Rz��<.�?�������� �-�i�������8V�t����%=���
m*�T�Ue(�M��0���W�FKH�HKx�j���#KMy�5�J�mE�	��N��Xz��z�Q>�$�����U�e��_���n	��+Wz�����
�4�^�y��k��O�U�d�`������:Fn�������=��(|�HrZ���	
m-�F��So��]����U�$R� �/n��������Dn��%��<)(���mU����Q�	[���l��iUC	�	��*�`y����=zC����Z]I�������� &�Z@�II���H3��(��/g���\��OW��0�'.Y��g���P9/d>	�����M
I�D��l�)���Z������.T�rEU��<���:�&!�4Yo��tW�8(���E�k�!�t���2��b������X�J�	#��@m'�j�)��Uh�3M��V�b[�J����dO&PV�
�v�lP���K*HKH�1U��Q,���r2��)��V��_
�s�����g�����u��B\������?g���~��c�7��U\
���b�M�����e��7�x#�{������^z-�&�1w=u��
��?|��C�r��#Tp�b��o�sw�$[�?���2��$9������p
������0�����
>���',r7�;|��PL����E���0�7�q���[�����~�jFz7 ����C|�;!�W���kv�{���}���?	��+���u���{bO���f���8��q�g�����X�F�N�����vx�+��$����y��0`Y�������Ivlvc�L����U�u�zU=��<t(���:��{��G�jT^!N����F��A����]v0��Su�����U��{sw�����;���c���"��y�����02��41�}�_�����B�e{� 7!�,>j�X�]��]���N�S��'��D�j=��2et�y�Q�96dh��|�@�=��e0�CrLrm?�%2���(Y1C4Vi�������x�c�QNjc@z@���5������Q�xO�ad�G��h,��*��������7�a��x��*e�"�����@&x|&E$���8U1�8����h��B����8��d/Bc=�S��S��_Ol�A�:@7
|���e">u���#�����2��x*��'hI���A��b'������M��!���zD�]1�r��	F~��#]�;��H���#,�tL����[*F�3��W�_8���u8�MW03�h#���4�c�B��SFb@@��w;8��a�b��~�m��l_���m3��u�F���P���Mh�>2���5��C�bw�����1��������-�G2�I��v�$@�f4�B �F����^��C�����1T����zxr|Z9��?	�;V�P��(!�B&["�:��1Px���Nv�4E/J�Q�����1h��\�����T�x��������{���������Z
��l���P��������6&� ��'���C3����!�S}W�q���^E�����5~��F+���J,�{�n�{���p������d����+}���3�j��U]�5��HR��wXD����L:�Y��ACv���&���<���z*�����^s"-���;�v�X��-|��J�������U�`EV��">jU��j���u�~/�|���ad)x����m}f��C}�E��(E���;�F�R��D@�{�d��@<��Q��0��D9|7{2�w�2�v�J*��l�pC��s�v����I;qDj�}���A�b����8�4Wy��U�}cG��_k2f�C�u��x���n�B����(����m���Q7ZN'O�5$���������k;B����<Pdo��=%��\��vCX�5�+�����m����
����cTu��a(k�t9*�3>�Hlva��'�z�&��W�M�t��=h_��u5�0}�x�D��
�����:�r����=����,�;_�0\�{��)!U����C�V?2���
c�*��b�O��%�6Ymv�$��Sw��#w�0f����o�]���/����+������"��.���R���E���(Sc�����7����e<�L�����Gf����-����9�����e$�nY����� ts\�Ek��^Zg1E����=���^�����8�I�nh��4���WKD�*���Tb;����Z���$ma��h~�*U�;s�m�B��}"V2Qo�#�d���4��1u&H�g�����^��W��:�qvn�'�S��i `�k�`P��=������^�]&�HRi;C��p�Y"6�/Q�V�9>2kdF��sr���>�2h���g{Z�7*�
���'�4G��pK(�/�D>D��-��w��
9�28����tg'���I8]{��*�7A�Mx	S�m%A.�Zx~�w,i��@3Q�e)�{#c �D�2�2c
RDg2�|f��"��Id�!��@�{�������5r*�Ic��#��" ��[��#BFvAj&'�Bo|E�>����N����`Z�������d%�#�Q�L#���CT�@�z`�v�^&�h�H��4���a�`�[	�&`�%�����gR(�,a��E5X��7�V�3�;��.>3���4u�0�i�U="5���"������������p�K�����]X�y�X���4����P(�[a/h�q�6�GEZS��G#��e�:H�0�����62T�E�3�C!0��@:���{,tqI�\������#��+����-WF�g�*��t�����sT1e�.	Q�M��s7�����T�t���9��Y��h�`=}2��H9�K`���P�	��|
N]���Lsl,i������d�3]l�3��9��Ffk)���W�4���A�Q��50��0B^]�,��������;� ��t���a����x8{�%�a�t�����%������������e4�`��yD�n ���"-������0X����$���pBQ��i4b�aad:�L)jDC$��Cu�y�+�tx,=TM���f��b�7p�jL���=gWc�z������������%��2�D�b�=��@��&��j�A_H�l%
���|3������a�����XC'����E�5����'	3�2�WF���>?Z��e��.!�@��co�
�f(�&ZC�a��b�����PD����1���	4/���0��=k��IB_�W-XV�_Y�[v�F��Y8S#IDNe%��^Yf���hw|q�g�`����HA��j�������Y^k�kl���B���0���k�E���9
�����vH}����ix����@a�>�/q?_f+1TX^�����*V���#Ey���E[�R�d�f����z�UH�3M>(,K��E	��R<wO{�*w5]���}��Z�e|����s��h��B��.�fp"F����:��a�������$h8�y��0\�l6b��3�f��B����Yz	k�s���w!�]��
�C�ZA������F�-�E�)���N'm�M����;��������2��.��
����)1<���(��H|��\��-D���r���x�x���kl���^�������e:)�)��0�.�)�W��
���F0�Q��D�rj�o	�3�P�/��������F@6�@`��A�D�O/G�yo����D�gIV0�@�L����>+�mNn�"�u[�O��8R0�nNGnh���)����:����!�]�r��;���W�
z�`�q�>�0~�Q�)$r�
��l�H6^|�w�B�z����Uh���W�qP_,>*m�a����~� c�_������X	������m����P��,�6�����)�6�j
����\�!�P����������z}������s��|���7�[E��L������4��2v��Bd.@
\yKmV�����������%x#C\�*�����D�c�E	!�'t�k6�2Jo���% �I���]����M�`
�*���Q&��67v���7�l�'NV�&�Z��O�X=�-l�g4����TY-�6���0��.I&��u��??�������2s�cf�U;j��o�C���Z/m�d����Z���������n����>�V}�t��H�m�r� ��{��w�HwRZ�b���s$h(j�R��:I�z��d
c3�'�@	������2{
|D�+HA�T��t�������������q s���=��RYb�Y_@c���7��@������KU0���%HB�.�(���A3���r�<W��eU9�����K��d�����YG���Fm��������wc���`���q7��5"^�7�p���(��^��U�a�[����x\9C/{��
������z���,o��L�(��.)A���R�4��=t������,��D6�!��cX�wj�������;O�Q���ry�w*}�����VR�G��� ��y^+�O�3n��5��rdo��g>���V������o��b�^��@_Z�e���"&�m�'~���-���K����n&uQ8���H�53���M^����?Z���2?:>��CD�\�P��OIo3R���m���'I�H��T��KZp���e��|�O�6���� ���99���b������i���;��l���p�1�kY��5fT�H
�%�Y�����c(��]H�LJf(���h�N�I���3��L���T���>U�n�Sg7QY^DU?�z9����D���U���1��A��������E��)�	�q��~�-��.���Q�0���3�f����Y�L�:��!������s�|��_\�Q�4a�5[�^��`�>�.���V�F���O���OIA�� �cK������� 1����Fs��1!c�c$�2"��*�#D�	���#�Z�~�������{fs�@��X���s���'S�����n~q���-���l]vt���U�)�n��@m��>�{�V�k+Fb����(3����R���C��^�D��;>jTj�p����
�c��(&���-��`���(�}_��1����W���2��q���f��P����K��p�T9	A'6�r���'��Z����7���M�B�����������Zs�	Y�CS���5�0	�G���3��_�2�'04;W]UB�%����6E���pHO�:�B��%����8<���[���i�������W�����s���������g��Xww��M���m���z7�l�T����E��� �@h�-��CYl���8�J;����>.�:?�G���gWl�i�)jC*�����u�Sr�J�Kn��0�jC]�����h6���-�
D�7�����7yl������{g;���R2:���^��Y��c�1�(��I�v�T��;z��r(��K�EqX|$��V��Z[��a����mZ-��g�������e~�~�_X]i�W����G��;V���"%���2��!�5AT�1���������1�X����$�Y��s�=G����-��[m��l�*x73n!��v����gx��1���������9��9�S0g�n����9�@�����p���L����[6�hj��P��8��5;U���[anuY0!&���xQ�v��+��}��,�w�2��8mC��
iiBv��L�Kj��dB�-���@����>��E����������(������e�K��y;���S���s�ovl��2nz��/b'^���-�����F��%*���,���w|��3U��_;R?������;����=�����u�6A�e!�yP@O:�[-O����l��zCS,����t��f�������U��ws�wg[� �hY���7�^�{�d�B�;;����[3��:s�6���{�OR����5�(��1���s�����,Jo�X���Z��8)�s������#6����K"��x/!/��at�3��?��B��S��d�vi{���_����$�;��B�m$����T��j�F�X�l���d%#s4����Ry���Kta����K&�)Eh%6;��H�-��H�O^!���E�~�u����7J�������>i~������x�4���gJw�h�F��3I7\$u�T	u���F�z�+�l|���x�0#G��p��r���1��$�h������,�h�#`��5��X��-�md�������8LZ��1^[-���m��N/�M-)5��Oc�6�A�=���6�6*�����T!�z�i�i�.Z�>�����Q<\��}��uQ��)&�%�� �����
~is W���7i������%�������t���Co?W1�T���p����
�@Y�+��n���.�2P�3��I-�����������m�;|�����1��&�X�!�S�(��>������1[�[������q;����k�0K�N�z�����4����8��
~��!v6����aomJ�M]dS���M�����E������omK�m]d[���M�����Ev���D���'R��.Bj����G�#�{����o��E��(�s/�T!����z���jV����p/;���u1����I��ut|WOO�OA���$1Q��%���IP��C�9���<�����	�b�L����	}[;jh����;�$!�=���%!I��GG���-K��&�x+9P���_�|��C:�M��(<=��L���Al����$���C)�e�i,�=���V ~�T�5�����[m0;g0��� R�����;����wz���������(����`�_�0t�6�9%����G�[��1�U;�l�>�1���^}���}�0�`���p&o�'z��+n;���x8n�3.����+�jONk�����w����������$_��F��8��/�e����_~�m�����u���������'^�ao|��J]U��Ui����)3�V�����FH`;���x*@���Gz��(D�&�U	��A
�7�o=+o�Y�X�����5s��4�i:k�����������Y����pw����s*�����\�t
���jE)�$L�a���mh�L5r�6��[�*z+�?����P��^d�9�tL�o"6�����������~�mSQq���^Q�j����r��*��^�R4��H�t��H�=��<�?�����m��<Z7�"G�~��Z�����[���W$`���	��g���nb;B�&����p���[T�}�/lS�M�1�����MP�xY��3������.<1jg���R�TVK-�����6����Y��6�r�-5~c�%g������
���n���R�b$�:?����V&`Qmi����t�4)��7��(~���C��m��N�����T���[�q���v���Nv�v������m"�����r6>��������-}�>�v<t�fR�`�'MCX��10S�b�7���d��m]�{��e[����v�@
��~���A[0v��?�6��	��x+. ��&���l��P�w[�������m�;����o|g��vV���4�73���GYUNO+?�B���g��L�ofp���-l���Zpa���e�Nk������<������C��a�S���������v���l�fAQdj|M����uf,d>�H���L8���i��
f5`10�@����4�s�	Y�f���rV-)UMC�?�
�s��e��-���D����3��0��`�@6S�\B�8�}�$���\b
���f��?3��}[��-�����h�Ro5��dI��-���i�����n��T&�>�n��v����������	��c~���L0���H�2T�=l���*�F��������6�rV��)dW��SF�F��o��>,`��c�SR3���-�+Ds�D�=�����?����/������/����7�/�������gf
0�8�TBw����������~OU�j}[��?����7��^��X�EW�qp5�����^4�������
��_6��0�%_r�]7j�87�;yu@o3� ��,o��6��[����ep[w�Vy�����h�#�.��f��_���;>�v>I'��J��@�-�vEh1m��%zD��?��[�/�2�Gm�/4(�c��/��c�m�/4�c�	4Vu���A���U�?I�1������pR=�>���4���'r������F(��@�j�P-E����SK�VZ3��cK,!$��Eh����2j�t�����������8��m�+�t�����h�O�+���{7��h��6�J�d��"^����E?P�����P���l�\��w	��
���B�C �%[�-���/���2wc+s/�{\{�KKV
���9}���~yfi��G�����8#
�-`{4p���[+ ��������T.��+zR!�v{+cc�7-�����w�47?����g���;?���s�������
�8a�<�y.Y�7�pk��ed��H�jLI�v��a���a�����{"Kc�}�&��u���e[�b�[>��]F��]�%���<���O	��^���[����Na{��SO~&�">1~��]r�bIN@�.��e��Sv����������NY���6�����s�S�PN���`��
�v���J�� ���^fqA���"��Y�s�����	X�%�~*�+B�~`IV�W]����%�=�U�B�i����9������W��,R�A���\�ah�q�z������w<�,�z���������:�S"v;T�Z�r�N���?Z�\��E�R;�x��X*kA~%w$��	����Z��������j{r������6���6��R�r��w�E����%\#���a�[\�u���'rV�Y2�!�$/��%�2��!74�/�bD���p�����,a0����Tr�.������~�� �
�c��{\��j�f1�$���O^
7��?���V���]�=1��4h�0E7�����@z ]9����5��*�4eL7�@�r�R��
��$3�-��k�������u|��
��psLkN�L��X�:@)�������XBSm>Y*k�y?��,��t�Ay�U�j�i/�l�.����h3r��mF��~�=��g�d}J9t�?�L�������R�;c���n�_6~����������3��[��������}��~����rh�,��CB��7��N�b'
�$x��&8��mO�������os�����{�L`'��6�I"���~�P2�����c|��$�{�b�_n~gK�����c%������4U�J��]o&*���6��Lmf�&���C%}gp�f����� �d���-v�G��I�[�/n[��R��&nk���

���.�'
V�\������0�Y����w�����l����1�����X�����$h�!q g��. ������
@�g��]j��S=�
�/f��z��n*;J����Z��
9�}�L���Ey��,�g_��<;��*|j�]������]�5�0A��{��ma<������5��������a�#���������������g�8/����8��o�k^���qg���V���'�����f�������������������l}�2�=/�Z��NDo�P��4O���d��43����L�������j�\��6;���s��l1��L�������[�w�[��%m�3p�,B�w�e��`~��|&7f����!=���B�Su��Y <M����X��$������X��NIFTL��V6M��gl9���8��0eJ�LD.?$fR��[ ��
�09�{B����t�'��-������E;n��v(�����zo�E����j>����e�Y�O/��`���������j`jD��<@"�R�r�
�W�s�"��(S!�W��A����Y�?mE��nMY�������`�3�T���"�X���R�3P�+�j�������V���2;G��. ���+�Is?��<�,���del�<�W�<	^�J+;t����'�!*��R����u3��0����?IE��g�(�G�F������^0�,�]�S�?mp����w�a�L��D��*i?ez��������IO[�9���*���R��^~v�3����0"�����y��s=6�7q=�l�����������(|��g�R��&�9}?�@����_����������Dji�7.��l��c"����>������G�&H���@E���+�A����2�KC_~�&=�H?ct�N�p��$m�pe�"	��-��8x����qY=x�Z�L�p��?�|#Hj9���s�-}�)��Un�YE�/��*&��L���}���}�3�nl������?1�/���n��}����~g ����?v����o����9��9�Bn�.o����� ��$�����m�����������n����;���''����~_��}���f�a��v�]��.|�u�t�"�*�uJ������������8��������]{��^e���[Tb�O-g�N-�a5H�����`��b���X��a���=t����gjyV�+�j���${���E\;�%����)��L������Fm��L���?P� n�0V�>���e8T�������F�v%GA�f�����B��p�:�y0�B����F��v5F��
�c*�?W���q�Q���Qxd���o�����m��������8������G���9�����HF��v�o��v�������������v$c�|;���|;~$�q.B���|K~|[r~�1������m��r�7�|s�7���9�����|_�����/-���r�/���#����v�o��v�X�����o�����M�2}2�=h���u��D��ju�"��q��
��c���8|;�v���at�k�o����j�Px�^��(�U�b��'���Q��U��������� X�1���������]|���l]���ju���m��~���o����j�������qjy�NA���0��q�|#�Zq�����N��F)0��ND
#�B�A?������]p�N���C����Mh�3n�bU��s�����O������a��gP�/ut!y��B5
��L��*���,>Z!��J3�_�2�wZ}Y=��U���UV����Ao�nt<���A��y��['���S�	��:�&���e9��y����7�T��q�5�f���!�F�=F^��2��y����Z���3���s,����
��~&��P98��i�^�o`���Mrq,���-lb����I-�F���(p��)\e������U����6U��|�>��T���B�A���;�|��D��?QK@y?�W7\�����a;���un�M���-L��������5�9���������O��C�M�F>�f��9Ha��Vv�M��vv����3�in�����.f(�-x?����-���V�������M��/k���?+?��qvqF�-Z�����n��vff���&~{�k��kWWcZ��z���:�@L5���[�B`���/+g
���8�����gG{����F2`�J���<�
�������e��
V�j9���������2�9��:���"������!3���Q�n�����N��b��&H���/��m���etf��^��T���p�I����G�4�k�
a�jG��������o�����U|�w;���>�v�m�o���;��r���� 1�]��6�wM@�w'�	y$m#��	���}���3jw��a���#������^�"��m�xxSZ����l�?�v�#"m4�,��zZ�k���G@~��tt�cie)J7�]���P��w
�L'v�����#=V�d#�vy��������d�6R n�'H@v��p�bMmn�s������)m%�	��	z�B�����Na����������9��e�~}?I������xy���x��W{���ljI�����������(P8oCb
0��%�����Kn}2A�J����N�q���R�[�B�������`{xXk���5�8���o&	����/�a�F�������O�7�b�f����lE����0B#���n�O6��D]DR�=\�� �<c�y����$qo6\�i�l>�	��7�����/c�)�b�q�\�g����6�sH_�F6����L
owo�8�;#n��:�����rV%�rHD��)�6}��r���N�m���������E�M����+s��AQ�SS������a�Vy�1h�h�
<75������/�R|��%O��)�cl��m^���� �1����WycY�C���O 	���I�	#�!�/y�5���9�������Q�	=~"��IsqW&�?j~n�HrJ�+3����N�$<��/�l��F��=rR�s[��XC���h����%�=���|U��f�����>S��M���=�Gj	F���D��
cD�U*���b���;��A��KW#���S���L���}0 �hd��:��
�$t��)	��r_M
�!!L~�)�����J��������`a�������:8>>�����$j���Qi���x4���:�I{�	�	8�1�@��*v����}h��P=�W�����z
��aN7��5��P���>|�;�(x����~o��e�J,+���ko��8�#��-
S--�����?g�<N*��gh�����+_�i�..�8.i��B�\)��Mh�u��"�o�	�1/�2�-_os���`8��`\�����*��\�>�9�q����>�!��YLq2���[�/)%����{M���� Xz��K����_����)X�
���n����KK�6��g���ZC{t�,��	��L�._=G�9���(�)(�����D���dL<�����5�7#s\���B�r�0����3��q84��l�=�)Z��*A�:�����z6��o�`�N�)���w2���n�G��/�����4K�k�/89����_����w�j�k��������66'[I��v�H��-m��W2� .�gLcz����.��[��������n����^����~�����A|�K���6��|y?���P�2Q����*�+�-�+n�R��+�w���ln���\�>V���J��������T@��_`�������ld��e�+��J�+Y�����F=GK1���Hk�Y��G�:�$�=W���e������_��S��#��Z��QD�R�pxgg������8,}8����)�^�q���v���u4�d�yV-�0c�fX�a��[6B�L1A�V��~
��u�.��W_�?����P���F��@���j�\�3���g��&�gp��K@�Z�QW�~/�a�k�N���U��������(���g'�S��K@�z��Ia=���s �����r�P���iG��B5����V:���j����V�IT�9�����Ae/�7��^���l���{p�5���:��p\�_|T��`�V�t��okUY
�uD������5���z�q���x��;�/�^K�S��*G���z��qv����++F����\�TG�����;���W�I�Wa�g����{�D�O��{�%4������{�LV�1j�
�����&� �Y�����e��_�o�o��5�>X���j�N4e������[����������e���wZd��P�����|/���$^d�����;O�~R@�9_��/��fz��	1��m|��������S�BJZs/r$6�w��9n�Z�1{�9
���?�`c�����$���8:���z��t}����m�m��
��A[�:H7c�L�|I�!	}�k��<�������_B��m~j|����ta�L;�G}���,��"��p9��0�:�d�Sgx|������fy����1="Sd��?�O�S:5E�QoU���L�	c97�}��A��9�sXh�#z��v���=Lo�F&�����m� �]�3Fl�\��>�p�Z1����,s=��k���L��
����$;�rz�������{����65�mzu���pg�u��^�����������-
_|�)� ������p
)F�\�h��+���;�5+3�n�e�8���M���6�[��`�B����i/?�oRO�n�;%x���x�m"�7�'Gy�Bv�2����v��?�n1��-$on��7�<��Xl;rd���$���m�{VO�A6�dYY����zQ���Q�{�Rl�^i�C��:�S�~F�
�D���Vt��]F�6B?�����������i�����a�wh������`��A���6��&]�}��`��G�L{��S��s�D���������X{�^V�d!U��������H���Xv}%�c��gy��+w[A�5�k�Q��M�\vV��s�E�:���}�����35y�e�
����X�i��������]v��,�����[!D�����DF�n�^���V���+8��7�%|���PK����T�9�JK��{��v�������n����%��dF��iB8j��Z���h�f����l���G�}�=�f�63���������.T[O��axk�[����s�{�w���9�=���|����H���q��l�A�s���=m����.�9�����
Fo��z:W�,�Qom��OZ�r�n77��W�M?���������2${��
p����-P�{X#@����
z�`��:���O�;�SBW��C��)2FW�����!� ++��=�Nk������V��k��
�/%���������4]oF
7��2������������Ni
���>��$���!�j�EB/�Y�D9
t\����"2�����/�7�N[=+[�F��^�@w�$D���8���3�bzt��"R7�arCfM�Cy���~���,vdo>��ll�zd��8G��N��j�N4�*]�<F=�:��l�Ogf�����d��w������Kk���;^�
<��q��'����b@X�f�k��uV�	��Qa(j�-�8���M?��U����Gf+���i���	|B�y���W�]4�G}K��0����7�5��z��S�\��	;e��"�������kR�5�����Y5\<�_�M1V�M�L��	�CX�����	p�����<��a�tX��2<��(�4��P}���[nUyKw���&�e���`�qW`�5Py���c3���y���5Pl�y��O��$��}�[w�^���W����G�����-^,�m>�|=��X�3��j���?&����q�9�Q��1����S��V�����V�X���@�	~
b8%��_m	��q�wP�$�,r����a���OVd����by��9uP�w��<&Q�?�f��d����� F��6:5H����Q�!��������z)��Z\�����h�
��Jv���ZgG58�����i����35�k�u��F������z�XII9���	�I�bH���-q�=`�{@�;����A���B���
�6o�[���,�[y856?*\����|+g���N[����O�n��*����������3S���p�rx��=9�[#��x�8��[/�g}F��].�46��r�a�/�1C���n���yqm�����Uy����i=��7f�T�]��K���r��OM�$��"r<�Pr�s���M�8���vr(#[�<����T���y8{ml7��ld����]��[���=�2��P�N��}�t��#��G�t(�����e�����$�������bk��Fz��AYn��!)�+���7�����M]s���9��Vyc�`�-�fFr�D����������2�i�K]s48�����������u�.}��HKR-�l��C���Z�6e7�������MY��)e�l��E�[�n�e�m��DX��#Hy�Q�5����*�Kx��������v��E���a^D�l��Q���O�y��sHW�"H�������~�������X�	��rDn)
��	;��kl�]����W���k
[N�il��!��;{�������)Zd�/���1�_"E��w5��o�h|�<� 
~�M4�8(����t4N��4L�����_;����@�9
����r,�g����!�(I?��(�\	���C���M��kG������)f�`|r�6�t}��;*JG�
1������.�
U�r��Zq����D�9�����
�`�>��{�d�aM6?e�d�����|�����+�������'��=JQ[�TY�?���i��n�Y[��Y�����18E���U�;��w�T��@7��5�w���wlp@D8�u}�wJ&0��8�Q ��@���'�gEv��*;~���0J��{T4L�L�B���D�Q��x�z��9��]�|�'v�8f.��7B�����������/�D�H���$��9�J��\�24�{����~o��������������8�#��-
S--�����$���r�bn��`tY����0����8u��� �&U��=^��0.��;&�D.W�#��0"J*��[RY��S5����U�<����y�u�C���0h�=!����2�L��R}�l�����|�e�l����iK�a�=S�/��j�?��tlG�u�~8!a�����q����a@��{R������)��7��~��4�����(����>M��-���y��I����6V<TR��D����9c�|<a�:��<�B!3���<���`V3������_�c�B'q�{x:w��ex�D���K1p��T����Z})D�������,Q
�%w$���$��T##����L��s��Q��)N�����W���U�=X���#��]��
��}^�
�e�Za�}�zb3������,��%H�r
���L��y�!�S��m����e�-:��rDt=
���A�{���Op�k�
����"�u=��3O��[����M�]��[J���ux'�V.^`t���n��$Yw�������f�?�m�n	��������"g'jL�/	���WO��z����]�����,���������LT!��N�
�W�27���t���W[[�>7����������mc����0i��n�s���C&��n�{sd���:8�<�|�(��8�	��;t�3���R!Q��1^�U�1`�0d����M	l>-K�'%S���4���A��v(�,H���?��:II�&}�e�&�.fq���0-�F��w]��~��b]M�����Y@����db<HQn/���csT��D�\����{�e�
��y�*	�b�#%�	�(�O#��	�nG0�/6s?9B�w��;����>I����&��z;���������M�N���q�����L�"G	:��r�H���M�����Q�� ��	��D�?F�7�[��Yt������wm�(qq}��qV4d�7����p��H���p8j�{���sd�O>�}��E|��8��K�r5��~%�aV��<��d���=������^r�x�wt\<�"%�L�8�*�w�0�
��w�O���M-��3fo'%�7��G�J�V��7Y��7�4��Bhl��}6IT�o�l�b:��1������9>��h�6��X3����@�����;dp���_����02F�	�����V�{$�(���i�GF
��3�+�d<��#V%?�|���W[d�g�[�@%d��~�mmm<]_�z��$�3���9/�h�en,j�6���H�=�`!���v��pgf�6�����':}Y�I)2���,��fyw6�+��J`PB3�l�2|#!T9����13�Q
�t�Q��{��*�Q
^~�w|pv(Nh�]o�bE8���G��������a�qW{�{M�k����
�0��}��D^_^��/�}���g�~�J��[�;O�z��:<�Z__�z�A0��pt��u��&�2��z�p���+�ss9�pf�������,��%����������j�
�	��eE2��N��?��?���3�|��Ut�����g��\>���T3��g�����g�����g��>���d��x��j�N����������������������>�oS<���n�U�����Q'�q��v���0/�h�s���g������������3��?����������3��?��g�0��?����y~���
0��?���3��?���3��?��g���?���3��?���3��?�����TB��w�����g�����g������3��3�6�6�6�6�6�6�6�6�6��I|[|��^__>����g�����g����������
����M��������g�����g�����g���y���6���������#����g�����g�����g���
����-V���#����g�����g�����g�gn?��?���3��?���3��?������S�����#���g�����g�����g���y�.//���a�:^Y|�����z6��D�p�����G��Au����E��k�p��/����t����U�������G���'�QyqPU�qJ���A<
;j�R���Ww��+���q��W}�Tg��v��N�Q_m���2�~����h��~�Q�@�Q����v������=�#�	��qj�5�?5���0�]��m�S����}�����*�l�����*8	~2�`�f���!�6��NR���E4R��v�7F���������:�/���Z.��ku����Pm��?��^�z���Z���Z����~�cS�;����q�����������ju���m�7�����
��/��,���i���*X��Q�v|dQ��FQo	���:jt�4�5����h�����z8�'4������� �::;8P'����������e�X/�
�cJ���?�.>ZQ?����8��
��&a���p��NN3����y�6��
�U���b|�4*f+������zZ=����\I��b{�m�F�����_��U��W�c�8�nW1o)��N���,=�%��Jd3R��x8�M��Q��n���=�����a��;j����
��LQ�Q;��������6���7���Dp��V�F"=0xVG�T.�:exCM�\�Xm/�b�Z�{kF��-U�����p�vT����A���_}Y9;h(\�|������-��#M8Gb�3����8���aG��(�w�P��j����M<����|�h�Zj_�%;��o�{�A�@�H�����Z����s��b�F��[I�s����%�U��^<0�t��n�s�TbI����^�^���/����	���x4[��R�BC�����Q�Ro������������T�^�U^UU[U�j��x����<�4�=�V��@s�[��4.dw�G��o��Nc�F�����X�����cy���`~�3p�F�,o��R���r���/�x8�GDu�\�u��{�a�����04x�mr�(R�d)��{ipX��|����Zra4��RC��*A��W���b������
;��� }�'/NF=>}>�y��n�bA��<sg�(��8���e�p���W�7�G>��AN[������Go��b:���YC�KA�w|����G�P~��9����g�90,�M����W�/5�_g.k�u���$�-��uCw:��q.����V���c�3l+��D��	�����a�]�/�
��
V�&�'��%�e��3P\�ax��q�,��m�+;�f������i&u�_���13��e��]���VV�5;[P�����S�����G�xRaGp�9�~�$=^��UO�+�xU�h_
R��|�2ZD!O�p���]h	c�&��|�iu��(Zz���+a�0k�n1l9�v#=o��$8�E������A8���{����pN������(�e�/�j����e5e�@_Q����p�2�
��0.W��?�[u��v��C���8�%���_��<\$No1 e�G��Gv�#;w$"��]y���S�}��-q��v�+���'�;�N-`��,�%�fR��A���� �1]�p���r���d
�n�$�*�}Tu��\;:9�;;XI����@G2#��<?QX����v+�Xif��u|����u�:eU�]�7�y�y�t[���L��g��He��=���-d}grE�\����o1�a�jGu6�{��
ov�~�Y����5W�����(�hM��	;=���Fw������f0R���(���htI?���.)'��M���x\������)��/�[�6q�3���&�� ���^���O�i�����9`���%J��F����YF��6�0t������L���m����7������%x��z�&W\|�v�{���#��W|O�b:���Jo#�?U�������^u���:�!�	0(�&o�pB�_�v��y��h^e�j�;3f���?�)�����#QJt
��l���W���S����%����B���1t��Mu���V�j�������]�={�^�#��������������u&�^iEj�_|t/�6{�+�eQ�����ms0������.��=tQ�~m�=��l�]�Zmg�lw�g�V��#���M2R{�0�Ho���?W�B���j4��n�_f��������g�rtc���n�`]�T���6�7v����.�!�+���[��>��n�v����?���}������J��OTr�����������z��qi����.����M=So�v����A|I��o���0
�n�]�m���A�`�G�>6z�D]�~p���b�%�KL���y��c�]<��w|
��V����T�����5�������������KU���DeG-��������-p���y����WM���W�="K(TDm@�^<Rl��?��`��W��hx�N^pK{2��0�w�\������X
���p�������A��Z?>��������YD��L����{C�(��S999�~7�~��a8`����U�'��cx�Wak$
��5�Tf��J��<;�R���vT��8���xY;�������G�:�P��	Z��cbZ��?�T����DJV�_�H�S��� ����N��W���J��f0��&[��DU{1Z��av�6�#�����]��a7������u@���>���Wt�����k����=5���[u>�_a�#jO�#-F���N3��Q���nW��!��B�
��49�C���Aoy���@qxO�g;�!w�3z��p�nmJ7QO]�Y��,�����j<@��
�P6@�5!��;az8[/���W0�2��2V��p��;�``9�����9����/�B%F�u���o�����]�G�|n�^X���:�1�v�4_�Q�����u���[�{������lY-}��0�r���%�L�����p��	�^������Ph��Ri����^�Q�_*��%K]�/�$����lF��`����+�7��';�`8LD����X[�82��
��1R�.<��<��_^=r'��ci\����&�[z��
*U�W5�KYe�
���a�
�b��=z����q.�0�
��P	�s�.�.�X]+�k�e��!�D�h�x }1z�];H����j�E!3
�����wU����<|�H����@�A����0L�{uZ���%�me%��qb�_�%4�F<d5;����z�M�Q�C�eG���P����T*!�c@������v<0~S��2�$����h"�-�6���j����P��/+n�����\�zo����y��<�(���DP��k�C��t�v�51*�/��/�H���"�=r�����5R]������<A/$���h@
�e�z9N^a)�����e�g�C�*�\�P\!n�7~�Ub��Hh,t��@�A���^]adL��[b��W��
�����h��|��xyIM���]������e��V	�XC���|��}a����;>:��$�<���Z�a8�D���._u_����q�C������^���
� ������|O��!�Fy+�V�+��KKhv�\Y�90���EY����_wD�!�h���s-�|�����C����0V�9|-�|#�����`W��|�0/3���@�9
�'����`�~��i���Jj�&�b���{%�R������h�`W�E�kX����bi&��-)�F���D���&T����~�uI�`������?��Yo���*1��J���o<I1%�?T/��PR�����9����8����6Du~��A���e�@����J�#T{�L���I���Y�����Q����q���z�'��a���*���+?��i�x��|r7�LL-o,/9��z���P��P�U�e@���v�FM��o"�Q�������%�~�vL]y�_�0����A
��>�X_���8�o�?G�������6�eq��%���Fuk��6m���n���Z+��bQ����d�':��������z�E�U�h�V9�@@�0(v��������s�p�Q�w��h���9=��z�������n������{���#y��G"��!]�g�v�����6Q'��V��S��D�3c��Q4J��[�[��������z��N� &�htl��������P��S(_8D��Tw���y�3�������H�l�p�j�����fR��N�gr6Y#�)1���Z�t3��0d��5��9�d�61{?��W��������;L����9j,����V���$~<.�9����6�,��#���B�j7�6n�U����&�H���F��_�\�N����3�1#���z��a`���4���i�(1��f��7v�^^Y6���$*�6R�E*k�+�TSq���(�����h�Xp��j��?��Qu�2�Pg�~�dv���Zo����|q���ju^VxC��a1�$_�f�#f�Qj�}�e�������]e8 [��
Bf7��*!����d�h�H}O7L1i�BX��i���2�U`��"|���4�����i0i� ��P��K\����bAt��4�a�A!SL����3���4n����m�aRk��BG�<D�e���$�������h��

R�}{8d�����,�0�C/T�	�"}�����;��>�Q���C���o�����G�6Y_�����#l��\��.�7,�]��G�c���@D�t|Eq9h�)>G�1��D
�#�#+�FB�B���V|q)H�V� Q��u�:���
�=,K
"D�����������ap������t�3�`�������D���2�,f
���b�+c��T�������m���0n�������.�
%>����(��kj&����Kh���U���J
W�.��@���~�5�69[P�V�l	<�6�>�E� �1��DG:3�Z�C@h@%i�"����6UV/����GC���s�����s��0�0���*%_��W�F�qz���.�tf
��B���q38���yl\��6�H�]X`��(�F���)1���g'����C�G�~�*���:��HV��������v����7,K�-"�h������m���Q�h�G9:���"\��j���qq+������_�]��o����f�����z�>ehX�ec6�������K��
a�s��4��)VQf|�����H��8�������z<e:���/�^m��74�������M%T�v�A4{��N�Rb�H��x^���m��� ���1�1~�0  �~�a����[C�3�����M=]=����g?nD��������8���A6�����#��|;�a��@&;����I������)��dk&���)__*���%��.�xU�h0������W�}�Rx���M��99y��z-��:��K�%���$@A��>Fu�].�:��F(*%C���N����*��
9L@�
�;�g�T�����]~3��U	`
��'�B��\��&�[�	����^�Y_�QM�s�g�.�U�>��_���[r`�O���:��N�?�����E��gS�Kh1l�&!�yx�Vd)��'�a��=1������=�����wa�� V�0��~��F�������yPi�}��W��D=Z����z��Ro���Zi|���x�&�r�k����[^�~QX*���4��4�"����@�C��;�P�#���d
�A���Lii��g��&���� �"O��_�;c{�����nb��D\$wV;����&�DCf�~k�0n�N�:N_�;��,R�nI��!�������*��?�[r�qt�v���]
wio��J�9�=�C��:Z�_��Zw,	�-(��\�����H����4.�1����E!�
��Sl�1�W0�,�+������lO���>09��u�����f?mG���Y�x��s8A�J,���q�T����rb�����y��9[o������p���:�:�B���_}�&9��NH�b�<E�����a(�a�����n������!�{X=lV�����z�c1�^T���������R�6�5���$br{�^\��&��<�e��0��i�q-�	8���~���F�m���V��z�i�����X�����-e78�����9=0����XE�7�q��I!{�2�G�{~�M�����d��G��O	��K�.[_�6�����@�p|�����82P������G�E�
~��{~!jp�]���#O�<�E��W|~5���t����~�hh���A	�s}�����*�
�r
8.�����_m<�����|OV y����t�_D�����3����8��e�=����:��Y�1���N����3��6���=�}�|�K�y�C��v�s����l_�W���<������h�.M(B]n�^�n)��Cmd��������8[�������{m,llnM)G94���[�t{u�����W0����Z
�<:�����������v�nU6�.hj�V<����=�[3�B���&z��p69��������ul�Hl# Eq�?8@�������h&�;��if����t�t/)b���?�"o!�(/��\����&z9���r������rq&K(w;��x����Go�yj�R�������W������x�����������\�����f���x���Y�es�c�W��c����_����m,���F�������l����w�l���Y���U�:D�.9:~;�qH_S������4�@[�8��1��"��h>����j������$�6�Iu��lc����
2zh�y�V����|s^����q/�9���u��,����%_��� �Yo��8I�_���g��������)��S.��Y@�8�6��sGo.��I� �%�D7����dK�� 4S�Z'Q��R�)�������9E�)�zD���\
�L�c3Z�9��� x�&+�8O����;�������0�,Kk!>��%��&������8��*�u�dm%+��Z��8�!{8�����T��=�|��L�z�(�^�>W��z����^�vTk������=��{�HK��@]�p�����m���%>.$O���<{u�G}��\/)a�fXAz����i����4`<�a�p���C�D�oK�x	���o|�YU[!]Dh8c��g�����c+$s���8\�?�\9t��(��c��@@?d������<��x�>��u��	������e��f+�������L�+�N�C
���66]�&������+�&�_�K��!V�;�tx
��nH�C�.�\��x��2N����W�N�_�~)Qr�j��}#�}M�s,���-��&6��#�=d	T��*�8�{�%rD`l��#�5�k^�J���a[��V�4��_���a���l~�6�)��+�A�F�L~��q�Ub\���0�f�H�s��[z������o��)���yzvtT;z�8��v�������ZC����?�E����J��>S/�@�oj��=���<�$O?��_�o(&�3k�]jM���c
��	"��	�|]��������l���`���AL�t�x�����`���bh�!{*>����`�A<��T��w���
[��B|��d��,8�8��m�I� E��A9�#��pv��*���+�P~W�7xN��b�?��
�q:]�	�/m��	����0�Q 1��[��
6� 1��2k�Fw���0$x�/��Q9��
���AS��L=�kY��i�g&�&��A��O?����r]4�����G���(@�:\��5���.��~��b��M(�
�?1+�u� 7��gl�-\����8�-���"C� �<x��T���)����WI���6�\�����v�0v��d���'�(�8��d�|�����&���S&��$*M���b���D4���I������"��B�-7��;d�n[B�O�D\'j��*���^�Rz(D?8R��J�f2���m3�$�5W��43,4�?��y�'uk���d$5��������� �8�=�z��)Cnp��i�Fm��j�;2���)�d05�b2��8�����a�%��jK{&-��=��Vr�P��w�����]�E6�z�N����eVb�1�o��
�!�5�$�t������#M00z
��������HN�O��G�.�u��`Yxnv�����^�b�H�v��A��d����hQ:������d�4D������O�#(f`�[I�B���2�Viq���!$w#�D�����]���q�$�`�����4�����I�q��V�m
u4�P;q����L�I�
��z����'F!�Q(yHNQ2����EG�'4��Vh�����F��$����f����qs�ij�a����F�q��N�}��?\@K0�y��n�L������_�����S
F5f8��5=W��<�XCP�.��
V�8Wb�c4h������ZL`�S�fT�H��/BQ3Z���A��x~����"�A��?�vY������A�{QkK��qCI
�N?d�Ta@��q���Z���5/a����Fqs�oVR�Xq
,h���W0��}�A���"e,G)!��L�(c�����5;=��4P�z[}�>|�{�*�3�f7�U1��Z�~����%���tX}�dN)�'f���� X0[���X�6nbx�w�_m��Vg����q�Y?;99
�o���9Y�<Z�	�Ad��e����01H����p���8a��&�����Z���T���]e4����B��'��-�A'���D[��{�%�
9=���BBAf7��x����?����Nj�X,����������q#c��eRKL,�,���Mr��
�
�
��2��s� .7eLq.@�oC��|�a%���LS��:����y�1P���_������>�X�c)'�<��7-6��jQ�0���-T�bz��6��!�����')����36&�?���?2~�$�����}��Wp���m���������)=w�/Ey|��A����D#S!Z���;��-8��n���0�=#����F��AD��5
h���������p���gc,��x�$���0E
M����j���Bo�QE��O#��C���Fj��|�\����A��:�`�������qv�c�6���Cw�m��;J�Y�fN?2������^7�/���}�a���b �D����@�"��G��K�f�X����r�29�Ez�~�c��w���?s���>�e41rN�$�,����,�$M�z�5{h/�I)��]+f,$
���+���n���fWSU��Q�\���C�d}�����|Eu�OS����v4��|�q��A�������bI�
X�k���-i�SwnF����kY&�O������8�������X��9���r�|�b,��hY����C�sn9���T�BN�)XE����[����o\��Q`��8��i
���I���s�����]O8�O@t��wPRXs�a������u��,���m�qb�J{d��:B�w��`p�Pi,QG����h�,�M4�O�P��aZ}iAd3i�I@E�rf�,��o�z���x*i�PV��X�>4�C�s@�n��?s�+���1�/��D�'\W[�����&�,���5����I#%^9���7_r��������F�Z8Y��������&�=�Rs��*r����^����Q�z��X���;s������UzHt*�pA�#`������-���������s�����h�8dPpt	���&����N����H�<��mf��rGi��>8��+�.���q��A�������%;��������IOP��-k�`��?������o��,:b���������������>V�_Lw-���k���g33��4D~��T�r*���}��l��8k��-���f
�2��o4$���.��[�(,5`>�w+��cY!|�L1�p��T���j����H���R�#L����K^to�$�w�C!��(��m��%�;;=�5~�*(�2��a��Hi(U�,-K�1e�(����?��\Vz�pK���OQNPoE��`�~�F����^����������#	�K��.�{2r��������uo�
?�{@����A.&�-s2H�1Fj��b���CS���0u���f���}����$�0L�O^�,J�Y9������8;Q�u�q���G��4�eL
�>�b;�{���fcey��X3T�._,�2r�B��*�����
����^0�/a|H���X�0�����_��3
?�6�9�,i�;o
fZ�k��"�6�a����-�X"�b�3���IMl�Q��}�O�9|���^���'��c�}�w}�����,���l7�����F�S�Mk	ev���h�w�3��'&3�"������b|�G�m����5���a�&��c���wT���WuYOi:�������N(!��[o��Hd�$��+3�O.#&�h����"�MQ�R�{�uSudh:�G�1��c��C�R���$%�+T&1����hms��=�L����D��h+����Z�5_d��dJy�?,x�Ye����5f���,�J�� ���7��0��TNSMJ���Z|la�CI�	���6���)1�CuE���%����w�V������?��T1�E8?vZR/�v8���iA�������<���?H�9T�0+|Qq��+)�v��H����I��(y��#��H�"�lr�%�[I~��	���6?}$1�k�)��W�R��<�R�^r���R�`���A0���&����!�LK�I���Y���&���3`��*���������x������9����I�Z\��6:�<�F�P����MG��$M���b#��P�w:�����a7��5����c�.G�%��0��K1�����};���OV ��o�rK� ���� {p�x����up�"�w���(:��e�QX����~Y=|#<�kP<�J�b�^�i���RX������c6�O�F��'A��@1����=��Z��a��&�����yLS���U�]�0UC���;$���������7y!-�*���tZ8�� N�����+��,�����2~N)A�$�G������D�!��i�|�8�����u{~a�+Qa�,�2����x�[������~��rZ9�bT>�}Vu���D���S��F��s���8 �6R��
����m�����Y����+L�cW^co,����c�&>����}5(��%��j���~N	������G�L����uQ�i�Rh�k��"������P?-���#�.�
�d��L�����V��G'�BY=�~V�����S)R����q?�,��Tm�c�'�o~�T
�dR8�:��aj6	���(���k�'7�����hX���BNb��U��/~�J������H�a����hI=��/M��$1�
����V
_|�����]{7���h"f�Q<uZ|�~�|R��_�������|�|Y15a��F�����-�� :t����@��x�4	�S��k[�WBWs�?�
cW����d'�q����';ORs�	n�-�Lw��v����B����P��dV�5��L�xM+���ZU4xW��F����5�����
w4Js��q����.�������_�=������%Wn����	];*F�#�*���F��QV��uh�PscrdS�9E���co�\^������E�%��bdB�P~
Bk��6�!+��?����2�mZ:��=T;1Y�zd�l���C=�8��	���NtU��+��%�A#���|���h_�����1������DZ��
�(�Q�$��d�$��d�g@13
��~����0�zQq��5X����7X�v�p��a��^�?��������,�?�l������Q��.���q�y�B�����.��b>�
a�;!�rRY�<����(�����jg�k����Nu��=$�oP_���~��sl�G�<�[�g;R�����v2�	q�������po�=����y��c��Wg�5�� �5o�d��+�r��_���1xLy5�s���v��9��d���Y#*���7� �j��w��g�����3=x$f����%��������
Eo�#�hL��������z6���U[]e|�"�z��o����c]to�t�k�Gm�G�:T�W&�w��a�x47�0C���Hu�)������������������N7D�{]bB2�f�8i���{�h��H�|j�8&
��B�.��v�X=��au�V$��o|�&�p�U�'��y���-����T��z�<l�_�m�.��En�_��X�1k�����������z��
��,�����h�P��"��,}��A,�i�	Iq&�\���c��]�bH�:��b	e�����Z��i���N�Tf��O�����]���_	��A����#���l�3$	����:�o�����D�Zz�e����Eq���G�(�s�N����q�g2� ���f�R��5��&���h�h�fjf������I���w5IM��������er�Y���[���h��x�2�������6f�3�HuK_��[��}����7!��v�X�����`0�4/��AV�<;����Un!�:&p!c��	�@������RN��,�2-]:���H}H��[��?��;d0B�h����:����<)���*��\�uo�`X���9�pD�B"��8M�|�����w��
���_"��W�la�'9*n5��A��=����E�,r��>�BJ��p�����>��r{8��xa���h���=��0��FM!�e����<��`���J�gJ���?M�c=��iK��sL���KA����I���?h�����[�xL��#���pfmt����z�9P+�-��O�M��3:��'y�d�*��=x�;�m-�y�+�jnT~��������<��_���=�����_��M����&3}�����T�yqR����a��,9s-��X�I���eB.?T,p(y���_Y�r��-�Q���~m�4��	W��?��Q3��#v����:<LVm��j����W^Vm���j����IjVm��Og'��Z�W^����f��W����,8��D��@��\���2���x���Q�(
�n"5���o��
�5(����-���7V������@�#uP?J�CQvA:�W\�~���segm�oB6�����U'�_k]�����������(`*`�w����(���;�RG �1�pbJh����~�)��LO37nX2�k�:��9��i����j���[��V=�&��F��23=c�$��u �R�QsA��V4�0���f�NIR�I�7�d|����#������w-��p27*$�}S��r��0���GTz��Ot����8�������U;��Ppp��(���Ej�������cO���U����p$h��/Z��A�h�r[(NFK�-S�b��k��	gf����X%�;���r@�(��t�M�
sG��p
�	�r���a�&�t�:������i��$��>!�u���t3l0=��n!"�o�;�X�[�f����hSq�����@h��f���h�=M8������f�Z9�hVn!��G��,�9$����`tSJ����:�{���w[7������,���EK
��a�
oek'N?�h���Fo`���k�/#Ul�U��w|G�W�1w��z
(�fbA��I/�V��Arj�������R��'��
�����zH����d7�m���d
:B����G7rj�z�^�I��C����*?G�6���x~�����G�Ss����zS}������T�����u����y�Y��������Oi�����D���TA����5�
����9��,���ce���_v�S�{i�fz����+�ax/��r"�7�-0)4�Sa#��r��wM�[v0��LS��a<�d*"�\:����}r[�pj���n����|%����a�����?��gH�H8����z.��!��,���.�|"��cB%m�����Z�;.O�4+`
,���^����J�k��Scb�u������� 6~�Ya�������,������	=�Ij,	�����1�E����TC�5��H��o���=�~0a�h4t.�A�Aq�<QS����h������S������������{(r7_Vjg�9���T�c�,��	G����.�E�%Zq��	
�������8iZ�Q�o2�l��uOE��~�A����Oc3�!
�E�t�����@�1�����]2H�]�y�����c���cV��8">�r�7���:�Z'������C$��t����l�z��I��x�y�J�Uc����<���MO��TK~;K�������z�_V����Q����S��g���������U0|��t�A53' bW�W��ZyM��h��������F���uA����;�sz[�bH�p8�T����e.��zu�pPZQ�f�R ����q��vH�P�RTW��y���GZ��(w��j#/FA����4��L0�_xu
G3�>�>�<�W���7�m��?C+b��&�j�	�./0I��L�? �nR�D6�{��{���0�����������\��W^�������
N��}�6�w�������	���%�7j����;9�7+u����V�zm�`7�����/�k*�!cX�P����V4� F]T��@GG=|�
��V�/Y�L�1/>��9C��yQ������a[��8\%���[2J�� ��c<����Wa�Gl:����M�X&���X5E4%vc��` pC��.�o����	]��|Z>��R�R��M<�`lW7��=x��	{W�rQ���2	c�������������$��V��c��;��k��1+��R7b������?�uo���#)\�^x�J	��8�rgPn+�a��u�\?2��]g���d�
��~!^��=W���1�`dv����-?Z�0��S�Bb�O�U�����@Z&��tV�l��w~J��[����;%]mb��7*)ZHQ���*3�ML�,�;�	
9	:3�I� �����O����������!%�d�^��7����N�+��M[������~&"���ys����
H[��q%?,^���`L�����n9�!�s�\����-�HN=�V
��,��-m���;%	�9���8��)iO����^O��-�-$�3R��;�,Tz�}������a��L�d�����5�uD�1�]�3n���ds��Ls���Mj��nQ�F�0�$�r�����l���\O�E	��ww����+[b?����f��B6��0D�')e���QD��t�Wj��6<��U���x���	m�(���,�y�&+�4@������y3�]L*���� ��`8��N���-���2O,��5������n�,�j��;]����;^j��2�+Oj��������7%�Y�3c���>�s���
�� 6��y8��kX�!��^�q="nI �A�$�Fcce��8T@�P&�������n�~��[������"��,N�����MC�KX��?�8A=
*������Gn���O���8�f�d��l��N��i���������V��IB������.���^��p�<��FpA�NP���:�K���^�2}R2H�f	-�(#8N0[��%����o�7q?��\6�����n�!�\_���6�Hj��R����i0�x�1 �,�K;�@n��:z�L��F����t�� 8,��q�J��K�����>���z;'�+����<�8&[����|��'����#��*���<�D�����,��W:���y��F�1��-��f@���C(�Y-u�JvW���0\(<j����=BmO���J����q�qS_b�7�'����^�=Q?n$�K�	Y�9/�i�����:2�~�8)Q5�iX7�sz�F,��#@]Z�9sG�����eU,w�x�g����f��y���_�f�C�o�V���!h�3����+��M�^�G�rs��YG�.V�y�p��Q|����?/%P���u4'8��>H��[�yWM��p��
y���C�W�29�}B�f�!���a+�h�9�c1��.�B��q|s�=z�)vWO4�rYT�m�������XpE��d%�g�4�c�lQk��(�SC�0�D3�
F���s�P	�2<mhs,v�F:��y����s6�I���&^9�Q�s�'7id+~��
��s�Zv��5H��q�R&VK�����H���E��rX�\��J�n�3
{��9Nq@��i���8Z#y[,G������%�k;���1���`X��� {daa6R�m
���`������eaBx-g,�r(H��7<5��D��	�v����$ ����!,���"(B�����J~b��� CXQ8e)gLf�\��Jh�Bo�V�M�Z�-v�ML�<�6Y���uA�9������M�o������8�9Z��J�:v"������F��U���m���eV`��q�Q�� �
���b�
+���m�4��57W*88��`p��r)�����tX�{�,b�/Y~���DI�
����0e��nu~_���J��_�h,�ET�z�h*~�P��6�+�9��Fo���uY�����wpV��P��Gn� Mx���kt�Z'A�3�/2�)yA@ #�\���y.W�����5�Q���4������-�������P]z?;��R�z�������q:�,$@����1��������U
�A��u�b�d���������h;������P�]�s���;�'^������������a���RX�����>%2a�iv� ��K����y��.O�Y�����F��R�����p����&�����>z�m�'B��|;��!�Q�V�/M��m�Pn���-���nR��|������mh���K���	���;>��n�n4K"�"vc"��8~ZFa��i���YrY�4���b%��H���4����Db�B�5��
�b4E���Z�Hs��R�Y���zGn\�1�����^������w�S:@���V_"I$�\��^���cZ��:Y\:^��@D�8���X
������G#z
���xZ�i-�	��@H�w���J�w�6i4ln��x���Di3��9���ID�>���1���.yK���h|X�
9�";���q��������
G5�Q-�����x���o+�%�b�18����r�=As����(q{�>�<*��=�r���X��7��UR�p���U��vGx3]��L�K-�
~��%�*+��]`�z���q_g���:����6		����+2����\E�J����^H�0F��]@y������NT����e)I4�/#JN�� �3�\/n�#���)��G�k���xc�c�
��>Nv��e�ycOT�f�iG��������03q1��~nj�}����w������5^5��,E��}?�I|;�c_rw�V�h|�^��$�\j�I���pc$G�b9�.�S�2�P�T���Z+^Qn���-"%El��%��H���C9�EZ�%u~1�������n��t�Z��K6a�Q�w�)(O�z,��Td&�w�DsK%��M��H�n����*���c����SEe.2���n�e�����`�Cw'�m�:�
t��v��3BG�;2��Y�������H!���\l��2P���,L��G���KN���Kl��3�P��<l(�HB��G,�1���M|-A��TN��>,}��uC�K������y��,lQ��n;����>W���8���"�,��~���0��u��d�k��*b���
�i�k�����.�4]�=�0T���o��/�<��k���W,\fmJ'����e��Nw%�4&���H�VY�2&� �si+�,?M#.�o����o��b<��M������rG�{�WuX�x����b�0�P��Q�Yi����00�ss����d��3�Z�
p{�E9����U��"^H����Z�l��?��^���j�?kk�_?�
:����U���y���=zT���[����������������}}��kf��ba��>��vwB�"BzG;���C��M�h�>$���^Q��;p �����{����w��;�����_W80���>j�^�d�N���
r����+Tc��^�;��]�����%Dl��������WQ�:WQ/��Q@���/6G�f�5&h�r�aZN�S���s���}��N��XI� ���c?�h��C�3�I��ap>n���.�9/����r<��>�X+�~����n|��0�-:�9Zo��"�&58�}t��2(JY53�@��K�$�CO]sZ��PmVNN~�_�������@��A���9�@�t4-�X��e��,E8I����f�4��1���/I�J���q^w���/V��`�/C�l�������zJ��t�d�l��:M"�MX�^FS�3�)��IJ��N-c���������Ui���}��@5h��
��~���O����
�������"%b���]�d�<�&l������m4Uq�j���X�W���a�v�_���zP)�gJ	�	P�|�����9�0�������|Q|�<���������9�Gh�T���b��u��+�>-���s1������Z�U�I#f�P���n
���U+��t��x�%���.���Q�G��9q�}t\E���4� c��>�OypC���^$�y�6���}�xF��pn�_��oN��Dt�Xe|�����4
�x�����V�:�#R��Q�Kc�*�L4��W����2Wk^��%��Y���=���F	v}���.nx)=_�v-���%��N$d���Q�������b���F��Sg��6��@�@|
l0+��(�c�d
Y�H@M��7��5���j�,�#��_���	(�E*��nt[�n�g&��K������E�U���`*ET/Y���8tQ��Q���u-~�<��z��f���L����v�!����9�e���	h7�kY��@����]�j��H�	�s^��|M����\Q ��Hn)H�N/#���r�k'`@Y�����T��Q��5�G)]�1
�������x�F��!
;v~�m��T�|��`�x~S���#��OJ>����}�n�a0�O�|�����z����Z����ptc���
+��B�9/2���'L�4��K;&����[A��a=�7��$�S���������r��;KX��^*��	] �'n�%k��������6���l����P�*��[�{���������Kr*�1�����������g�-����9^^Z}�<�3dE$���2����������koA�ZQ�b���gJ�o���
u������m�v�E����?4~lj#u�t�'o��}�@"y���yq�p��� �k@{�r�>�m��������M���&p�<k��JX���g����Xq��"���V(dV�e�s�P�qlN��|���[I�)3�H�F;0��y�$c!<|��N�(������G�BuE���$Y'��Ca]����4�z=�D*���%q�'��:l��C6#���f��!�@���n�~Y�L���~���i�|�6���5PR�~��f�zZ���_%+��������e�}JH�}��{�=����Ex�)"���%��04>�H�~#�:�0�r �\�� ���������j���6q�j".Q��/��z%�p=8����jpZ�J�����9���n�HG���.pN
:A�,�44���)��s4���+���.�wB@e�r@NB
��^*�������i�Y�d�k�o$};�D;�m��2�X���G[�P�M��4V�����2y
%����PM6Rb��-U��$�aH��W~M�F���rG��
�M��m,���o�����m�,����pfH��	���`���.��oeu����mwHo�m�'�!NjA����0�>���<0�(	W�8�E`�wC
@��I�����6LU����?�>��5 (+�Ff8�b�M����1�]����h�n?Kz^G�k�6����3����m)�]T�y�z���8�����B�)�J$��#a�6`�1_�x��
k*���k�'9��'4>�y�h���#���Ke
���(���-��
Z[�t����*6�9T��UF�������}����������EL�M%z������R}���L"����{��������H����������6������-�f�$���i%������;���7���%�e�%�r:w}z9F��M5�x���)yP5}�$��$p��QOGu�`��F���i��4>�[���Xdi���%�fD��Y�uQC��i�����B�z���h�E�.N���Mn"�9+���8��v��Hj/��_8�I'��|�����T���X�o�J`G�3��p@g5�\(0��Q���j���1g��l���M�(E&j|�D}�AL�&�@Zr�I�a(�8��Y��7�/iNKV_�d.k�mt.*�6`�5yv���f<xrZ;�������������N��J�~Q�z��K�+�=h]��r�EAg�F��s��Z�7%�N@�4�����z)�V����'z���o�{�5_��Y���I��z

g�v��"h�Y�3B����o���%o�v��(9���_��t�]��pE��L�����Q��^�=�
�yF�X�l^���S�Z���<����|����W�iS�+3�����I���vDf�	S��B��3R,�s]�y����Li9Do+�p3���6I�E3L��u#L�XM�n��:�����"	?�<�U
5�\7��<C!>Ct"��8~���0h�����yy��4������e���D�VzD�L��{�"�4&E��%�wnPa�����=a�fe��@��������{$��PeN%��N�ZrK��}����R��X8!)H:<�2|8���M,�^��]�,3u_���'�$r�X���{� �a�~���-�#��=�]!=�,�e��A�is�7��t�SF����v�����LV���ock�����.���;�KIz���M��������G��&�����bl���8�c]ER.�T�4����*���Z1O��%��_PJ[%l��=��nC4��)J4�WZ�}����M�KC�&����Q�P�8_�B��$�J�.��N�)���������7M�A��=�������g����e/�!&��3,��	���?���3Fdp�|�{��������O3�B���#�v=�����:c
�������dh����A?K~r�w���:�"R��&}b�
o�b4�^5�e�d�)w�i<����_��ex�]@p������
]V�8�P.�-SzO��w��D�i[1]����`�F�-^��"��vE�� ��b�sF�Z��@�L�m��F��VO����o������A��h2am�X;�sK)
�b��(|���e��L���6��<n.�H�L�����cH��f���e�t�DB�����r�9�'�����6 ����sq�b'���4j
Hg�!���J�������#���qIm��5�%J)b��i�+��o		L�s���������g������,c\�T���si7]Wp���uE��?�rR���� �.W��+q�����GA�����3���Kat��8�_��s�����$���dw�����m��o�5<n������������!k��%X!�eG4��.Ee%U���bK����-	�.M��m8%�-O�����v;x�.��iH�hQ��������x�V�Z���3��~I)T�������S�P�kN������0#Oa��S��1*����5����Q��A���5"�o��=V#����X)Y~2�p��Xv$��	q*��k��T����T����.��&.,?�\���j�]��
�l��'r*���2�-R%���}���F~cw��m�a�d��a��q�Na
G+0��P��=Ea�4[��Mi-��L:��zMj;��U.q�9�5�������	,�*t�D=�@����:�%Oo�H���_6���eQ���B.�_w������],/]���?���%{5�HjA��{��zW�����V��:�;x����o�m��R��u��H�NN��O/�������y�->B����W�x�D|�l���S�0�y�{�wN�nr�fHYj�r�+��d������IEKv�����\�r,��!��&�;)����3�%�S+�o<�3�
������\1�;)��� ���X{-L�Ot�q�g25{_!���va�6�q�V#mg�#qMQJ/����e6�N��D9a����:��Ow�Y��n����pj�	,R��*�z��dfF�:pX�'���2�%,���0�&��/���
G�!���i�#C�3:�u>T��c��M�[-~��=Fw�+�/U����r,iZ���2�q#�:�qd�+���Aj�RX5�L���/�����h�B����X��R�$�
G%L�4������rX����I��-Y�����x4�~�X���7/kU����f��a�����������nhV%����.)��I��3�B����s$��D�@����d��q8�I�dzz��]_�i���pi���<�2���'`Sw�1�[��������s��z������m���ZM�$����_��KVSU���L�,&�(|�v�~]�2�uI���d!j�^����N��@
,
�VVL���������_y���1�l�/�w�}�={����Y������cE�u�(���p�M��\,7��lr�z#`t���9,��MO�;�����!��N�������_�ln��T����?�`M��#�c�GT��x��x�����H�/���4?�������8M!��l�j6����G���}���d�|v�w�q�g��U^�	����(�(	������������B��$�E��
�u��[&�V�
^&��(����o��~K���^�+��������K6������lA�@�B�?.�d�,S�}�Z,D���A����i���IX���/��0�;����'�C�D�'�c�D2���9�`8n2��M�q#��H�1�@���I1J3��_�OB����.�Z�����������������ks_J��1�KQ�Q��~�(�'��/����;�v������k�Ga�i�x�M.���] �T�)!�g�$��=o1(���V�a�L4Y����Z�N����`��A��f�8��9A�YM��1�$�[�i�cI|���[�tE��b���a�1�,`f4w��B�y����a����
0�j����3�ly�a���uS@�:&���H��!2�3�%5;�S.�0�>��oD,��k�F����t��l����@4�U�M����(5�#)��q`����������_o�U��7�M	mP���e�N�T�<�R�voN(#T�%�4XY��]O9C�"!P�G�����%w���WuO;��qI��\����a�!T{Um@��Q��ZZg��d�e���������u������d�4���|���2����Ut���YG���H�rQ@?f=�)����<���T�����7Nck�1�
�t�K��G�*1���Vy(����,4F 
GGG5_����0f�W�d
J����W_V����I���ev
Y�����C��8QN(!y.'����)�}��p]/����vT?>"��{�13��H~���D
�=/a�L���KQ�������Q�����%�a�oR�#�:���o<P��v��m�J�/(K���-�T�`"���LI���m���-���������H:��@��R�:)�BY}�[w��T-���BV/��C�4���T���	a�|8����
�cs������
.�����0*Z�	T���G���:�1q_������
'��r=~���=�e����'�����g�z�Q�<������ij��fF�,F#�3�!��9�wl��m��$]*�����#;t4�������hXd'n��E��E�(��i�^���zv����b�,��c���J��(�k��xx?��;q"��Y��D��(q�8�Z�P��29+)"7�a���?2OU��{�d�Y:��
��Y�/�F�DFl�nSG�I�sL���>�=N�X��rZ�������V���Wc�htIlL9s(���n�����aB������n_B~�u���[������ ���������������sSM��:���@��*�����L.�������=��	���d`�6������o��j��T�Y�����������������:"Gg���_����[��8
^���5&�i�ms�J��w:x��t�.�tZ��(R��~��vLo^K���sR��PbX���,w�%��
d�*��r��t��c�Pt�G��}���{+Mk
_d�{T;�53��I�)�m=��)���#+�j)U1��
WR�@w�PA�	%��������x�W�~!��G&��^j��21g��f������g�+xe���&�'i���{�k����s���j����0�H�9��p�a�����Q�B`�Jng���5el;�bs�Tn�-9S
K�����K_���<����.��C��|5�e����v�E��p#�	?��c�V�]��#� s#ec���������������#Jls�� *��v�rZl���+�������b\m'��2j=4�@v�rD9A���	7�N�?���T{%DkO�@mT���������35���x�%k�!^��-�q����	q���s���MM��#GG����������U��Uy��'�&c����"�,��Z�W~x���k2A	�k%B�yP�2��{)�0p�Kq��6������Cq�hC[0a�"�p�G����"��
uL���	C-{�
���SZ�X&���X���#����D���s�����.����_�)���������gEWM�t���uH��^�M��k�8�t��ttJP+�h�Q)�i0�M���p6v�>�4��EK�Ux�RjY{�(<S5<;�.	����9�Iz�����K]>'#Z=���.g�n�cJ�^E�<Qz���3n�����]=���,���������I#guRj
�
��'��s�!@��E.&��<�$�xJ�F'�	��da�q6~qF��g�@��"�[	���R�'[S��s����f����:J2�4��,�Qmz�L�_���@����DZ��p�?R���E�(��Y�VM��n�U:�dL� 4�)E_�R�W�X�C�>��;_���M�2a���K9��J&z"-�*����7����QGj6�Q��
m�t�_M��9�]��iL�o.n%&�R�[���P�W���%q-!Gb�����I�k�,%�����E�|-xb���<�7����X�?)�'�=��3�[E�w:���D�c���("�=�������l�v���VsT�;8��~�r_:�c%B�j�p�;u���cD�$m���q�XJ&���0~��
���z�U�zz�����&'=pgc�"�!*�(�Q������,�m�rk���dD�Xd�X�I���3��P���LoKh��B�=@������^(��9��������z�����F��z|�(�������!�m0����
�&�c��T��N��E��m�v���.��l��p#��l`��X�����1��(�(p�oB���k��,��?���0�PX�\����&�;���t��@�L�\�4��(9��yf:8-F:E
�i�{�n2z���c���v���������W������9`+<j�0<���^D�l��0���F���h&h�IJ1=L�t��+4]X	?����Y������H\���ige�8C��o��t�CK�b�m���C���g���$��������h���������m1~��i��T��MO�d%9D3�\���?��5��q�����r}��'J�{�>�-w�?M�\�gi,�zRW��n�wynp�?��+�N�����!3���
[jM�����3����r���_��P<���#��-�)�=��"dH����uEu����s��J�LR�G�J"����v�e(2��M^�|�w�;m����n������=�Z�H ?�E$�\��������gE����IQP��Q)���E'w�{9���o�����1Y��`0���X�����m��B��e����]EQQ�|f��R}���OWLc���)��L�y�P�E�&��pl�zF��[.!��J`bCE1"�@�$�P�&���;S��B�Q�%-��W]�6�3��|�b���/��0���Qu�7f�u���rX�3�,���B{O�c5�����)`�e�����:�77%g����iw���0}�C����%j�51BM�T����I�b�������8}���w#Q�(H��j�������9�8MvOqo��I/�}I?J���L����v���4.�K��:��l��f���}�H����~����R�i���9�B�kA~�[L�i��v����4*�^�NS_����f���_�^t:�J�2��sa���4`(���2��� 	�@m�~&�{OS�/�����?�[�y�	���u��F �7j�r����8��%�>���R��������0������r�t[o���I0��L��N�ot�������v�-RRd3�81y�)�z����1�����n��B��rGRuRTr��B��#��M���w[a����49��m�k[
��&���w��O�N����u.�)e�������u�����e��%�<v����{�At��%9
M��Yg�$�ZE�e+t�L?D&�!�#D�yo���96�����7���������f���H$�q��D@o%��;�<t��$�!��{�M����7N��`���j��,�.�~F54+��A�*0]E=���:o����J������~�V�������.���*D��iRE��DV&�Q!�t:OT��_�v��%Lfr��|��@�����
��;h���,����@EA%���:oKy
1���>����h<��_�cYq|o��M?��!�z��
B����������M��C��_�������;�_�~��9���'L��li����h���\��3�J0[����q�!Z�R
���c���"����f
�zY� � �������i��o,n����4��MKs@t%0a��`���A%�v�"�OF��
%3	;����������!��0�8��>�+�6�L���p����
�8�H-�_5��$��Y�5�L��c�)��@�0�o�b�H:*}I5?&�\&xK�8�*s����wP��~{;j�bW����W��+kyx�~�k���8�v4�)�4���C��������YtD"��������R;�<�=HN��*�d����ctX��u�K��;�B�G��R�l|���w���5�}�l���A��� %w!2&f�cM4��C6IM"�[0F~�fO����Gj�o��$����]�"�	������9I��-��p�0k�a�TV�&�0?���Q,�=�H.$n�7�o�N�
HB
�	�@�bQ�Rv#������Ef2*'�f��y9W^b�i��i�F������x��K����J//��_���A��C�uSl�x����	�E�O����r���8F����� ��a��k�[���VV;�y�Q���&��0?�?��J4�2���MQ�~���'��!���!��x�6�~�%���;�\2
��_����(rh�������d��W�d��,L��� %y�o����`���oo��e�����[�yI�*�qi��L�<�
/S�.b��$_�T�b)K���S���-�1c?JZ��T%���mN�7�,��������[G4��.i���4J��
�TC��Y��l\[
����������/\{V��V�qQ����f&E-;��4{�9M��vp���U�����A�Wq{b���q��2�4M�@���'}r*�v�|�t6�4b��s\��w=�l��v4	/oo0On1�d{�cy���^�3)M�^&r��&L�E�)h��t���c3s�|.f����+��>'+�'��������(��]����3)(hV����1B}�y���=/XK��� 3�x�M`�j��������~����'�S@�F��	����n|E�����
}�8F���YwC�:b�����b������+��`�i�4��f^C��o�����&o�tC�y
M����m����r*���>+=����
�o2�	��������-��ze��&��"�X���b��"�N�@�p�z��}	�&�����TMB�<dE��{R#k
GC��}�
;c�c�Gd&#�c*'�r���n���U|�&���s�;������%�,��I��>�@�=�?�`��kt��2As�|f(��>���1����j�����Tw�������lct�0�#`G=�*���7�W=�&
������������j?,��`M�b
{����Z���%���9���LI��=�A��b��{���b�W��,��g�����K����3|O�y��:k����i`]��",9*n����*g�X�:%$]}V=��6���0�g�����z�2����U�{�B0�X��.��s@!���W-c��N������|:�P�L���N����# �vv~,�5�B����Q~����mw�q��%���P*v��L{�g�ob��y`�^3�s����oq��C���s�V����T�*~�������_��J|C�����0g��g���=i������Y�M��_5@i�8n
E��/�������_���Z��%�m��u������Q�c��<_s�!�D����	<�J�C&p{8�Q<�a���\��/�ge�n��S����k��+����% ���hV�15�i
�t��eYx�Z�ax�&������me�.�Jc(7t_�]u�_�`B�u�N���W�N�,I�W�Q?�G���I`y�E�Nd��i��l��/%�5������71W9�t�F�hho����Q�T��u�����A����"0��"��/�8��Z����9[�b�L\����&�N�����O���2���A�dj/������k5+s%x"���)��t��aI�+���}��Y������k�b���6�����d�A�����;E�2Ge�I��dRP�i�M�����2�n�<���`���(9�i�������~�;jJ�Q�����6d0)Lc��F�P�k:����<�����:��7�r�l�T:���4UrR:����oX*IH�+���K��8�~u�wO�I�A��Nux8���=�l5<*��q>��Y��<
;�E����Xk������}���%8�$7���Y��j�L?��z�����B0�f�![��P�O�gf+������������^���>��������G8����S����"��� w��7/?��|�������4d�.��|Lq��/�� ��p����%�6���2���Y�'������s��V}��#wR~�I�iB.���[�j�������������?w�m	�S�����!v*��8+�=8��p�!��A��My��Q����=jBa1��w�m�N}L��m�H�-������q�G��Z����~z�Q�)|���q����Q��
������U8�N
d��F��������U����QF?]sI�9�"�q�-9tJ�;����i~9��%B�����U�gj��{���|D���E{O�c-��W�}�&�\w
��D�������p����#�v2#�$��}���YLi�9�;�KW�)�2����)��1J��������Au�Y�7��W���b�:�
A�y������O��������cB3��
p$�.���Z|�F�T��G���������)N�]�h��������G+"�&�����w_V�	m�{�F���A�	0�)8�>KY�T0�>`�n�i�7	���d�|Iy���"�~Q�-�������l�ug�@�6`fU�=��J�)�(��}!���.��-�$yY)X�hp����-wIM��Z�����Yu�-mn��4���()d��>%���3���W�D���V��,���4�M�����,N�����
��
�3qO��0(Q�(D
q��������
���t�\����������?b`���)t�4��T��b2��aX&����hSH��G���
�b\�c���f�Lg���I���@o��a�0�����
���Q6d?��P{^2�KOC��8�\���O$� l1�:�R��
������9������L[��K�������st[��0����tf�$���q�wZE�����J�7�Pw��u�y��$�3���!���j�E���<1�{��q>%��E�VP�G��6����F���>C7�+"��������r���/��|�u4P��
�0	g��[��'�tA:j��Y�+�Q�]�d
�!a��	)�l�? '�9����
��(�JX�`
�V$D�
L�H��s�j*
 �)gtT��6��9���ub���R�sX �=��?72NN�+p�$���\{���Zj���@�AJ����$�����u4/oDKF�������c4:���f��%C$�w�Qu�vz��}�n�W����OW�����/9Bq���c���l����^�|"����IK���	�j_
y/D�VZ�W�{
�o��Z����0�+����6GZzyz|��Z	f����`M��V�*?~[=���H+�\-A^^��N�H�n`�����{�]�G�vf��ZM
�;����%����,���UU��I��V$���G���D�I|�`��%.�s+�c2"�bt�������D,
�uU���8���>-���x@
�~O�_��<������(��5�M�K_���i@�����o^gl
�`����Vh�I�Ici��>�BNsn���R������?�}���m��2h)�!~��3sv8`��=s����^��^�
��x��r{�.oZ4�Sa��^o"�i����4�*���~U�i��<���v'+��4��vxD=	���2������Y��:����~��z��r��h����f�'�/�����e�'�����tL@K�
0{�`�����y,�����Z�e|(-�z+�,���������*�B�dM�1���f:��k:���7��9:n���.p`��Aw<��D����`8����75E�<�V>NT�X�r���[)����G��=��#+v��n'���Xc�K�k�.��8��������H�[L��>��p@�_����tg�;��B!������D`6e��JY���O�1
�a����:86q�!������U���G�2���Q���i��T����n�dP�	�b��\��)���6����v��(c�>/|h���)��M*	�K�\�voK�Wg�%�|�pI�T:{�����9�uZ�\�L����dJ��De/�����\�m�����t��gE
0fS{��u1��A�H`����s/����:5��:�X�����U���L�l��Kc�V�-�gR�0-.�8*�Pi;�s�t��gZq���M�l����o���s/�Cw:��t����:9�GQ��!k�����R��E�������Qs�����-5#Z�#��e������t�kw�m�7�3p�M�DVF;��G���:+g�X���*8*��+�^V�B{
J�u%J��-K�3+8�eK�h_���0��p��A���x�����X^�����`n�PqF�J�y����\n�MHj�|?�5�v�$�a:d�*��j{�m/����H_M`������f�y4����*x�29�����V~b]P����{$��1?�Z]��F*x��< �M��[�����U�H�E�r��	Coc}����b�j�?kk�N�����U���y�'���G�
��������z�Q�Z�P��7����J���:������%�H�������)}Y	�������:����a���������U�"�W��x`����KXE\�D�����{��cD��a�)�A�k�K�iU;���h@�CY=Zs������!���}�U���?a
�I�������� vG�E�������lZ�{t��
QG�Z(�������p��3���Bb"����+��
���\r��0�N^���D�����D�T}���c�4@�p�z�p����H��n�9�'���]���A�29:~�$}�|9�9��&
_E(D_�����?8���Q<i�A�"����(��4s���jG/�:n�2��Y�L��<`��Q�����xq��[xW^�<�J��K6v9a��������.��CI���_7aWa���O��/�57��tN{�����5�����x���G����\%�����a��^�Y���MVr��Oz���J�/'��4��M�3�}����7��Kt L6[�`�H�L��ex�f����������vE���4�W�����(lJ�jRRT5�c
I:�5���:�}5z+�������!p�]ha���490R"�A�3�#�CUqhSE���h�G�4���>	1�T���Dw���'	RM�����f����-.��$�����FA
-g� [4�\%^!��v���_0���w?<���.Z�_��-��^-���	OA�C�	�J��RYaS����$?����5��p�E����q�ad?]�nE�E��Q���
n�}KwL���/�^m�
h3J!K!-*�"�QS���*��Y���{�*��ry��dAm%k�{�q����x���(���Az�C���wEF���P���z�t�JeuZ=9f�y|Z{�G�5������1��u7�	3��s�.��l����:��U��������[�k�m��g�P��q�5mM�}�&j��Lm�H�{�evv;.q��^�\������c�Mg�'������q)���.�3��;�
].hF,L0P�	����Z����5��A��|�lg�;��s���fKh�3�����=�dsS-�����Zju#!MZ4-<��;�*�X���;�5�3�7�����O����1d}���R��jt�B�\*�$9��I��^�x��f#E~�.Z�G�`�87TVbj���U��M�c
']���L��I�Am��[��\4�~cch�3@Pt��G��q��!nud~��Xq�-�blz����f�"6ch�{|�_=m�8{���}[9z��e����CN�m���=�n2���h����^�����b��.:��M��-��;�����v�Fq%�g��2�6T�Q?u��k.�y�������upc"�X��t�����J�L�7�6�emG�6��l��W\��������B��.-@l��5���aK��FX��.KM	Zi�k3�8�k�����8�|�i��0�[po��I��'0��jL��<�����$K�����@y�PR������I~({g��$6�4Wj�H!��-�{_I�_��F5g:��\G��#im�����:!�_ruq����Hj���k��P�1��������H!��l�oM��N�+b����@����j�������w;���F����B��$P-���B�2�B|��P��;G$d����k.u���j�������4R�=bl�������9Zu�HQ�����WPO:���
�se�
��bw6����!o�����m�����k�(Q�C�
���=�������w�("4Du2/���@�S6����!���??�����X�
�l�SZZ�A1����d�\�d����&|y~m.�Y��,���E2k;�����Br�cc���(��-j?����B��_F+��*6�Ql��L<�\���R|>]��3�7��!��#N�z�)L�`A%S���
��,��"RV����CM�}�<��5"h�G�H?�+�����"�?��}�,vX��2���0<�1��^G��]�tQLt���Byc.K;H
I���M����d�^\Z�8�1�����rBC��r<�� O����!��L�I��8��y(�W{�!�D��||�5�;9�a���D���E�����8�{?��6�2�����}��1�H��D|��]�����t�F�1�ze	+���:Bn�U+��94_��e���Le��]��{�Xr^'�$^��wN����j%�%M+��VB��6�N��+d)����~�>������������]�s�\�s��M$�d����|��
4��)����0'�5&�/�Qk���y��e������T�>�{
�	����r,_�����e�a���N�k)�k>�T(a,"+�^��^�+�:L6�:�����l�	�0�4he�:���K|}�i\��J�Ei���%x�����p"3�3��]��"K|XF����Nz��2�N��4���u�H+���WC���R������)���1����q+����.	#�{}��\��/�xrNW��<\����z%�+zXH��f���j���iPK����/����`��&�h0�����
�d?'��)"��T��(���p&�E2k�?���mH{~�1#�G��9�`J@��@��I�I>ml���?';KN�����B�d�L�Nj&�i]X��
�{��c�&v�B�as�����^/���{eW��W�W"K��k��������k_��#0 t���K8���������$����{�Z�K����|�Bd��Tk!`tJZW
ci�����1h �q��a
�h}��7G���v$f'��@�����[��Y�Pc���hWp�A�:1�-�M��IK���Y0l$�8�[���O���6+''?;���B�<r�j�_����e}.q�9��(��|pL}����_���`\�a�d4���`T��&���#�;���!���r��K���^���b�M��|-A��4�l <��_<V��W �;���-��pH�NT!WX��k��8=�pbU�@nX�l=n!F���f���8f�8�	9�=g�f8�k���f����^�P_[��FA]���S�k��h�����U���G1�� l"Ai4V
~��%Y��h�c@eN��"h��"�`|8��pA�T3v�xO��(����d���O�U���N���}��$�-����Z�R&�����z���O��+��t;U��Q=b���a�����F�Sxp+9\�4����� �����������U)����e, ��0�F�h�$%���QS�J��&B�a�����:����}�d�&���?��f���I�K��-����yPi�}��W��l���g�����8��1��~�������U
����,���/
�����g���<����{���P'������[��z�5<��6�"E�
�hH����B��3_}�����3�rF�p43_o�43�o���$)��z>��]��"�%����d�����o����~�i|`���Rp>�Jh0�����	�An-T�r�������'FG�����A�3�h��ySUS�����$=���b�F��oe��&�`������r�*����WH��?��<��j]��_5����@�\K���(p�%���(y��������U��&]���
������	���%�,ex0���L4\�%�_	M�(F�����V�u'�XL�F��sJU&�.,ld��Q���tk8�>]�K��3���d�Ij�D
�y�D�!3E�3.��=������s�F��Q&�Gc����$
3�����+�Q
��)	@�$(��|��s�3�W/�ny�dQTViy,	#��(s�!8�2�X4yI-9�l��U"��3
���r���z�(b��+������6�n,��%�������d�����2�8�����_�:�P�:/�)�k��`w>�|�S�����NVY/���,IN�.T��z��%����!��VM��J�����6`=Ndq�&�^�q���@�el���0y��l�f\"� P#
�>�n�������D��D��*����"�^K�g,�#��z7���P��x�b��L��q%��3c�&��B��R��'�����o�����������Y� ��\*?t�D��
_%���jUmL�#%�1dk>�7�6ZOb����[�t��h����rc6���lB��rQ�k�V&>�z���Kt���8�vqv>���M�Y�1�{C-T�q[����2�g�_��=��c���������������X�
���*v���	`��{���(�����LN���"����1c�r�X����B���&����?ZYGW9�}����eQ�L�^"[wC��B�k37��[;%g���Vf��JV������oR�������Q�1����A����=69'�n��>|����-��������t�L�2�6��������u���+��Q����oTO���C�%
>�%]��43Y�B_���j����������|�c�;%C�{b��-!�+w�1�!��
|���"[51.p	�A?�)<�`ir:����-)hj�"Me����aN}��g��_$v�����t��a����Pw� �=�Uc��f��|3���<w;�x�~�qCe;hJ�Wo���B&h��|�^(�����X��AB����A?b�_�+���\!H�)��c�e��2*v��$��>�p;�a�/0����)��B�����|��
Gx����tJY/�s�6.M���Wk	r��#�j��D1��c����m�P�#�co%����,�(hOo_�J�%R���<?�~[�3�;�������u�R�,J&�1+�X�Ng��Kf{gP�r3����Q��/�y��R��c��<����������h[���c�^����X���6Y���I�a��s�����������T&�q�1���l�<�G��K
���{�2��������d(����B3�Igp;�&9�lA���*2e�n'f}��l��:��r6�G�yX� �Yw��I��\��6\�9���v��Q�\���4s�=�R�/����������������B�#��Z�K��7�~���yD�AtK��;%��������9#�?��L�1���;���5�N�Y�0���S��=��;����p�G�E���;<�����)AHJ���I���L���/Gc];/o
6�>��S�F������C�s�.�6�N�{��zM��wR�|?�f�r��NB��0���^���Q)|���o�������w�(������Gs�s��{do��
���s��,�{����%8�o�	D��Y�z����/�|�[�o~x��j����g���j�4����4�,�*/Dv�Q��s��C��������9o�M+*:���,Z.cJ1�.Vz`�"����sqH�'K��MQ?�e��@��|pG��{H��|��E_��w^~:m�@����~=�f���,�A"�������k�k�yS�	v��2�h/�,f��u�^>�{�
�:�BR{��.������mf5�7qT�� W}`]
&�d�&�d�+<��C���3����MB7�����9�Z=����yITS������V^�������VN��
c���>=�(uv��_%0J�����L��`�:�K'�a���BfN)��V3J�6iB�:��+���\�G�� >��������~.�q����/[x��v�+�m���rNr(��,dE��bn���i�7���||��]�.>��$u�S������$/��!z�<�/x9��.!�R���^���f#9�����8�4��06���(4��&Y�4�N�tO^?;������x*����(l���Y���������*�c�X�YGh�B��,��m���y�9/��x9>���u��D�7��������2� �/c/�M`�8��(��3��.+��X�d�>[�#Su��0�SRr������q�q�����{�%�����U�(5�S����v��
m���v�t9@�e�^3�����pD����2�IhO��SXM������5����rb�r�]\��(�9�~n1>?�NT����eE�ap3n���~��9M:�O|�cR����[��W�}������/�4���-��i����&�*T�uu������:�fY�f10b�j,L�T#?�6<��M��Q�I?�������d04����1%�f����� <)u$�l�����BHV_�2VD	��(����'(7?w��[�prdm�(�*IES�}5���^	>������FN�p�{�I����G�,x�)����9�����{��j_��Y|����������On����Of��+Zm
�Ix����t�����W�sF�m-��G��=��rH��(��&���"��*�"�d��v�f��fz��Hc�t�|!����A��d������v�:����n0�L0���tg���v��,�-�e�����"pw�<	w�l�.��������27��o�`��h���%E;�aY���Lf���?�oRqg��<^�]Rx�$�&�Wa7���E���n���n�i7�D9�7�G�H�����L,�,s��i���'v��u�A��1�|�G�k�7s�^����~U�I��Tsx/"�7/�J��l��0��B�T7-���_<3���h���hm��
� �L�O����e�O/�M3��$���IM�D�5�d���;��SBG�
���Qb���'e�.G����~�tQ �\w6|������������:�,&��O����`���&�R�x������;;3�6����������8�������Q�C�������(���I��y��gW��G}��s�l"��M84Yes���V'm�Hq~��v7���D�$�/Q��[g3	�����?|��t����9n��n��5���\'3�LNZ2%+o7���G(W���7[5;�	���T)]�a�k��i��6o��q�g���'r���	&n�%V��P���rD<|�/	#7�~bx~�wH�'t�u�sri]��]�C��g�
�Rh��mU��4P����#��L����YK`�C 99�h���H�&c�Q_����{v�
������m����PV�������cK���}�#l��;6��� ��a�Y&����n���E-s���,�vW/8cf�����:u�������o?*�\�ZR�?�Ys�yN����W��Tg����4�
��vL>��
�o*����@tg���y����l�G!Jjk,/�?c��6U��F�](�m;G{�������q���8�p1�6k;�n��T�a,f���x�R{p�����h6��	�3�Hm3�y �Q�����-�g?���������Y�1L�YO(B�����G0����Fo<!V��P�^������{�'��@|���M���A��]� ����)���?���Xu���9i�l�H�p9��#9��gVn�z�������;��>�v��&r�(�y�]&]��e��&(�����R�*~��i����p��GS��`Q���>�2���u����Wi�5�E)O2���u�I����:������f
�J����-�+_��_��h��wn$d����kTi��]	|��������G�<��Q�A.�v��F��&�e���wk�+�Q=x4u���s�"���yA������'�Pa2���Y�;�X7Z:�[�� �B�d�d.������ns�� ��Gm�<4B8��������_���@6u�	�>�%���;���`��c��_�~�/T�h��rE/'qU�$bX!���sSh}���Y�=�R�����*�k��0/���0�}z�����Fm'��ElM]������<�B������D����v��i��p���������xg�� �T��>���"����&��*��A�_#��%�}@[J�R5�������o�~}�D;4��s"�+�1���F��R�������6p���AS���������q�=&�����i��n�y(��_F"��=nV=�n�4\z��~�^Q��
���&���X-���2�3��c_u��������P���Jz�e�h�3�B����l0,��+����_�����@F�����,V=���xh��
��t��?���)1�tS��P��es���i���;8�O
L��@-���q�g4�V���AX���-"���P���HO1~'Y��.����r��w��q8�N���$��C�	kz5�&a�AB]>�Fc��SM��@wgg
R%���^����#h�~��i�{��g��	��)~��z��]�?�j���J�S������"7_����<� <��?�@=�������]sbV�
���E�(�>a�1��C#���������~U���[30���F���9�j���l�&�\8S��O*�~�������������/��o5O��m��-��wS
{�	�z*�����.��'N	�+��L��`�F��l����!�,�g���It�F�9�1�hVYx'f�F�2��V���g��?})I���������!/Prq�
��-}�[��.w���)�0Qv��*�`�	��g��>���T7q4���\ �j��Xj�f�-U��+�T�&c8 ���{�z_�g.ZhC����e�@���EZh��r�����T��D���V�OU~*O�![#��G���=2yp��I�P#�^8��"�;������5	{Rx� �'�")��h��Z���� K�bS��������y��dC~$v�	�g�������'6n���ClT��$G�S�9X4,�D��m��S���$!}	������!
�C�Y'�6�(����/������0�;G���YP�����O+��g�������k9�]G;d�]��Z��<Nc�X�_	�0��1w3w��r',G������_<�L���y�<u�����|�����7/z��5�.l�;�oK��K��;vj�C����Q�����^9����DW�F�:8w��^���z[�C��y���/l&Y����3�J:���K�V�,���D����M\,L�2��\Q��6�q��?�G�����y�Y�i����k+��Z������������Z�*IV����I&�0.�F��;�������?�h2c����*��-�����p�N;[��E��5��s=�	1�������yDa�~��s���Qz]Wi��r�s?E��Zz7�O�$,N��\����n:���)�u��{
��,��2L#~�st�'ZV�p�<mV�S�����Dr7�+
|�7�/J��J��`2���
��D�\e��LT'��T6���LS����
�������,������n�����9;-Wp���-$��lz�O15sf3Y��i���Ze����9�P������#sv�3F�M������g�x��L��������m8��\��L��@lr
<����h�TDH����c��9N��^y/�6����y"���"�9P��t���r��,���W�F�8:�&�����bL��� �����.�
�����c��f�-�@6w<erO��<{�Z%����� ��I`��t\���\���k�q�l��c�R�&�1xY��0A���=c�Va2�(����/Ro��tC��,�mwH�R������ P(��$�[2iw�k��D�J+�eb^�t4�pa�D���1���
�Y��F�SQY;��d�M��<Tuu�p���U2�.��	�:��#*����.�l��j0�,���9q�&Sx>�����p�y�X��(�����'�N(9O�EI�����J/�����uA���;b�}=���I���2G�-/�x�E�e?yT�������;�K�T�
6B��l���9JJb���������Z%�+X�P9:�b���a�@pJb�pB&��j�n�����n�+����mR���}��Z��3����d���5���K��n��`<�������OQ��7�w�S�����y+'���K�t����H]^����j����-P]?������	&��Q'�NL����.;+��;��/�����-��l�]�]�L 6M�c���y!����i,�[��F|��8��@|�P���p��G`Q Tb1�fsum��z�����$�nQ�!�%�������&����g�KLdq������p��F���Z^4@#H���o��N���fg�M��2����d3��fK6�:&pyw��,5�V/��������i�Y�A�.��[4Ks�0p�����������'2C�>���R��zO�������SM�W;����
/����SJ��Z9��W�S7^d��J��C������D"�0
�k_���w�5����\�*Os�?����}��D�Gg����V��@�vA������_w*K�KYD��(ss�i��<-b�x)j'��J�(g�2	��#T$��,L�	��R�|����%��J���l@�I����'A�QP7,E��o�}���67�3��L�uA��[=�	h�/�g�FV@q0*56&�F�j���&x;�O�����
�r����+��@�g�.bJ~=��*BF#�U��F�@��~z�S��w~|arK�)q���`�������@�:��?����V���iD�v��[�j8�v�&WU���B��V���w�x�RY[/,�C2�����D�0r�D��H��%�0���3f�J���������g�����]_�s���;NN?��:�\!mF;�!����]���M9$sc'6H�()��}6U��Ps�Fjy�f���Fj���XZ���{��Gh�*�Gj���}�}���f�p��=�E�~��t&���F�gc�U",�������b�&����zu�!����V��}���p�`��H%ma��m+�M�X�a�T���[�|X����o��-���~oGj�9�������V���1.wp�����Y��X�O�?c����y����
{CS��%y������]!������i�bsl��/�����gL
����pF�!V�l��r�����B`dER�["��t��Gi�D'��i�C��#L,�ln��$�������:��I9��&7�[��D[��TRTI}�����6������*����j��i�i7,���)�����W�
eJ4;��RQ�������S�D��!st�P�
@
c�3?���wr1���2����$^���
L��@&������I���6g����m�>
m���|���O)�y:%:������l$�������J+d7����pyv��D�_��L���G�$@7+��z �GoH���r	B�D�����f-��B�\�r�������*��`/�Mb*u���s��n��x��3a^��u��D��s��%�����R�CBM���<��K"�w�p�;��J�7��DQN����E��A�h�������:��`��PD96�J���w'!��k�)�?�^�/���+|0o�/P�	�v�����=�H���)g	���|�V�,�)!��b��7��+W�Yy_�7���\����s[��'��B��Bl������h��h`���HRT�bY	g�|-s��m�{��Mt�����Q�T�5�W���D@&l�����F�����''iq�})��%�� LX�|�=d�8���[�7��#@
�6�"�3D����&^�]�g0����:�rf�����,�__uh[N��@�1t�zb�;������H������.�@b���W�TN����������Mh��cU����,Xv	C��3��
���q�"�)�j��}��(���R}*=�.���hh���[V��l���+������Y���:Gl5�U����C`���������������L�P�f�_fZl����j��K�l�Y)��|�n\#�X
b�/��	�#�h�U��~���$�-~��rs�����{e�j�V�(
�2���$M�p���n7l+�hvM�X��-����/2W����j�4�@�6�������j�vwW`|+�xg�M����w]�N�GIN��r�P���M�z����7���c�[�(6/]h��c�=��K���:��FW�Q�_'����$R!l�v�T�]��������?k�	r�Z1f�R�d�q+r������6<:��0:�\�,�Vr\b������{�|'����d�JB�L8����[-��Yl[8w��F���)r����|d���(��P���:�7*���#�t���meB���&�Jc�����`��K���Ye������
sM�yR%%'��"m�\�G���2y����*wg��7=kbnr�����A��A���9$�J��j�r�C���C��T>Z�*��f��J���u���]�)hK��-���Q��O��Ynn�3���V;Ql�KE���k���\�G�2[�3P�iZ���fh%��d��!5u��VM�M[D�\hM���=��,��-eG��0���T�X�d�3-e��q�L}!���`�@�1�V3�,�8���^l%�j1;�pG�#rm��@���b�S=Y���p?�n`K�d���:�Y���k��
B����h[1���OB������Ui�9���:g�>�`�x��)���7�M<43������C �SO������(� ��D� `f��'�	���v��gy8��_����v��
$�T\8n�'�]2<A���c�B��ct�x�l��[�;m�mT�7y���EPlR,��=����}4�|5A�����d�O�D�������D�n�����nw�~���{���v�H��&�E�:��)�"�z�QZ g��o����xm�������(;����ps�S��q�I8]�s���
D�q-K^+)��.8K\��a��x�k����%�R�\����f�4'�?��s��j
e�.F�����^d�8�nD��V�>m�r��EI�OT�b�<��d�E	�l�>!v�\��K�*n�4s_T�-=��a1��7��2�D�m���O�3�hq�������d�m���9 !�gjko';� K6)�u���/��� *�����$�s,�>����Z$�Z����p�{S��_#�����2�&�����J-�f�m�,o(��y��H��I�f%9�aW��'F����)'��^$�f4���`\Jc�Q�gXF-p.���}��~�����1rI~�}a��O������+�
{�4����D~D�K�e��X���`,1jk���g�'��,��v�O��k9���/���T�������M�A�K��}K\fB����Lu��)z�u�K`:��s��*
S�J�N/C���X��i���m�H?d��
���������{�@���U�pM�
)��/�8�:�h7��tV.�B%5����>I<2`;����T ����bd����#+T\^0�� ��i^N�)���	�e�C]�CR���1r����<~����h�G�_
���\�G��@<)MQ4r����W����B��4�b�"�,M�e`����8d��!y}��qr���Tb�T��l����A���]���By���(������
(��iAfO�T�$IFf_M��}2�:�|�.�
����jz!
����FEmF<]0�pE�����]�<���R����M�Sp}�h�I�\_��Q�>�Qx�t����s�����.<���Tr��F7 X�l����5������{Y��38�q�MU����s�i��O*�)��Q�I���q��P�
YgWMGhL�fc�y��)����7���w��oQ�>i��\�O�(�=��������v�����r]����������������t���	i��oP���L�kT-2�.��q��igc���!3L�1�'�&�aPU�3����J�A0K�����0���t�<�a�UA�����r�">��6|J����3q0�&Sf�4-�Y"�\�1v��q���OfS�QY���g'�Z�3I����;�?�%������k�L��mA��;0�8��>�@f�}fC���a��Z����w*O����[f���yfB�?*3\��Q&������?~}(#����,n �ZV���P��X9��2Sy��&�Q���t����v6��W�*�+�(���V�HEO��A�?D�����NRv�hw�4���u\�@@gC`�c>Q4�]��Wa�o2S���<"��P"�J����x��;;�0��[P�y���t�������U���
������)��+i�A���\����Y'�����A5*�Owe_��j�+�8��J��r,&E="vBw�t�_U����Ip��Yf���>N�����k�g�I�$��~c^�_!?�+8���
i��_%
��Y	�N_=X������[���
��������
���\�aZJ��4[��*v�>��gp
�B� ����mN7x`��!DD������rMeK0=3�^l_)���������-����k�$CQ%��g��Gu+�1*#C�ST"��+������'
��4�^�����//��.$��i�N���8��1�C��+��g�>������C����'�8V��Lc]�����"����t1��
�E��T�(w�(��R37�}�� � �%}���T���m���6$�w�W=�e�����#�0+���fG�*�gc2��;��J�7����ZC&J��K�2�p���4�vX�-����-��J�o�bq��Qo�?�J&��;�I�����V�G����3G���S0�\���b�f�UWR�n���@2�o�L������v����/��D��L&JA���$��)��Y�4{R��y�+�MS��G�kH���=�%�I�E��P>VN5��q]���lg
<--I��,~���Bb�X�@������e��X�K��jX@��9��H0C�����.��Y�����-0%����t����U+�>p
?A�(�P�P���{��y�8h}7��w�fW�y���gr\�4�p��ve�"�mw$R9(o��h�O`�:����8��0=�]j%���8)�fO�����W�$\T�eg��;�l�I��LZ����`2�9����Kb3B����~��.2�23d���\�e�A���#f�����pBo���6���'H�An�p����o��{����������Y������h�����p'A3i8H�;$I�� `��/���l����Tn��P�S�5�M2����	�g�NSL���|��	�pb�6wI��n.�B� ���l�Q�CB+��?m�J�����)1e%��V����P��P%tPsk�n��UH����7����Z)����3��B�%a6���r�`�U���
Mm>{M+�����g��l��&X�I5t�T����i�V����f"����N�[�lAW5O�D�Lr�/gC���W�5&�1��������xKn��@��
C;#E�,��t���p��s\��1���e+P��k����L���=�V�,*B;��@��*/#
�,��,n;�S6U��&�=p�5-e��;_�����I?����:5�Miv<�W����F��j�xro�K�%���r�'�Dp����3G�I//T�lf�H���8��7�KWn�T�!fo�}�P<�������eI���K	���\s-�;���q�0�hz������M�#������i�32�>N�@g�h�~A�tvg)~3�X���-��{��oY�_s'X[���tV�������k��~��,���p�r�������I���1�o�_�@��:*$Kt��R�d�e��5cA��b�q�k.{(G���
��8s��@�#�{]�����o�N��$1Sl����`:��FI��t/8��'a��6;bw��,����9kD�������M�cR�>��*�\@�5����Xv�;��IlTj1t�@�����c;87Wb>�!N�`
������:��"� �	�r�8L�����a����c�������(�U�+z�5r���A�|�Q8��Lb���������)�X��S�Rg�\$��y7UF ������U�05o��$���V��h�[���:�:q�+!)^
���\���J���f�J���on6�7�[B��[��VPqT�J��_A�NkY��Q�J���
Hv�+����Q�jVw_�"1Y:�.��7���m	���E���n�wxrt�����XB��E�Q&������EC
N�S�`��?��O/�6��N�y��/�-��!*�<
���+t7"r}��GB`�'��_�R�T��e�$�c
D����b/���S�k������)���	O�x G)������������?\�R����}��)���e�g|�io3%p������p�Z��E/�5��B[��������O�/��5������ ~\E
M�)9��3kR*��������yB��T�P�@s��3I��7��a���0�EY�������/,�q�w��u$�?��������T�[���Pi�����tc�����]"�I.y�CAa��,!cj�	�R(�0j	��]�y��j"�{��M)V���}���;?+O#||���]0�4,1�h���(�~^�������a���/�r�B~��*�e��$�����#<F�=�J��I�gU�g��k
���~Q�	WdB7�xB�����,D�R�kp��"�����b9�sA��zQ�=���z�����8*��P�S^P�Ip��hK(1� E�7Vk7T*ofA�������,�����<}2�R���O�T �e)�u���+����L��r5'V�E���;�!�{�8���.��*o�1�����cdV��A����A�`
'���N�����@z�::���W������jz���n�Z����M���Z��4�����G)�`���X'�G��I�������\X�����V��mO��L�c�q��m�y�i�����O�]z$	�}�\�i�x.j��U���!�g�v�x�B��O�����R�~�H.$U��E5H��/�a
{��
F52����)���CR��C����	��.�=�$�}��t?QbS�\�����fU.�MuF-��&��%j�}'��%��k�r�FU"���S���i�b�dUNE���`��v�O��e�Q�i{��d� \��db'�����3f\�G�S�K(;�7t���K� ��"a0Xl} N��#�����1�P.��&����g��;�7�|�G��wG�Etx�`���'�R
�
��^Z��(��@T����~���h��	�fc���$"��VN�����1Jp%Vj�+3��m1`�nQ��)�5�)*u:?�)}p������n! `�g"	UL_�
�:��Q9��J7���*<0n~�-�lZ�7�L����W��jun��\D�}x��9���V���/����K��������FB�:����O���y&h��3�����������A&�L�?�aF�����I�����x_����k�,`=
����BS���j|1�19��6���kb�z�������3�����]�mHg��[
MG�I��_�T���-i`����QW�����O����Yw�5�w�{���.B��C��)�������,pZ%GG�-eh=3�KO���ir���m����w��y}Y�;a��19�����������?]���$���TGNw�v�*
�%�$������ ��&���6w����wE�]�J�w2��"����r&u+�����	u�
����������"��R��W�bm)7*wH��������Ta�K�����s*���2;>���������<�zN��C5O�~1����a��O�}O�C@���S�B��w��*�
zg�}�{�L.!'�f����*{v��#���+M�![����z��4��O��~)��N���8�_S���(#@V��������gA�s�"J)��,7����,^m��0�<�O�|�_"����W����<]�w��*��f�DWa1p��_�?�!�>��pGT��,�z���p�k��[;?6[��<=���{����f^������{�hL-�
�8-V/g6���&1�H�>j5��i���ZG'V6u�!�W��,������.����B�\����
�r�����Z�I�4A�;?����k������,�u��='���J?�^�2���"����G �h���Q��
}%���?�/F2�*��;������o����H#)�jW����,B��0����2Y�a�����Oup����E�F���H��ybRM�E�.���%z���'��BxYp��n4�'WZ)���1�J)�=E~��i������FJ=9%#/�8���h�D�o��o�a��4�[����[K<��rs���W�w��B�@#������F|I6$�%
��
��m�[������
5]-������y�->����[�  �{�A$��M�0�pL�X�%v����k)�����<���$+��R;���wA<�t���b�����x\���x�� �a������&<
���w|����]�L���a�:�����������u(WH�r�p���v��Y�|������^�����X��T��=E.V������+l�]�{+����m	��X9�����)HZ�>P�����b��9�u2��P�P&�0eZ�H��t!�v
��Z1� "�
{�Sh�@��6���(7.&��/�������jn�����/�����Q["�����m�2O�����,��>��x�d#\+����������A9��e@z�xo#\%�=�@#���H���sqLm����M��1ym��	��(��rX��&p6�-�����wF�~���NP�ak������"��Z|[�R�}������?U���`���('��]��c+Q���y&������XG��}������k�B��/��
4�$����[��P�(E���p��A�5����/�|���d
�k|%���~�����W	��s�t�9�������C���><��A���������?6�= (@X0����[�=���#D��4�f:i��^a��=,�����||t���m5
?N'~��?���?���x�V���i41�6�(�-@��|�C���mf����0M��).�7�x���~r�}|����7��JUOe��
)���������v����1n�w4
����eh��p��$0�s�2�.Ba��n���kI^LF��aI<[����0	��%����L��H��<����� �2�b�h��@�d�Dyr��,�_M��I'���������KzK���v�p�����-�;������p-u���V;r����@�X��2ZfT���]���en�'����y��S�
Po��!��1�+M&@�.�������{5�UU�Xwv�c����E�t���f�=$��kxaS?���������$�+=�u[���Y�"\��+�a\�����3	T�[�)�W~��Le�����i���cna���tc"���!%�0��ca�!�bj`9����	o��!�C4�������4_�*:���U�+�dX*z  �!�HPU����p;f@�g�cl�[��S7
;9�	)�\N��(D/nTF���R��n7��W��2�.����+��H�F��
;��;�w��:�H�N��'U3���0�����������f��rs��h6����Q�%���l�O��3�r����#&�r5�Ab�����z*&�6�x����x��
�\������M���<hP����j�r.�6�cPFc��}HWSrc��\|�Y������l��MT#[��X�V/{������x���d�1Pf�~c�����_��6�l��F�� ����Q�\�mk��Wf�����[b�����9cc����.9=���h�0���M���Vl���D������$L��X��,F��|�.B�����*ch�
	"�Zf��G(7�D���\�S�4��V�U&���Zy��~�����bt��cN��8�pM��T����`�w��E]
���*��B�
�]��w���Q�@}=�lN���8��$|�+�V����^�x���T�l+%(Qf`��h�)������/�q��D&pZ�,�Y���d�a����_I���d.d�Z�H���];=N����a������w�8R�x�8*}�6h��i2j�x�[M]YF&�vJ�9���O���%��eX
k�Tt����e
#�u|GB}UF�fu��5�	�#{fW�D�N�[�����`oIt�L�������Fk���2����pR���|,Et���7���j�28m�w�~^��8��P�����/����M���N������7�(I���_
�~�>�����������p�%�0�&�(��8x���b8fK���'�
�^����p��5);��b�������|{�h����}{@���Y�j�p�]������J��/����``������#[{T%�����h����La0��%�}T����geM�o;D�W@DGb�bw�*?S�#��~R�)�FQz��{��������H�����_^��Up�:4f����3��j������/��T��8�������v{���1���|O������1w��d���a�xU${���
GC1q���c��Mo���^hF�\�R������Z������B��e�T+����J�\G�;�Zb��[�^����yj������;|s������Y��uH%9���������9�d����T�3��S���^r8��N�}�W��������uv�����������S\<�,�
�����G�@��{������X7�|�t�U�Y��#��3�����Cd��^�\����%V����}D����4
�2D�x^��x:�0O~����I�y28S=l�4��c'��uJK,�x9t�W�&Q'�'�$�%;H�|��[E�ewZ�KO��K��"m���N{�z��S5��(;Z�e��\��T/�v�?k'R�x��s�]M,g�w������J^�Qw� v9�����s�>��y/Ou(���E��.~a��F�V����"���d-*/��K-��3��4K�W[?���o
fK�q�gB�N���5u�7�]���E�UU���y��)VJ��I�W�i3����0���TOV��������n�
����(�H^H�����3���TC�u�����p��L@PME��T�b���$	�O%�m�"s��b8r���zuB/9.n�$��u�#����C�m��}r��/�R��4ni"E�i���S&���f������b�@e��&�������E����)�Z����T1Z�������%%
��l����
+���@�V�N��5�U!�"�d�������y�f����B�c�i���@"��\�X�}��|�����?�jS��jY]KO�9������l���0D������h�k���D��DT-4�3�v�.2v\�6�I���V�02���Xze+��#uo+����*E�SI�
�RC�k��U+�����dU#���
�|I+��(\����i�#;kW���SRd��n5C�S\�ny-E���
9���'��JHh<Cw�w�����'q�i��J<��9�X��b'���\���x���n�����T7FD/%D���\�q�����.��KItd��`��:��U�E��:�pNE��
�#��CzC��q��rO7������������Nh�G�2�6���BW�8	����t k\%f���H�b�G��)m���N��p�L����`AW+�z�K�C���(�A�b�������(Uh�O�����/+�Y����!��yS��Dd�gndA�+��	en���Ef������wS���4��?=���)=�ar��@O����]&�sm�bs��v:b�U����H��wQ}DW�CH����:���B�8V�f\)b���������I
�f6e9'�q
ga�#�[LN�bEM�������nk!I`&��5>HT����2�?�A�-�F�4}d��>!CJ|1�$�v� 6��V5;M���3�_���.V�3�z�S9Xt����!����H��~^=�{�n�j�A�_���X/a� ��."�	}L�6-�X�x���9����c� %�����|�09r��bK�������\N7l�y8,����N��J�*&�\��4#�;�%���Q�sN�r�%(���L#m{4�^H1���DTu`������dJ�n�Cb���w�9��%S��
���s��cCq�K,�M:���jF�jL��S�lO�d���
��D����2-�����a���4ba���ZW��v���Ffb|Y�s�	���G�G���N(Z�����\��3�� s)�6��n����:�A��"�JAX(F�s�'{3�N�p�TBh���-,��xR�����.�;7��������h��8�P���^ &k�����^ %3��`��2*���6���9 J�b�J�����*���I}`�n4����a.BO�3\��2&����@��)�O��?r������Z)'4�KIp&����Tz2�D�qU��hO�:�aC��C�n6��@�h0�.N�I/9��^%yN��-F���a1<WrR�yR;d���������U�����m�E�l�A>����z.%h~%������EYA]z�C������Y��n��(P,N ��y��Y���0���z�����{��s9����
'�9��!�9��ex���>��8r4���=��`R�H�Gz��J^����N���L"��S�����t!�����Jt�z%Fu�L�[�����"5��(T�e*��-�Mu�v��W�c����� >�$�Td5w�OJ����0�sn3d�HT���L�'���*%3�e�S�|v�`���������C�k�.�{�Y}���'�@L8
�2�������&�����3��.�]��H"�	Gg�Pju�t���G����Vi����C��/���r�$���h~&YVI�(��t�_U�pw�)����ZL�P
�����Sl8g���D�t���������n���4�;�k~�7�D'r���������d��'q�Y�S���=��d\�T&�4<\��i�EC*q�Q���KW�H-|�������$#��ZP4~�����X~���I�u\�q�Zp�3(�S���j!�d?�4��P�L[�l��a���RJRyr���!�J����BK��������+�����sb�g��~��d�wqy'��x���D�����W��U5�M�ho���h�&4Hi�A'b�sH���!���4#�:�5����Hl�Bh��"�]A{/��U�zc8`/� �Wq�e�
�����y*�����������0Y�R2-�z.��vt��������R��Exk���1����`����
?���+X�)��1�����eo�cpAU�d�Fr�*gOa���T���{�F����B]�����]*����x=�p��J�'�����L���������]
m��u��e�A�Zz,'�`��7A]�%��]��S��om�|��:�a���q���`�J���T8��ym0�8�������*��;�wp)��!<P����nra��g� GDTr��6���G����*��vD���=]{VK?��[�R����h$Ee�K��{�b���S�
p�?��w>������'���~�9����T�%����h��������Q?��R4/�� ��H�����Xs �������W"���c�������d,��5�w�H�?EY(����$�&[����<��v�U� �C����|R%"FAtv%t�n]�:���T|�%����5��B�����U�x^7�B$��UD��7��	-��7�z"m;�3����Yma�!���8?E&�i�U��-���������U��:���C_�+���9bY�l�,S�c�+<D	n��W���:����4 %Y�l^>Q��r�C�Q(���0�
$�@H�,Ee�7$��!6
�,i�x�'�L�EP������}N#��WLa*�h(%�9�_�|L\E����$���p�R�'L��N���C?���r�F�����[fzn�Y��������A���Vex�UYsUZguC6g��Z��Z�VkQ�ar��]�7C�������d�j�	�3��������1�3����w��>�����S��"�Z4�c����z@l��v},�w7�hF*�1���'yn�I��q!��BN�&�W���&=�����h�AFFZ8;��nP^������/�������&���b�����r[Q����R��{�*�9G�^v<@��`J@�B���X�UyW�~+�&�=�j�2��:���K��p���"8k/���-~�
U'��]���&�x�E|$�5B#5���������V?��l�4�N�Xj�`������n�3�|�?��	��ia6��Q��
)Q��w�:�cJ,X&�������El���y�\3b($\��n��C���*p����������l���LG�Z���OEK��GY��i M��>GU��O�*0%��D�g\��Q��E��p���q����n�y����+3��K���JN8��C�\��p�Usu���$h��/���?��j�qq�GL�E��������D����;X�����)��Br&U����-;�MM1r�7�z�U�.��8�VV�#��(�N��.$��&�^�D��U�����D����ml,���������g���7VVV��A��)�5O�<)7������V[���k��{��?��+�������P$�K��ub]�a4��PX{���5���5�(��$<�a�{�u���~�xL�_�NF��������Q��Y?����n���������'��fy�����0t����pa��Qw���Y�z�h��,����$�o�^����l��1&���p�0�G*&
�F�KE����������Y�:�e��O���|I�Tx���RY��\/1O�<s�T�F+K������%k�+#�&[x����}�L������~C	K\��t��Y�i9-��:<:l��~[[�Ldt�����+�r���=���~c����,
��|Y�*�sVJ��;���WY�-�/2KUGr���������%��������X��g�=�������r�',p&��|
�&e5�W���o&((5�X��#�2CJ��d���l|_6�Vn/��eK���l�9���0�vv^��W���'eN��:�{)0��Gd�x�y
c{�v����^�FY�)���l�T��H�[��6�|���94�kiL8��fK�)��G������!�NrLmJs�"/K�����Jq������?���U�<�����gbaH���O%�����������gdi�3�<<�b+y�q������A4L��`����4�N$`�{	�U�AA>�'�I��d�3Xc�L�E�(��,�<Ct����Aa�������$�*!ol�ot4�S-�J����X2"B����}�� V[�Q_P�&���;�J����~����9Tg��bH]����I�mC���_<����(�/aB�C�L���'V2-���	'���Y$������`S"X��I��J��� ?>��0�JDL�`V
�h�~���p,�p�DT��jT�a~O�,���Lz%AI�>"���{b}�����:p*��p�j[��"��=��t4��
�<��[�E�_��W�<	i^�2����Z�7[$�X�(�w��m��4Y���B�������#�����1����z��t�81X5y�+�)!�,��=��r<GH�=��Fhl5��{��}���HGI�xs��(�Y���e`�R�Q��^�m���r�e������H,6.�i/��AW��<6��c1K������D!�n��������1����,
m�����{g}��f��[��'�����������A�z���}SH���O�(�6w�:�2�d��4�����<=2�tZ\������m~F2�����	 �YU��G��?��|`RX5k�y(������K+�^�>�>�~�l)I
/����+���<�9��Ky�S�w��n�of�r�EJ:�:����Nd�og����p����&���E�2��^�3;�

%��d�<#������ynN$��XRb�ba�5GI�����?������6�P4��u����(u01��3�J_�"[���v�`�Q/yoTA�5���7�w]V�u���w�v����{{�"���w�������m��U��>� �L�'f���w5�������R2EC�����(oJ7H�r9*�����G�G/E6M��/������h���	��U
�8J�k��i���%re��C�OL����S| 9�	��Z�>����XR��������A�;9��SNOdj1���jDq����X��?����~�������J���

B�v���VN��k���8�_�i!pc�xR�9+���t�3*fB�	mP���<N������.	*���r}�����Yv�"LP9����&�g`��T������C�$��(`B_i��Il���k����"�/g��u������U��3R�z������)��&�]k��f���~x�%���W�H��,-����$�rNPf���Py�\���K�S&J>��_P3�������d\�"K�v� I���P�VK�t(�=%-�rn�K����	��#G����'4�"����]�������Y����������7J���<�2������ua���QU"�P���Vg��d�Y���
�������>�d1 kN����s�j�G�=���-�)�����r�<^�j�/���q�/]2�j<��:��eFMOE9A�k�>�)��o��E..s��u�,�4(|�)C�0���cS&i�,CB0�S�)RwlQP)C%����f�~�6����b�9�����Z�e9�g�qI����<�%���n�iE����R<HL�"�*�V8F����iw�?6�����Q���)��fu�O�d���2<lw-���Z�T���X�3�����-��t\�#����\N���#[��u�
1��]���*�*�o���5�L���EIaKL���"��X�JbT	�3��2�a��pi������ �S�������c9k3�y4��?f����sX;����x*7��z��YJY&KT��
���C�#Oh�����99�EF�|�i��:@6��������� 1���8��V%a����x���d��	��R�&��0��-�jB�n�f��),-c�����
�S*	��&���x�
������p���q5�YoZ]���S�{���_���&f���A�bW'���L�"4(�L�J7AA����8G������XJ6�U������%ZLD�'��rN�F�cJ����~}tN2�C\�b���G_�1��#?��(��;U�C�nc`������BLU�.�G@��U�?bY�A
	yN�~/�:PD��@����+K�vC��uQe��:C�B����a��]�?���MqU��/����I��
,��L�+$��a/\�������KS���@(?$!�����X�z�{_l�/�H%
���ZO�"d����yq�������B'���5ff],��Bni� �O�\���pgjA-�f��P�!�]M����\a5�u(
���J9&�=?����uL��k�k�;��Q#g���#z��()�c��9������3�F��s�q3�p6�����64��e;e1rSg��F�q�Tb����^�;�REY�Ple�v��b	��3��C�u�U��c"�F����D�
}m��U����0F�|������1��be���t(5���Y�Df�p2z,��<��_�#�jf������gX���������<F� c���@7{�����n��]\I��b��jV|KU�!�&_���^����XXVWS����%��O��B\!4FUU�b4D=dQe��W�*�A8r>��e�����v��ce�qF(�"��RdyL��'iu�'���:���
I���U�2��*i"�_����>�ni��y���J<a��iM�Z5k����j�d����t�M���O=C��.������Ug�����J�E[���O~������{�����Vk���A��u�E
���\A@�5;����n+��7���<���M8RQ9&a���s�N?t����G���j�j[B���&C�0�@}��n�[#&]����D8�E������=��%m�[��6����3$%#����2�X��Pr�.����5�ry++>J��H�
��\�W������

��"��])A��/^46�J
"�QI���������#d��gp��(���"<��g�Y�M�����^���������s�.�c.z��z��_O{F������S�LNLeb$�$�e�,���\��|�c|5��G�(����9	��Q?~�]^D��� =����d$j�����Rb����%�w%�b@���G��p�W��a�,jI�\���
9�E	�cV� ���ZL�&���X���%]�=�������f������zt�"����W���o����>��8�_����>��C����6�1k���3��,Rz�0���3�'�Z]V�-8j�7�?b����Hhz�-Y�r:�>�#�&D�*X�uP�0"]�������x''�+��7�����s�t�X~�},
������"�����`'<��X"��+bP7�UYc@>�B�;�{���V�O�DE���<r%�>��(+�d��U.
����.1�}0
9��$�H�"��+�9~.�4�0B������8De��t�]� t����� :��0�,��^�����E^.���b,I�M��S����'#����t�1�%��>��gQ�b�*�vBZJ
D��z���Q�?�����6R	&d\S�G{�9����������pL�����aG�P��gh������B���, DL
����T����0zk��z����X���o��g��Z��.S?t���p��Y6x�C��n+WN�h�Z��zT-�^pr�UW�%��l���P�����g�~�W5��b3S��S
�Mn�S6����REh���	�+�k��QC�9fRDjh<e �+����d)�F������}�M|.�B����aR*Zjt+v;���T���(��3%�gD�x�$�(��A����q�$?�c"Q�8Zd�p�R��������?��� �����^8�qA��Pa��&?�{T�Mk��C`kIX�	!���'��0���C#}����G�X\Y����Yh��f�
�M�����W?n��6�
����O��U��=C�SdUCt/��g��G0i`#'%x1:|6;�b���������U��t��IM��Rw��p����ep��Y%s�Q����?f#L���9\Q
8�(��AFK��L�y��~��4%�+�����0�����#hsL����'����qB����:����T�zId��n��g�Fi�!.�T�S��)
�|1��N���;=0�3"ja��A�CQ��(�m8`�����Hv�L":kbm�4�Ma�_��b'�l��w��xl�\T��}djHl�Mq��$�=KJ�+[�1 �re�����W��6 �`i��"&X@�U�p5�'u��&��������n� �n��Hz�}�|%��(��	�h�X!���w+��r��f�\ >�<�����)���J��h�/�]���01A�����$+�0�)cC�����LT�Y,����<���FC-���Y�k4a����Q��q�Uv��#��q�E����D���o��!6 �Pp�Xp���?E���)��B����Y?Bh�����X(%������k�����e���F#�|Y�<_�����1�4>X#1�Q�;�c���z<�>;G�)m�����P�������(�T�j�~�����\sM�n��UV�&���/�q�����56_���1��-1��yk����V��M��u{"E)�nH����ley�x��%5�%�v����8����t�	y8�2E�y���Sc�:����M�Oy��Q�e
��b�����53����<)I+M���w�0�U�2&�K�^�����Q��=�e	�����R�%�WK�~3�����1���A�3T(����40��N0�-��o�|Ovg��|�`��h�����9��^7�;`�G��O�QM��5�������`�@�8u$��,��cK���b�d��'� �L:�;��/��fZ'.J�Z*:�tO�+��`���SS�J���g/^����3Lz���L���!%wL�������"(>	��������h<#�IO����6FE��3�u����z����W��������+�/r6a'��BU����';��v�����klcR�	���l�_�;Y�Rm���oz�Y�Z�ew�wn[��w�0��?�By�F\	��y=
Gx4���1a�P<��[��u���Ce���������������l�Q[���'�2�M�sl����������0��r���-�0�����I��@��L�Zi,-R
�\��vp�K�r]&4��$�3�l%��N�Q����-����tK�2��R���sf ����C����a�(�{��9���k��<_5��n�W�����������f�7��,#E�/��dl���:H�\�+��3}L���Z�)?�GVg�������{n�i�s����S�G��sbDek}
5�#��N9�������jA^7��.<��9jy��W_]f��?::FW��7^U���Y���M�"�~z�l5=�	�nK����gV�����[r��e�;����5����l��(qX�	��a��f��X��q/
�6�*�~�+�<U4/L!^��r_�U���|�Z`!L�Z������Q[�ml�Y�y��:@�77�����R���}�a��I�������J��h�J������������i��,���l6�jy��p��x&
l=��5���s�Hp����F�LV���A{E�70�4���K_��!s�#�@)��9XR����,9�d��3n�������Y������27��y�o��+xf����Cp���X��<���7ad�9|s95%���h�����W��~�eA�����U]�Te4��f��<s���X���]������
.2�����=��Dq���������N��6���p�`7hI���X��9>~�|70��m�,�~�m��s�A*XT�����l��l�R*���Be�l�m^1���������0\��L�(g+^�����9�c�36c�c�	�����?�8�z�g!�7q���.����^�m��7�{#����2��_��3�:}@�u�a���VR��f���������66@����v��OQaLy*H���������Z��^��$4h��2��U���\��B
]��c�\MW�^^x���W���F��7���i�����G��"<;HY��'���]���8��:���lxW�?���w1�M2��Q���Gl�-����"�0�����IH��9J���Pvv���Yo��E02l�`#�Au2�t����t/����>lc�&y2^���'����W������>:���������|)b�����Fs���+p������_��'��G���k���,�Z�yB|�9�����2�V��b�^`��*X�?p
"�,���<�zx�K��e��st���)�t������;o����'/����k������G�Z���vk��g����z�uq�����F-��]&0����������G��)�A�^�I"���$�3��z�>�D��,� ��s��e�SYe��{�Dq���|%������g�\b���_����5��\��'��I�V��t#���F�KtF���m��OK.�06wK��)����{'���_��$���5t�E���RY d�dcCk t9���I��%N9�,�S�OJ���)��g.I�?|a����r��*?����O�p�+���l�s���(4+�R����W]w`	�}�>Y����+��BGJ�=����;�A).'����l.GuoV
!����L�}�xYw�G���:�.^X�K����T	���Q������y�J�4�!�&��;��-�m$����tvk^Nj��90�"����vX�
�w��[xRG��4���ze�^���\�;���D?�
�2M����d�[_x���m,<���6��-'\�4��Fe�Q�l��F��^Y_�l�W6���z��QY��llT67��F��YY��llV67��f�����������LN��������Hk�]�AK��u��B6(E/��u����Buo0��0�u�����'��yd�yS�����3?j��G5P]��yt�����-���n��X�o�Zp���4�~��.qp0! �{x�F�rb7���yo�d
a���k���)�>*3''����c����@)3<�<}w�X���w��������4�������d`���:�=�����v������=������+��Z T���1��1 ���L����O��)t���\���p)w�	J�]�����f
�y���:�u,�>��h4\�rU�l]�h�����5w��%:4����o��\�f��p���k�,x����Q��M%2�!Y�+��F��K�G�,����9�z���y+��9�J�A�Q`~.*�������t3�t�����
<�b�P����S����AM�M~eq�}��������'��-u@������,>����'���-��h�Cn���K�����;��e�
�R2��	�4j�:LP(I��'>�yu�1j"�\���cTe��A��c����)u�����sg^!=i�Ch	31oc�8�������Oo��W��*�Y�����F����ed����p�H�
���k�+�Y������H=�=~L���E��~$��5�V����N�W^
?�z��zR�o�n�z�(����/_c�U~��\���6�(fi��zn}�(�8�7dJ�6*wm���X(�u��t��KOD�v�Y0�'b�	;�J�����=��z��]�iHO�B�!1��/�c��kr��u�S���F��-\6������X6!�����h}�3���w���e��N�C��\N/+o=�Gs,&���Ge~���������w5��z��iQ&T�����L��=l���=lR�?���95��w���%�S�uF~��a�F���������vnLd�����`.<tu���� ���4�;��#�qg�����Q��%��"$�S0%�r��l���f�,�%���''3(�d\�5�K>S�\F����6l�(n�Mn��\�'3��''��oKv���������z�m�n]������R�n�ny�iJ
�����sNa���^��������7�Z��5�0�_��q�jH��W���a�(��������f�eL������d���.#���DK�~��9(	�����-��!y'�A������O}�w���i����
������
�l���6��{�Y���'=ch�jY]:
����������������N�����#���YE�3�usE�����_�k5X��-Q_ooE9o�����hs� &�����k�u{r��2��Ln��a�h�Vf�q�m�6�2��k�Mh���k*�����Z([��2zk���B�T���]�^����r]��Y��e�r��N3����"��3�J%?��j�Lys�<���q��03�F:t���.���$<��$|�0����s�g��K!��Z���ZMQ�����������(U���:�b���j���e�D"��=���kC���:���_gu{���H�W�8�5�`�X���eVk��u���(���.Y���j������H%�q��z
d�mf�����wE�������}a{��
��;��L�����z�96����xEu {����9��@e���GI���I_>x�����{|��Se.:���o'�<6�����I���[�#j��v��?H�M������������xjT(0��zR�H,���m���9����l=����W�Q\1�sW�X�z�����������^�����������e�5�(��L/4�9�H>��~� p�:�^�v��k%}t�j���������W�L��{�f����9�Iw{�no�E�J��8��A�������}
k��Q���5�d�Gm������(z��d��(��]_�E+(�W�������>f�n��.���pwn�S>\
|�L��_��@`��p��Y����N�*�����,������,���i��O��%}�����C`9�|!n�\���=s#yk-}
��/�2���_r-���p����f	Ww/T)���]�!��,&���caR�4�"}������X��[�����6`�Y����!0��O���n�r���J���e1l@SQgYE�	�'R�T������m2�f�rf�|��Q��_�#� w���U�w���a0��������p��gxo�X[�G�R�������7gF!x���}I��,y�y�{�|N�M�Q0H1{=.�z��=������.D��m�1�d�I�^{�?�����T8AI��I���]�����-�t}����QQ�7�Atg���9�i?G}��t��/E�]j�)7����uU�C��(l�6�R�JN1��-�AB3�($��k�����|'�k��S5�LL�����S�hY�F8
d6R}�-=r6������>����hW*?��o{���y��$G1S���o�Z������L��yS����\���I�������4�U)����H���3���� Q��R�u)���z��!���D�����hKS���#8��j��k�.���'2����?�&�������8��D��r�w'��v������uY��a�@{��	aY�N�����:*�Wt����A1@��*�W7D��>���I�@�R���O<j}�O8/^c�O6�����e��$
�7"����d�6sQ��}f@L_e�f
k��[u6R��8��0[��W��w���y.k��h�����7�����>�-�A}�(�!PZ+��*�����8��{����N{��P���$���.{\��D�a$��'�R�����g�{����%���o�=�~��N~�R�N	?�O�)ap�����i;��A�6�<�Q��k��K��5VU_���2\!��m�Ibo������SlX�5��izcyke����Y?����h|]������&���]l�A���l��fw;��K��BU���>�r�s�Z�z���M����~�s��I|L��b����[�7n��w����!��/�>�G?U��������  ��D!�7�p;5�������-%�M��gH����������z�:��C��fA_I�G�'�����A�M���W�D�-j���p9"���q�s�&@Va��lUA�*q����	�n�Q��'�[6J�\��5����/�,2�-����B{m8!�`��t�c��%��m���a�./�	�����h�O	��?�M'�WE��-{�Q6b?E���bM+���.��_��A��7x�����u��)����<L�|>����5X`
WeR�zm	��2�P�N�wr��2�5E�x{��l���������������B��Gpa����^[g���`6�~�N6��$���AS"�eX����*���yM����3�3�Jk����a���F��s����:��I�?

B���\g;����r%��]���$o���+=�����)L�*'D1�c�]
�Y����vE����w���FU�N^M���v�'������J�5h�����JV��J�P7X2Xy�������wK�@.�X���5�6qF�VK�[]��#�����B�\e����w�^�L�Ry$_�).�J��7�,cLQe>������:�I����
����1�M<�'���3�'� ���:���-���M��
�9`V@6rOf8,>���852(9��D���z�i!�~��Ol-����u����y���
��6�v�Q�k7�%6k�<�M���p�������c��t�& R]$�?����>�&$R�%�$
i�gVK�i�9��PysL�����hx6������V�l]��~�����e�a�"fS�`��?���'Y���������.~��6�?Q
���F��\x"�#��GL�]d��N��,�(�������hZ�����>>{~V�lt��G�����e�y�}���;
C���!��R+d���d��Y��wgS/�B�.���!�q�����/��]��In��� n����t��\��a4<_��\U�;{'� ��x6'��;���^~����~��Fy�7:9�+�����4�������L���mm��
 ���6��<�j�)���E���Ov�5�uJ},V�E�y����6�f�SQg<��?���������?���5}��;���|z����"dGl�$���}b5�	 �'y�x��?�/�����s����zW/�.^�?����b��������k;��?>���y��.g�2�������Gm��6%N����m
=�|)��)��j����$�q8R*�+�w�%��/�6'YvvV�K��I����^�IQq�����A�4v�!��I���}����M�BC4�pZ����(�����%�9� �7����\1����K��@@)�����F{SqS\[W������#����R��������3��&KR~3�A��:����)d�t��I������GR�u#��0#�_��o�%&\�F������6rqX�K[d�>_����>��?�
��r09a	!X�a(������lz�u��1&���~���7�Q����7,�?��,�Nv���_�S�V��o�o�6z�������f�����uoE��ZG�XX���V��K�(C����|/.x����C�	���o��R��m��t"=���!	?Fh7?_~@$YC��.���Y���\`���m��H>����<f�T�f�+/������4���fG������*�M�}�=X������?���8�`2�.��n����J���OK�FG?6w���({�EU�
��Qx��mJ��Z�y��<:Bs�X� �)6��Sv�����x>;Q-�����J�
�
lw����h<�b�����N���A��W�f��1hj�O���������������������?�5�:|@��S�/G�E�I���U�S�h	-I��j�i5O�9$VR�'��=Gr�����_�����\Q�\���p���%F�Rd���h����d��$�R��7�����K���b�ox��OfS���N���dD9�;�s��UZL��hXQ,xR�y�O����DQ���j��9U�4��=+t����N}|��|���bd�!����:�� ?e��O���1�`��G{Z���3��+����^�jHJL.�d�
����@�Q���hd�^t�b�*���u��>��hv�� �c+A���9T��-�f���Y���	zmR�D���K�ou	�>M{N�����+������=�r�R�p�X�o
���A#����h��Qz���.?��r��������F����_����:(��������05I*��t�R�g������<�	���W���
�0�������������+��n��E&�� �'����Z��7%���wP/����C�������=8����������X�f��a��7����1G���tbH
�$��K����|�ls.Zc�bz�llRcx���.b����f��bs�L��?�+���3E7!S��7"��������Y���hzcQ���
+��$3\_�C���%�_���YX@r'�� 4a���D.��P�w+z}a|�)<�+~�g��H|�������e����Z}C	��u�KG�Q��
�^��������,�T�I�A����YEr��A��j���?P��(xU�������-p�Cvz����n�g��t��`���r�:��]��jJ4{��������d��3F�Q�� �`w�p{�'{�o�����S<�3�P�%��3����0���i����n����9V���y�MzUo|!'r�un�uv����(�����E�\�`�&��K��� A�����
��>��~�]^D�>���#�`�����{$t6���T������H;l��,
�ig�O/��{h7	��dh�w-����������:J^��&�A���]Ggg�0@)1D�8�W�n�x_[7��{��5����	�:W��h[J��������j�H�*�����;���"�����p`�q������-��������M������v����	�g>q����1c�K<��3sm��z��c�<=�?z�����d6��8�Qr��
�sQ
d�/��Ggc\pI�l�P2Y��|��v���������Q���������������v����X���c�)�����L��
\�g�����fF.�Q4���XrT?����'6_a?4^��`���a�W4]?1�9l��?_ks�@������R>���!�w?`9�j��s@,z���~J�zk�4;�diK;/����&�$����y�k�C��I_z?��~���{=$)����X�������gFE�,��}H��"��;�7����2���M\���[2��{}'Vsm��(>���~Z��IJy���,�(%%NG~<-��5���
_���AcG"s3:�!]�H�H��E�Nf�������f
���2|#<�z^�(1
����|�^.�r���P���Y	�h���9i����Z���j
"��m9�+n���c�M��/?��JOz���F|ma�.������b
���-��
�OY�Xcz���{����	�!�x�_y8jc����ctRV;v���+����_����b*$'U�X�[o4�R��0���=fz���'/�����?_��|�����������?_��<��Z��_�u��Z����� ja�_���+���������?_��|����������C<�F����vqq������fe��EX������x�������?��u�
#2Shulgin, Oleksandr
oleksandr.shulgin@zalando.de
In reply to: Petr Jelinek (#1)
Re: pglogical - logical replication contrib module

On Fri, Jan 1, 2016 at 12:34 AM, Petr Jelinek <petr@2ndquadrant.com> wrote:

Hi,

I'd like to submit the replication solution which is based on the
pglogical_output [1] module (which is obviously needed for this to compile).

Hi,

Impressive stuff!

Apparently this depends on a newer, yet-to-be-published version of the
pglogical_output patch:

.../contrib/pglogical/pglogical_hooks.c: In function
‘pglogical_row_filter_hook’:
.../contrib/pglogical/pglogical_hooks.c:173:35: error: ‘struct
PGLogicalRowFilterArgs’ has no member named ‘change’
HeapTuple tup = &rowfilter_args->change->data.tp.newtuple->tuple;
^

It currently doesn't do multi-master or automatic DDL. I think DDL should

be relatively easy if somebody finishes the deparse extension as the
infrastructure for replicating arbitrary commands is present in this patch.

I wish could find the time to get back to this patch. I didn't check it in
quite a while...

+PGconn *
+pglogical_connect(const char *connstring, const char *connname)
+{
+ PGconn   *conn;
+ StringInfoData dsn;
+
+ initStringInfo(&dsn);
+ appendStringInfo(&dsn,
+ "%s fallback_application_name='%s'",
+ connstring, connname);
+
+ conn = PQconnectdb(dsn.data);

This is prone to errors when connstring is specified in URI format. A
workaround is provided in this commit for
walreceiver: b3fc6727ce54a16ae9227bcccfebfa028ac5b16f

--
Alex

#3Craig Ringer
craig@2ndquadrant.com
In reply to: Shulgin, Oleksandr (#2)
1 attachment(s)
Re: pglogical - logical replication contrib module

On 5 January 2016 at 00:46, Shulgin, Oleksandr <oleksandr.shulgin@zalando.de

wrote:

On Fri, Jan 1, 2016 at 12:34 AM, Petr Jelinek <petr@2ndquadrant.com>
wrote:

Hi,

I'd like to submit the replication solution which is based on the
pglogical_output [1] module (which is obviously needed for this to compile).

Hi,

Impressive stuff!

Apparently this depends on a newer, yet-to-be-published version of the
pglogical_output patch:

.../contrib/pglogical/pglogical_hooks.c: In function
‘pglogical_row_filter_hook’:
.../contrib/pglogical/pglogical_hooks.c:173:35: error: ‘struct
PGLogicalRowFilterArgs’ has no member named ‘change’
HeapTuple tup = &rowfilter_args->change->data.tp.newtuple->tuple;

Good point. Looks like I forgot to push. Done now:

https://github.com/2ndquadrant/postgres/tree/dev/pglogical-output

https://github.com/2ndQuadrant/postgres/tree/pglogical-output-v5

I hadn't posted the rev to -hackers yet because I still have to finish
SGMLifying the docs before it can be a real candidate for inclusion. The
current docs have drifted a little as a result of that WIP. I'm not really
working for another week and a half though, so I might as well post the
current status as-is.

Still have to finish the docs conversion but that's the only remaining open
item.

Note that this patch has 9.4 support. I'd be pretty happy to be able to
retain that, mostly to avoid the need to carry a backported version as a
separately packaged extension, but I'm not sure what the general opinion
will be on that.

+PGconn *
+pglogical_connect(const char *connstring, const char *connname)
+{
+ PGconn   *conn;
+ StringInfoData dsn;
+
+ initStringInfo(&dsn);
+ appendStringInfo(&dsn,
+ "%s fallback_application_name='%s'",
+ connstring, connname);
+
+ conn = PQconnectdb(dsn.data);

This is prone to errors when connstring is specified in URI format. A
workaround is provided in this commit for
walreceiver: b3fc6727ce54a16ae9227bcccfebfa028ac5b16f

Thanks for the heads-up there.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Attachments:

pglogical-v5.patchtext/x-patch; charset=UTF-8; name=pglogical-v5.patchDownload
From 6904c298cfcd9086b9ef08bbc2d4dd5574c109b8 Mon Sep 17 00:00:00 2001
From: Craig Ringer <craig@2ndquadrant.com>
Date: Mon, 2 Nov 2015 19:34:21 +0800
Subject: [PATCH] Add contrib/pglogical_output, a logical decoding plugin

pglogical v4
---
 contrib/Makefile                                   |   2 +
 contrib/pglogical_output/.gitignore                |   7 +
 contrib/pglogical_output/Makefile                  |  71 +++
 contrib/pglogical_output/README.md                 | 612 +++++++++++++++++++++
 contrib/pglogical_output/doc/.gitignore            |   1 +
 contrib/pglogical_output/doc/DESIGN.md             | 230 ++++++++
 contrib/pglogical_output/doc/protocol.txt          | 544 ++++++++++++++++++
 contrib/pglogical_output/expected/basic_json.out   | 140 +++++
 contrib/pglogical_output/expected/basic_json_1.out | 139 +++++
 contrib/pglogical_output/expected/basic_native.out | 113 ++++
 contrib/pglogical_output/expected/cleanup.out      |   4 +
 .../pglogical_output/expected/encoding_json.out    |  59 ++
 contrib/pglogical_output/expected/extension.out    |  14 +
 contrib/pglogical_output/expected/hooks_json.out   | 204 +++++++
 contrib/pglogical_output/expected/hooks_json_1.out | 202 +++++++
 contrib/pglogical_output/expected/hooks_native.out | 104 ++++
 .../pglogical_output/expected/params_native.out    | 133 +++++
 .../pglogical_output/expected/params_native_1.out  | 134 +++++
 contrib/pglogical_output/expected/prep.out         |  26 +
 contrib/pglogical_output/pglogical_config.c        | 526 ++++++++++++++++++
 contrib/pglogical_output/pglogical_config.h        |  55 ++
 contrib/pglogical_output/pglogical_hooks.c         | 235 ++++++++
 contrib/pglogical_output/pglogical_hooks.h         |  23 +
 contrib/pglogical_output/pglogical_infofuncs.c     |  55 ++
 .../pglogical_output/pglogical_output--1.0.0.sql   |  11 +
 contrib/pglogical_output/pglogical_output.c        | 569 +++++++++++++++++++
 .../pglogical_output/pglogical_output.control.in   |   4 +
 contrib/pglogical_output/pglogical_output.h        | 111 ++++
 contrib/pglogical_output/pglogical_output/README   |   7 +
 contrib/pglogical_output/pglogical_output/compat.h |  28 +
 contrib/pglogical_output/pglogical_output/hooks.h  |  73 +++
 contrib/pglogical_output/pglogical_proto.c         |  49 ++
 contrib/pglogical_output/pglogical_proto.h         |  61 ++
 contrib/pglogical_output/pglogical_proto_json.c    | 204 +++++++
 contrib/pglogical_output/pglogical_proto_json.h    |  32 ++
 contrib/pglogical_output/pglogical_proto_native.c  | 513 +++++++++++++++++
 contrib/pglogical_output/pglogical_proto_native.h  |  38 ++
 contrib/pglogical_output/pglogical_relmetacache.c  | 194 +++++++
 contrib/pglogical_output/pglogical_relmetacache.h  |  19 +
 contrib/pglogical_output/regression.conf           |   2 +
 contrib/pglogical_output/sql/basic_json.sql        |  24 +
 contrib/pglogical_output/sql/basic_native.sql      |  37 ++
 contrib/pglogical_output/sql/basic_setup.sql       |  62 +++
 contrib/pglogical_output/sql/basic_teardown.sql    |   4 +
 contrib/pglogical_output/sql/cleanup.sql           |   4 +
 contrib/pglogical_output/sql/encoding_json.sql     |  58 ++
 contrib/pglogical_output/sql/extension.sql         |   7 +
 contrib/pglogical_output/sql/hooks_json.sql        |  49 ++
 contrib/pglogical_output/sql/hooks_native.sql      |  48 ++
 contrib/pglogical_output/sql/hooks_setup.sql       |  37 ++
 contrib/pglogical_output/sql/hooks_teardown.sql    |  10 +
 contrib/pglogical_output/sql/params_native.sql     | 104 ++++
 contrib/pglogical_output/sql/prep.sql              |  30 +
 contrib/pglogical_output_plhooks/.gitignore        |   1 +
 contrib/pglogical_output_plhooks/Makefile          |  13 +
 .../README.pglogical_output_plhooks                | 158 ++++++
 .../pglogical_output_plhooks--1.0.sql              |  89 +++
 .../pglogical_output_plhooks.c                     | 414 ++++++++++++++
 .../pglogical_output_plhooks.control               |   4 +
 59 files changed, 6701 insertions(+)
 create mode 100644 contrib/pglogical_output/.gitignore
 create mode 100644 contrib/pglogical_output/Makefile
 create mode 100644 contrib/pglogical_output/README.md
 create mode 100644 contrib/pglogical_output/doc/.gitignore
 create mode 100644 contrib/pglogical_output/doc/DESIGN.md
 create mode 100644 contrib/pglogical_output/doc/protocol.txt
 create mode 100644 contrib/pglogical_output/expected/basic_json.out
 create mode 100644 contrib/pglogical_output/expected/basic_json_1.out
 create mode 100644 contrib/pglogical_output/expected/basic_native.out
 create mode 100644 contrib/pglogical_output/expected/cleanup.out
 create mode 100644 contrib/pglogical_output/expected/encoding_json.out
 create mode 100644 contrib/pglogical_output/expected/extension.out
 create mode 100644 contrib/pglogical_output/expected/hooks_json.out
 create mode 100644 contrib/pglogical_output/expected/hooks_json_1.out
 create mode 100644 contrib/pglogical_output/expected/hooks_native.out
 create mode 100644 contrib/pglogical_output/expected/params_native.out
 create mode 100644 contrib/pglogical_output/expected/params_native_1.out
 create mode 100644 contrib/pglogical_output/expected/prep.out
 create mode 100644 contrib/pglogical_output/pglogical_config.c
 create mode 100644 contrib/pglogical_output/pglogical_config.h
 create mode 100644 contrib/pglogical_output/pglogical_hooks.c
 create mode 100644 contrib/pglogical_output/pglogical_hooks.h
 create mode 100644 contrib/pglogical_output/pglogical_infofuncs.c
 create mode 100644 contrib/pglogical_output/pglogical_output--1.0.0.sql
 create mode 100644 contrib/pglogical_output/pglogical_output.c
 create mode 100644 contrib/pglogical_output/pglogical_output.control.in
 create mode 100644 contrib/pglogical_output/pglogical_output.h
 create mode 100644 contrib/pglogical_output/pglogical_output/README
 create mode 100644 contrib/pglogical_output/pglogical_output/compat.h
 create mode 100644 contrib/pglogical_output/pglogical_output/hooks.h
 create mode 100644 contrib/pglogical_output/pglogical_proto.c
 create mode 100644 contrib/pglogical_output/pglogical_proto.h
 create mode 100644 contrib/pglogical_output/pglogical_proto_json.c
 create mode 100644 contrib/pglogical_output/pglogical_proto_json.h
 create mode 100644 contrib/pglogical_output/pglogical_proto_native.c
 create mode 100644 contrib/pglogical_output/pglogical_proto_native.h
 create mode 100644 contrib/pglogical_output/pglogical_relmetacache.c
 create mode 100644 contrib/pglogical_output/pglogical_relmetacache.h
 create mode 100644 contrib/pglogical_output/regression.conf
 create mode 100644 contrib/pglogical_output/sql/basic_json.sql
 create mode 100644 contrib/pglogical_output/sql/basic_native.sql
 create mode 100644 contrib/pglogical_output/sql/basic_setup.sql
 create mode 100644 contrib/pglogical_output/sql/basic_teardown.sql
 create mode 100644 contrib/pglogical_output/sql/cleanup.sql
 create mode 100644 contrib/pglogical_output/sql/encoding_json.sql
 create mode 100644 contrib/pglogical_output/sql/extension.sql
 create mode 100644 contrib/pglogical_output/sql/hooks_json.sql
 create mode 100644 contrib/pglogical_output/sql/hooks_native.sql
 create mode 100644 contrib/pglogical_output/sql/hooks_setup.sql
 create mode 100644 contrib/pglogical_output/sql/hooks_teardown.sql
 create mode 100644 contrib/pglogical_output/sql/params_native.sql
 create mode 100644 contrib/pglogical_output/sql/prep.sql
 create mode 100644 contrib/pglogical_output_plhooks/.gitignore
 create mode 100644 contrib/pglogical_output_plhooks/Makefile
 create mode 100644 contrib/pglogical_output_plhooks/README.pglogical_output_plhooks
 create mode 100644 contrib/pglogical_output_plhooks/pglogical_output_plhooks--1.0.sql
 create mode 100644 contrib/pglogical_output_plhooks/pglogical_output_plhooks.c
 create mode 100644 contrib/pglogical_output_plhooks/pglogical_output_plhooks.control

diff --git a/contrib/Makefile b/contrib/Makefile
index bd251f6..028fd9a 100644
--- a/contrib/Makefile
+++ b/contrib/Makefile
@@ -35,6 +35,8 @@ SUBDIRS = \
 		pg_stat_statements \
 		pg_trgm		\
 		pgcrypto	\
+		pglogical_output \
+		pglogical_output_plhooks \
 		pgrowlocks	\
 		pgstattuple	\
 		postgres_fdw	\
diff --git a/contrib/pglogical_output/.gitignore b/contrib/pglogical_output/.gitignore
new file mode 100644
index 0000000..6cdc3f1
--- /dev/null
+++ b/contrib/pglogical_output/.gitignore
@@ -0,0 +1,7 @@
+pglogical_output.so
+results/
+regression.diffs
+tmp_install/
+tmp_check/
+log/
+pglogical_output.control
diff --git a/contrib/pglogical_output/Makefile b/contrib/pglogical_output/Makefile
new file mode 100644
index 0000000..e63aa21
--- /dev/null
+++ b/contrib/pglogical_output/Makefile
@@ -0,0 +1,71 @@
+MODULE_big = pglogical_output
+PGFILEDESC = "pglogical_output - logical replication output plugin"
+
+OBJS = pglogical_output.o pglogical_hooks.o pglogical_config.o \
+	   pglogical_proto.o pglogical_proto_native.o \
+	   pglogical_proto_json.o pglogical_relmetacache.o \
+	   pglogical_infofuncs.o
+
+REGRESS = prep params_native basic_native hooks_native basic_json hooks_json encoding_json extension cleanup
+
+EXTENSION = pglogical_output
+DATA = pglogical_output--1.0.0.sql
+EXTRA_CLEAN += pglogical_output.control
+
+
+ifdef USE_PGXS
+
+# For regression checks
+# http://www.postgresql.org/message-id/CAB7nPqTsR5o3g-fBi6jbsVdhfPiLFWQ_0cGU5=94Rv_8W3qvFA@mail.gmail.com
+# this makes "make check" give a useful error
+abs_top_builddir = .
+NO_TEMP_INSTALL = yes
+# Usual recipe
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+
+# These don't do anything yet, since temp install is disabled
+EXTRA_INSTALL += ./examples/hooks
+REGRESS_OPTS += --temp-config=regression.conf
+
+plhooks:
+	make -C examples/hooks USE_PGXS=1 clean install
+
+installcheck: plhooks
+
+else
+
+subdir = contrib/pglogical_output
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+
+# 'make installcheck' disabled when building in-tree because these tests
+# require "wal_level=logical", which typical installcheck users do not have
+# (e.g. buildfarm clients).
+installcheck:
+	;
+
+EXTRA_INSTALL += contrib/pglogical_output_plhooks
+EXTRA_REGRESS_OPTS += --temp-config=./regression.conf
+
+install: all
+
+endif
+
+# The # in #define is taken as a comment, per https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=142043
+# so it must be escaped. The $ placeholders in awk must be doubled too.
+pglogical_output_version=$(shell awk '/\#define PGLOGICAL_OUTPUT_VERSION[ \t]+\".*\"/ { print substr($$3,2,length($$3)-2) }' pglogical_output.h )
+
+all: pglogical_output.control
+
+pglogical_output.control: pglogical_output.control.in pglogical_output.h
+	sed 's/__PGLOGICAL_OUTPUT_VERSION__/$(pglogical_output_version)/' pglogical_output.control.in > pglogical_output.control
+
+install: header_install
+
+header_install: pglogical_output/compat.h pglogical_output/hooks.h
+	$(MKDIR_P) '$(DESTDIR)$(includedir)'/pglogical_output
+	$(INSTALL_DATA) pglogical_output/compat.h '$(DESTDIR)$(includedir)'/pglogical_output
+	$(INSTALL_DATA) pglogical_output/hooks.h '$(DESTDIR)$(includedir)'/pglogical_output
diff --git a/contrib/pglogical_output/README.md b/contrib/pglogical_output/README.md
new file mode 100644
index 0000000..548f1d5
--- /dev/null
+++ b/contrib/pglogical_output/README.md
@@ -0,0 +1,612 @@
+# `pglogical` Output Plugin
+
+This is the [logical decoding](http://www.postgresql.org/docs/current/static/logicaldecoding.html)
+[output plugin](http://www.postgresql.org/docs/current/static/logicaldecoding-output-plugin.html)
+for `pglogical`. Its purpose is to extract a change stream from a PostgreSQL
+database and send it to a client over a network connection using a
+well-defined, efficient protocol that multiple different applications can
+consume.
+
+The primary purpose of `pglogical_output` is to supply data to logical
+streaming replication solutions, but any application can potentially use its
+data stream. The output stream is designed to be compact and fast to decode,
+and the plugin supports upstream filtering of data so that only the required
+information is sent.
+
+Only one database is replicated, rather than the whole PostgreSQL install. A
+subset of that database may be selected for replication, currently based on
+table and on replication origin. Filtering by a WHERE clause can be supported
+easily in future.
+
+No triggers are required to collect the change stream and no external ticker or
+other daemon is required. It's accumulated using
+[replication slots](http://www.postgresql.org/docs/current/static/logicaldecoding-explanation.html#AEN66446),
+as supported in PostgreSQL 9.4 or newer, and sent on top of the
+[PostgreSQL streaming replication protocol](http://www.postgresql.org/docs/current/static/protocol-replication.html).
+
+Unlike block-level ("physical") streaming replication, the change stream from
+the `pglogical` output plugin is compatible across different PostgreSQL
+versions and can even be consumed by non-PostgreSQL clients.
+
+Because logical decoding is used, only the changed rows are sent on the wire.
+There's no index change data, no vacuum activity, etc transmitted.
+
+The use of a replication slot means that the change stream is reliable and
+crash-safe. If the client disconnects or crashes it can reconnect and resume
+replay from the last message that client processed. Server-side changes that
+occur while the client is disconnected are accumulated in the queue to be sent
+when the client reconnects. This reliability also means that server-side
+resources are consumed whether or not a client is connected.
+
+# Why another output plugin?
+
+See [`DESIGN.md`](DESIGN.md) for a discussion of why using one of the existing
+generic logical decoding output plugins like `wal2json` to drive a logical
+replication downstream isn't ideal. It's mostly about speed.
+
+# Architecture and high level interaction
+
+The output plugin is loaded by a PostgreSQL walsender process when a client
+connects to PostgreSQL using the PostgreSQL wire protocol with connection
+option `replication=database`, then uses
+[the `CREATE_REPLICATION_SLOT ... LOGICAL ...` or `START_REPLICATION SLOT ... LOGICAL ...` commands](http://www.postgresql.org/docs/current/static/logicaldecoding-walsender.html) to start streaming changes. (It can also be used via
+[SQL level functions](http://www.postgresql.org/docs/current/static/logicaldecoding-sql.html)
+over a non-replication connection, but this is mainly for debugging purposes).
+
+The client supplies parameters to the  `START_REPLICATION SLOT ... LOGICAL ...`
+command to specify the version of the `pglogical` protocol it supports,
+whether it wants binary format, etc.
+
+The output plugin processes the connection parameters and the connection enters
+streaming replication protocol mode, sometimes called "COPY BOTH" mode because
+it's based on the protocol used for the `COPY` command.  PostgreSQL then calls
+functions in this plugin to send it a stream of transactions to decode and
+translate into network messages. This stream of changes continues until the
+client disconnects.
+
+The only client-to-server interaction after startup is the sending of periodic
+feedback messages that allow the replication slot to discard no-longer-needed
+change history. The client *must* send feedback, otherwise `pg_xlog` on the
+server will eventually fill up and the server will stop working.
+
+
+# Usage
+
+The overall flow of client/server interaction is:
+
+* Client makes PostgreSQL fe/be protocol connection to server
+    * Connection options must include `replication=database` and `dbname=[...]` parameters
+    * The PostgreSQL client library can be `libpq` or anything else that supports the replication sub-protocol
+    * The same mechanisms are used for authentication and protocol encryption as for a normal non-replication connection
+* Client issues `IDENTIFY_SYSTEM`
+    * Server responds with a single row containing system identity info
+* Client issues `CREATE_REPLICATION_SLOT slotname LOGICAL 'pglogical'` if it's setting up for the first time
+    * Server responds with success info and a snapshot identifier
+    * Client may at this point use the snapshot identifier on other connections while leaving this one idle
+* Client issues `START_REPLICATION SLOT slotname LOGICAL 0/0 (...options...)` to start streaming, which loops:
+    * Server emits `pglogical` message block encapsulated in a replication protocol `CopyData` message
+    * Client receives and unwraps message, then decodes the `pglogical` message block
+    * Client intermittently sends a standby status update message to server to confirm replay
+* ... until client sends a graceful connection termination message on the fe/be protocol level or the connection is broken
+
+ The details of `IDENTIFY_SYSTEM`, `CREATE_REPLICATION_SLOT` and `START_REPLICATION` are discussed in the [replication protocol docs](http://www.postgresql.org/docs/current/static/protocol-replication.html) and will not be repeated here.
+
+## Make a replication connection
+
+To use the `pglogical` plugin you must first establish a PostgreSQL FE/BE
+protocol connection using the client library of your choice, passing
+`replication=database` as one of the connection parameters. `database` is a
+literal string and is not replaced with the database name; instead the database
+name is passed separately in the usual `dbname` parameter. Note that
+`replication` is not a GUC (configuration parameter) and may not be passed in
+the `options` parameter on the connection, it's a top-level parameter like
+`user` or `dbname`.
+
+Example connection string for `libpq`:
+
+    'user=postgres replication=database sslmode=verify-full dbname=mydb'
+
+The plug-in name to pass on logical slot creation is `'pglogical'`.
+
+Details are in the replication protocol docs.
+
+## Get system identity
+
+If required you can use the `IDENTIFY_SYSTEM` command, which reports system
+information:
+
+          systemid       | timeline |  xlogpos  | dbname | dboid
+    ---------------------+----------+-----------+--------+-------
+     6153224364663410513 |        1 | 0/C429C48 | testd  | 16385
+    (1 row)
+
+Details are in the replication protocol docs.
+
+## Create the slot if required
+
+If your application creates its own slots on first use and hasn't previously
+connected to this database on this system you'll need to create a replication
+slot. This keeps track of the client's replay state even while it's disconnected.
+
+The slot name may be anything your application wants up to a limit of 63
+characters in length. It's strongly advised that the slot name clearly identify
+the application and the host it runs on.
+
+Pass `pglogical` as the plugin name.
+
+e.g.
+
+    CREATE_REPLICATION_SLOT "reporting_host_42" LOGICAL "pglogical";
+
+`CREATE_REPLICATION_SLOT` returns a snapshot identifier that may be used with
+[`SET TRANSACTION SNAPSHOT`](http://www.postgresql.org/docs/current/static/sql-set-transaction.html)
+to see the database's state as of the moment of the slot's creation. The first
+change streamed from the slot will be the change immediately after this
+snapshot was taken. The snapshot is useful when cloning the initial state of a
+database being replicted. Applications that want to see the change stream
+going forward, but don't care about the initial state, can ignore this. The
+snapshot is only valid as long as the connection that issued the
+`CREATE_REPLICATION_SLOT` remains open and has not run another command.
+
+## Send replication parameters
+
+The client now sends:
+
+    START_REPLICATION SLOT "the_slot_name" LOGICAL (
+	'Expected_encoding', 'UTF8',
+	'Max_proto_major_version', '1',
+	'Min_proto_major_version', '1',
+	...moreparams...
+    );
+
+to start replication.
+
+The parameters are very important for ensuring that the plugin accepts
+the replication request and streams changes in the expected form. `pglogical`
+parameters are discussed in the separate `pglogical` protocol documentation.
+
+## Process the startup message
+
+`pglogical`'s output plugin will send a `CopyData` message containing its
+startup message as the first protocol message. This message contains a
+set of key/value entries describing the capabilities of the upstream output
+plugin, its version and the Pg version, the tuple format options selected,
+etc.
+
+The downstream client may choose to cleanly close the connection and disconnect
+at this point if it doesn't like the reply. It might then inform the user
+or reconnect with different parameters based on what it learned from the
+first connection's startup message.
+
+## Consume the change stream
+
+`pglogical`'s output plugin now sends a continuous series of `CopyData`
+protocol messages, each of which encapsulates a `pglogical` protocol message
+as documented in the separate protocol docs.
+
+These messages provide information about transaction boundaries, changed
+rows, etc.
+
+The stream continues until the client disconnects, the upstream server is
+restarted, the upstream walsender is terminated by admin action, there's
+a network issue, or the connection is otherwise broken.
+
+The client should send periodic feedback messages to the server to acknowledge
+that it's replayed to a given point and let the server release the resources
+it's holding in case that change stream has to be replayed again. See
+["Hot standby feedback message" in the replication protocol docs](http://www.postgresql.org/docs/current/static/protocol-replication.html)
+for details.
+
+## Disconnect gracefully
+
+Disconnection works just like any normal client; you use your client library's
+usual method for closing the connection. No special action is required before
+disconnection, though it's usually a good idea to send a final standby status
+message just before you disconnect.
+
+# Tests
+
+The `pg_regress` tests check invalid parameter handling and basic
+functionality.  They're intended for use by the buildfarm using an in-tree
+`make check`, but may also be run with an out-of-tree PGXS build against an
+existing PostgreSQL install using `make USE_PGXS=1 clean installcheck`.
+
+The tests may fail on installations that are not utf-8 encoded because the
+payloads of the binary protocol output will have text in different encodings,
+which aren't visible to psql as text to be decoded. Avoiding anything except
+7-bit ascii in the tests *should* prevent the problem.
+
+# Changeset forwarding
+
+It's possible to use `pglogical_output` to cascade replication between multiple
+PostgreSQL servers, in combination with an appropriate client to apply the
+changes to the downstreams.
+
+There are two forwarding modes:
+
+* Forward everything. Transactions are replicated whether they were made directly
+  on the immediate upstream or some other node upstream of it. All rows from all
+  transactions are sent.
+
+  Selected by not setting a row or transaction filter hook.
+
+* Filtered forwarding. Transactions are replicated unless a client-supplied
+  transaction filter hook says to skip this transaction. Row changes are
+  replicated unless the client-supplied row filter hook (if provided) says to
+  skip that row.
+
+  Selected by installing a transaction and/or row filter hook (see "hooks").
+
+If the upstream server is 9.5 or newer the server will enable changeset origin
+information. It will set `forward_changeset_origins` to true in the startup
+reply message to indicate this. It will then send changeset origin messages
+after the `BEGIN` for each transaction, per the protocol documentation. Origin
+messages are omitted for transactions originating directly on the immediate
+upstream to save bandwidth.  If `forward_changeset_origins` is true then
+transactions without an origin are always from the immediate upstream that’s
+running the decoding plugin.
+
+Note that 9.4 servers can't expose replication origin information so they pass
+zero to the row filter hook and don't call the transaction filter hook.
+
+Clients may use this facility to form arbitrarily complex topologies when
+combined with hooks to determine which transactions are forwarded. An obvious
+case is bi-directional (mutual) replication.
+
+# Selective replication
+
+By specifying a row filter hook it's possible to filter the replication stream
+server-side so that only a subset of changes is replicated.
+
+
+# Hooks
+
+`pglogical_output` exposes a number of extension points where applications can
+modify or override its behaviour.
+
+All hooks are called in their own memory context, which lasts for the duration
+of the logical decoding session. They may switch to longer lived contexts if
+needed, but are then responsible for their own cleanup.
+
+## Hook setup function
+
+The downstream must specify the fully-qualified name of a SQL-callable function
+on the server as the value of the `hooks.setup_function` client parameter.
+The SQL signature of this function is
+
+    CREATE OR REPLACE FUNCTION funcname(hooks internal, memory_context internal)
+    RETURNS void STABLE
+    LANGUAGE c AS 'MODULE_PATHNAME';
+
+Permissions are checked. This function must be callable by the user that the
+output plugin is running as. The function name *must* be schema-qualified and is
+parsed like any other qualified identifier.
+
+The function receives a pointer to a newly allocated structure of hook function
+pointers to populate as its first argument. The function must not free the
+argument.
+
+If the hooks need a private data area to store information across calls, the
+setup function should get the `MemoryContext` pointer from the 2nd argument,
+then `MemoryContextAlloc` a struct for the data in that memory context and
+store the pointer to it in `hooks->hooks_private_data`. This will then be
+accessible on future calls to hook functions. It need not be manually freed, as
+the memory context used for logical decoding will free it when it's freed.
+Don't put anything in it that needs manual cleanup.
+
+Each hook has its own C signature (defined below) and the pointers must be
+directly to the functions. Hooks that the client does not wish to set must be
+left null.
+
+An example is provided in `contrib/pglogical_output_plhooks` and the argument
+structs are defined in `pglogical_output/hooks.h`, which is installed into the
+PostgreSQL source tree when the extension is installed.
+
+Each hook that is enabled results in a new startup parameter being emitted in
+the startup reply message. Clients must check for these and must not assume a
+hook was successfully activated because no error is seen.
+
+Hook functions are called in the context of the backend doing logical decoding.
+Except for the startup hook, hooks see the catalog state as it was at the time
+the transaction or row change being examined was made. Access to to non-catalog
+tables is unsafe unless they have the `user_catalog_table` reloption set.
+
+## Startup hook
+
+The startup hook is called when logical decoding starts.
+
+This hook can inspect the parameters passed by the client to the output
+plugin as in_params. These parameters *must not* be modified.
+
+It can add new parameters to the set to be returned to the client in the
+startup parameters message, by appending to List out_params, which is
+initially NIL. Each element must be a `DefElem` with the param name
+as the `defname` and a `String` value as the arg, as created with
+`makeDefElem(...)`. It and its contents must be allocated in the
+logical decoding memory context.
+
+For walsender based decoding the startup hook is called only once, and
+cleanup might not be called at the end of the session.
+
+Multiple decoding sessions, and thus multiple startup hook calls, may happen
+in a session if the SQL interface for logical decoding is being used. In
+that case it's guaranteed that the cleanup hook will be called between each
+startup.
+
+When successfully enabled, the output parameter `hooks.startup_hook_enabled` is
+set to true in the startup reply message.
+
+Unlike the other hooks, this hook sees a snapshot of the database's current
+state, not a time-traveled catalog state. It is safe to access all tables from
+this hook.
+
+## Transaction filter hook
+
+The transaction filter hook can exclude entire transactions from being decoded
+and replicated based on the node they originated from.
+
+It is passed a `const TxFilterHookArgs *` containing:
+
+* The hook argument supplied by the client, if any
+* The `RepOriginId` that this transaction originated from
+
+and must return boolean, where true retains the transaction for sending to the
+client and false discards it. (Note that this is the reverse sense of the low
+level logical decoding transaction filter hook).
+
+The hook function must *not* free the argument struct or modify its contents.
+
+Note that individual changes within a transaction may have different origins to
+the transaction as a whole; see "Origin filtering" for more details. If a
+transaction is filtered out, all changes are filtered out even if their origins
+differ from that of the transaction as a whole.
+
+When successfully enabled, the output parameter
+`hooks.transaction_filter_enabled` is set to true in the startup reply message.
+
+## Row filter hook
+
+The row filter hook is called for each row. It is passed information about the
+table, the transaction origin, and the row origin.
+
+It is passed a `const RowFilterHookArgs*` containing:
+
+* The hook argument supplied by the client, if any
+* The `Relation` the change affects
+* The change type - 'I'nsert, 'U'pdate or 'D'elete
+
+It can return true to retain this row change, sending it to the client, or
+false to discard it.
+
+The function *must not* free the argument struct or modify its contents.
+
+Note that it is more efficient to exclude whole transactions with the
+transaction filter hook rather than filtering out individual rows.
+
+When successfully enabled, the output parameter
+`hooks.row_filter_enabled` is set to true in the startup reply message.
+
+## Shutdown hook
+
+The shutdown hook is called when a decoding session ends. You can't rely on
+this hook being invoked reliably, since a replication-protocol walsender-based
+session might just terminate. It's mostly useful for cleanup to handle repeated
+invocations under the SQL interface to logical decoding.
+
+You don't need a hook to free memory you allocated, unless you explicitly
+switched to a longer lived memory context like TopMemoryContext. Memory allocated
+in the hook context will be automatically when the decoding session shuts down.
+
+## Writing hooks in procedural languages
+
+You can write hooks in PL/PgSQL, etc, too, via the `pglogical_output_plhooks`
+adapter extension in `contrib`. They won't perform very well though.
+
+# Limitations
+
+The advantages of logical decoding in general and `pglogical_output` in
+particular are discussed above. There are also some limitations that apply to
+`pglogical_output`, and to Pg's logical decoding in general.
+
+(TODO: move much of this to the main logical decoding docs)
+
+Notably:
+
+## Doesn't replicate DDL
+
+Logical decoding doesn't decode catalog changes directly. So the plugin can't
+just send a `CREATE TABLE` statement when a new table is added.
+
+If the data being decoded is being applied to another PostgreSQL database then
+its table definitions must be kept in sync via some means external to the logical
+decoding plugin its self, such as:
+
+* Event triggers using DDL deparse to capture DDL changes as they happen and write them to a table to be replicated and applied on the other end; or
+* doing DDL management via tools that synchronise DDL on all nodes
+
+## Doesn't replicate global objects/shared catalog changes
+
+PostgreSQL has a number of object types that exist across all databases, stored
+in *shared catalogs*. These include:
+
+* Roles (users/groups)
+* Security labels on users and databases
+
+Such objects cannot be replicated by `pglogical_output`. They're managed with DDL that
+can't be captured within a single database and isn't decoded anyway.
+
+DDL for global object changes must be synchronized via some external means.
+
+## Mostly one-way communication
+
+Per the protocol documentation, the downstream can't send anything except
+replay progress messages to the upstream after replication begins, and can't
+re-initialise replication without a disconnect.
+
+To achieve downstream-to-upstream communication, clients can use a regular
+libpq connection to the upstream then write to tables or call functions.
+Alternately, a separate replication connection in the opposite direction can be
+created by the application to carry information from downstream to upstream.
+
+See "Protocol flow" in the protocol documentation for more information.
+
+## Physical replica failover
+
+Logical decoding cannot follow a physical replication failover because
+replication slot state is not replicated to physical replicas. If you fail over
+to a streaming replica you have to manually reconnect your logical replication
+clients, creating new slots, etc. This is a core PostgreSQL limitation.
+
+Also, there's no built-in way to guarantee that the logical replication slot
+from the failed master hasn't replayed further than the physical streaming
+replica you failed over to. You could receive changes on your logical decoding
+stream from the old master that never made it to the physical streaming
+replica. This is true (albeit very unlikely) *even if the physical streaming
+replica is synchronous* because PostgreSQL sends the replication data anyway,
+then just delays the commit's visibility on the master. Support for strictly
+ordered standbys would be required in PostgreSQL to avoid this.
+
+To achieve failover with logical replication you cannot mix in physical
+standbys. The logical replication client has to take responsibility for
+maintaining slots on logical replicas intended as failover candidates
+and for ensuring that the furthest-ahead replica is promoted if there is
+more than one.
+
+## Can only replicate complete transactions
+
+Logical decoding can only replicate a transaction after it has committed. This
+usefully skips replication of rolled back transactions, but it also means that
+very large transactions must be completed upstream before they can begin on the
+downstream, adding to replication latency.
+
+## Replicates only one transaction at a time
+
+Logical decoding serializes transactions in commit order, so pglogical_output
+cannot replay interleaved concurrent transactions. This can lead to high latencies
+when big transactions are being replayed, since smaller transactions get queued
+up behind them.
+
+## Unique index required for inserts or updates
+
+To replicate `INSERT`s or `UPDATE`s it is necessary to have a `PRIMARY KEY`
+or a (non-partial, columns-only) `UNIQUE` index on the table, so the table
+has a `REPLICA IDENTITY`. Without that `pglogical_output` doesn't know what
+old key to send to allow the receiver to tell which tuple is being updated.
+
+## UNLOGGED tables aren't replicated
+
+Because `UNLOGGED` tables aren't written to WAL, they aren't replicated by
+logical or physical replication. You can only replicate `UNLOGGED` tables
+with trigger-based solutions.
+
+## Unchanged fields are often sent in `UPDATE`
+
+Because there's no tracking of dirty/clean fields when a tuple is updated,
+logical decoding can't tell if a given field was changed by an update.
+Unchanged fields can only by identified and omitted if they're a variable
+length TOASTable type and are big enough to get stored out-of-line in
+a TOAST table.
+
+# Troubleshooting and debugging
+
+## Non-destructively previewing pending data on a slot
+
+Using the json mode of `pglogical_output` you can examine pending transactions
+on a slot without consuming them, so they are still delivered to the usual
+client application that created/owns this slot. This is best done using the SQL
+interface to logical decoding, since it gives you finer control than using
+`pg_recvlogical`.
+
+You can only peek at a slot while there is no other client connected to that
+slot.
+
+Use `pg_logical_slot_peek_changes` to examine the change stream without
+destructively consuming changes. This is extremely helpful when trying to
+determine why an error occurs in a downstream, since you can examine a
+json-ified representation of the xact. It's necessary to supply a minimal
+set of required parameters to the output plugin.
+
+e.g. given setup:
+
+    CREATE TABLE discard_test(blah text);
+    SELECT 'init' FROM pg_create_logical_replication_slot('demo_slot', 'pglogical_output');
+    INSERT INTO discard_test(blah) VALUES('one');
+    INSERT INTO discard_test(blah) VALUES('two1'),('two2'),('two3');
+    INSERT INTO discard_test(blah) VALUES('three1'),('three2');
+
+you can peek at the change stream with:
+
+     SELECT location, xid, data
+     FROM pg_logical_slot_peek_changes('demo_slot', NULL, NULL,
+              'min_proto_version', '1', 'max_proto_version', '1',
+              'startup_params_format', '1', 'proto_format', 'json');
+
+The two `NULL`s mean you don't want to stop decoding after any particular
+LSN or any particular number of changes. Decoding will stop when there's nothing
+left to decode or you cancel the query.
+
+This will emit a key/value startup message then change data rows like:
+
+     location  | xid  |                                            data
+     0/4E8AAF0 | 5562 | {"action":"B", has_catalog_changes:"f", xid:"5562", first_lsn:"0/4E8AAF0", commit_time:"2015-11-13 14:26:21.404425+08"}
+     0/4E8AAF0 | 5562 | {"action":"I","relation":["public","discard_test"],"newtuple":{"blah":"one"}}
+     0/4E8AB70 | 5562 | {"action":"C", final_lsn:"0/4E8AB30", end_lsn:"0/4E8AB70"}
+     0/4E8ABA8 | 5563 | {"action":"B", has_catalog_changes:"f", xid:"5563", first_lsn:"0/4E8ABA8", commit_time:"2015-11-13 14:26:32.015611+08"}
+     0/4E8ABA8 | 5563 | {"action":"I","relation":["public","discard_test"],"newtuple":{"blah":"two1"}}
+     0/4E8ABE8 | 5563 | {"action":"I","relation":["public","discard_test"],"newtuple":{"blah":"two2"}}
+     0/4E8AC28 | 5563 | {"action":"I","relation":["public","discard_test"],"newtuple":{"blah":"two3"}}
+     0/4E8ACA8 | 5563 | {"action":"C", final_lsn:"0/4E8AC68", end_lsn:"0/4E8ACA8"}
+     ....
+
+The output is the LSN (log sequence number) associated with a change, the top
+level transaction ID that performed the change, and the change data as json.
+
+You can see the transaction boundaries by xid changes and by the "B"egin and
+"C"ommit messages, and you can see the individual row "I"nserts. Replication
+origins, commit timestamps, etc will be shown if known.
+
+See http://www.postgresql.org/docs/current/static/functions-admin.html for
+information on the peek functions.
+
+If you want the binary format you can get that with
+`pg_logical_slot_peek_binary_changes` and the `native` protocol, but that's
+generally much less useful.
+
+# Manually discarding a change from a slot
+
+Sometimes it's desirable to manually purge one or more changes from a
+replication slot. This is usually an error recovery step when problems arise
+with the downstream code that's replaying from the slot.
+
+You can use the peek functions to determine the point in the stream you want to
+discard up to, as identifed by LSN (log sequence number). See
+"non-destructively previewing pending data on a slot" above for details.
+
+You can't control the point you start discarding from, it's always from the
+current stream position up to a point you specify. If the peek shows that
+there's data you still want to retain you must make sure that the downstream
+replays up to the point you want to keep changes and sends replay confirmation.
+In other words there's no way to cut a sequence of changes out of the middle of
+the pending change stream.
+
+Once you've peeked the stream and know the LSN you want to discard up to, you
+can use `pg_logical_slot_peek_changes`, specifying an `upto_lsn`, to consume
+changes from the slot up to but not including that point, i.e. that will be the
+point at which replay resumes.
+
+For example, if you wanted to discard the first transaction in the example
+from the section above, i.e. discard xact 5562 and start decoding at xact
+5563 from its' BEGIN lsn `0/4E8ABA8`, you'd run:
+
+      SELECT location, xid, data
+      FROM pg_logical_slot_get_changes('demo_slot', '0/4E8ABA8', NULL,
+               'min_proto_version', '1', 'max_proto_version', '1',
+               'startup_params_format', '1', 'proto_format', 'json');
+
+Note that `_get_changes` is used instead of `_peek_changes` and that
+the `upto_lsn` is `'0/4E8ABA8'` instead of `NULL`.
+
+
+
+
+
diff --git a/contrib/pglogical_output/doc/.gitignore b/contrib/pglogical_output/doc/.gitignore
new file mode 100644
index 0000000..2874bff
--- /dev/null
+++ b/contrib/pglogical_output/doc/.gitignore
@@ -0,0 +1 @@
+protocol.html
diff --git a/contrib/pglogical_output/doc/DESIGN.md b/contrib/pglogical_output/doc/DESIGN.md
new file mode 100644
index 0000000..711539d
--- /dev/null
+++ b/contrib/pglogical_output/doc/DESIGN.md
@@ -0,0 +1,230 @@
+# Design decisions
+
+Explanations of why things are done the way they are.
+
+## Why does pglogical_output exist when there's wal2json etc?
+
+`pglogical_output` does plenty more than convert logical decoding change
+messages to a wire format and send them to the client.
+
+It handles format negotiations, sender-side filtering using pluggable hooks
+(and the associated plugin handling), etc. The protocol its self is also
+important, and incorporates elements like binary datum transfer that can't be
+easily or efficiently achieved with json.
+
+## Custom binary protocol
+
+Why do we have a custom binary protocol inside the walsender / copy both protocol,
+rather than using a json message representation?
+
+Speed and compactness. It's expensive to create json, with lots of allocations.
+It's expensive to decode it too. You can't represent raw binary in json, and must
+encode it, which adds considerable overhead for some data types. Using the
+obvious, easy to decode json representations also makes it difficult to do
+later enhancements planned for the protocol and decoder, like caching row
+metadata.
+
+The protocol implementation is fairly well encapsulated, so in future it should
+be possible to emit json instead for clients that request it. Right now that's
+not the priority as tools like wal2json already exist for that.
+
+## Column metadata
+
+The output plugin sends metadata for columns - at minimum, the column names -
+before each row that first refers to that relation.
+
+The reason metadata must be sent is that the upstream and downstream table's
+attnos don't necessarily correspond. The column names might, and their ordering
+might even be the same, but any column drop or column type change will result
+in a dropped column on one side. So at the user level the tables look the same,
+but their attnos don't match, and if we rely on attno for replication we'll get
+the wrong data in the wrong columns. Not pretty.
+
+That could be avoided by requiring that the downstream table be strictly
+maintained by DDL replication, but:
+
+* We don't want to require DDL replication
+* That won't work with multiple upstreams feeding into a table
+* The initial table creation still won't be correct if the table has dropped
+  columns, unless we (ab)use `pg_dump`'s `--binary-upgrade` support to emit
+  tables with dropped columns, which we don't want to do.
+
+So despite the bandwidth cost, we need to send metadata.
+
+Support for type metadata is penciled in to the protocol so that clients that
+don't have table definitions at all - like queueing engines - can decode the
+data. That'll also permit type validation sanity checking on the apply side
+with logical replication.
+
+The upstream expects the client to cache this metadata and re-use it when data
+is sent for the relation again. Cache size controls, an LRU and purge
+notifications will be added.
+
+## Relation metadata cache size controls
+
+The relation metadata cache will have downstream size control added. The
+downstream will send a parameter indicating that it supports caching, and the
+maximum cache size desired. The option will have settings for "no cache",
+"cache unlimited" and "fixed size LRU [size specified]".
+
+Since there is no downstream-to-upstream communication after the startup params
+there's no easy way for the downstream to tell the upstream when it purges
+cache entries. So the downstream cache is a slave cache that must depend
+strictly on the upstream cache. The downstream tells the upstream how to manage
+its cache and then after that it just follows orders.
+
+To keep the caches in sync so the upstream never sends a row without knowing
+the downstream has metadata for it cached the downstream must always cache
+relation metadata when it receives it, and may not purge it from its cache
+until it receives a purge message for that relation from the upstream. If a
+new metadata message for the same relation arrives it *must* replace the old
+entry in the cache.
+
+The downstream does *not* have to promptly purge or invalidate cache entries
+when it gets purge messages from the upstream. They are just notifications that
+the upstream no longer expects the downstream to retain that cache entry and
+will re-send it if it is required again later.
+
+## Not an extension
+
+There's no extension script for pglogical_output. That's by design. We've tried
+really hard to avoid needing one, allowing applications using pglogical_output
+to entirely define any SQL level catalogs they need and interact with them
+using the hooks.
+
+That way applications don't have to deal with some of their catalog data being
+in pglogical_output extension catalogs and some being in their own.
+
+There's no issue with dump and restore that way either. The app controls it
+entirely and pglogical_output doesn't need any policy or tools for it.
+
+pglogical_output is meant to be a re-usable component of other solutions. Users
+shouldn't need to care about it directly.
+
+## Hooks
+
+Quite a bit of functionality that could be done directly in the output
+plugin is instead delegated to pluggable hooks. Replication origin filtering
+for example.
+
+That's because pglogical_output tries hard not to know anything about the
+topology of the replication cluster and leave that to applications using the
+plugin. It doesn't 
+
+
+## Hook entry point as a SQL function
+
+The hooks entry point is a SQL function that populates a passed `internal`
+struct with hook function pointers.
+
+The reason for this is that hooks are specified by a remote peer over the
+network. We can't just let the peer say "dlsym() this arbitrary function name
+and call it with these arguments" for fairly obvious security reasons. At bare
+minimum all replication using hooks would have to be superuser-only if we did
+that.
+
+The SQL entry point is only called once per decoding session and the rest of
+the calls are plain C function pointers.
+
+## The startup reply message
+
+The protocol design choices available to `pg_logical` are constrained by being
+contained in the copy-both protocol within the fe/be protocol, running as a
+logical decoding plugin. The plugin has no direct access to the network socket
+and can't send or receive messages whenever it wants, only under the control of
+the walsender and logical decoding framework.
+
+The only opportunity for the client to send data directly to the logical
+decoding plugin is in the  `START_REPLICATION` parameters, and it can't send
+anything to the client before that point.
+
+This means there's no opportunity for a multi-way step negotiation between
+client and server. We have to do all the negotiation we're going to in a single
+exchange of messages - the setup parameters and then the replication start
+message. All the client can do if it doesn't like the offer the server makes is
+disconnect and try again with different parameters.
+
+That's what the startup message is for. It reports the plugin's capabilities
+and tells the client which requested options were honoured. This gives the
+client a chance to decide if it's happy with the output plugin's decision
+or if it wants to reconnect and try again with different options. Iterative
+negotiation, effectively.
+
+## Unrecognised parameters MUST be ignored by client and server
+
+To ensure upward and downward compatibility, the output plugin must ignore
+parameters set by the client if it doesn't recognise them, and the client
+must ignore parameters it doesn't recognise in the server's startup reply
+message.
+
+This ensures that older clients can talk to newer servers and vice versa.
+
+For this to work, the server must never enable new functionality such as
+protocol message types, row formats, etc without the client explicitly
+specifying via a startup parameter that it understands the new functionality.
+Everything must be negotiated.
+
+Similarly, a newer client talking to an older server may ask the server to
+enable functionality, but it can't assume the server will actually honour that
+request. It must check the server's startup reply message to see if the server
+confirmed that it enabled the requested functionality. It might choose to
+disconnect and report an error to the user if the server didn't do what it
+asked. This can be important, e.g. when a security-significant hook is
+specified.
+
+## Support for transaction streaming
+
+Presently logical decoding requires that a transaction has committed before it
+can *begin* sending it to the client. This means long running xacts can take 2x
+as long, since we can't start apply on the replica until the xact is committed
+on the master.
+
+Additionally, a big xact will cause large delays in apply of smaller
+transactions because logical decoding reoreders transactions into strict commit
+order and replays them in that sequence. Small transactions that commited after
+the big transaction cannot be replayed to the replica until the big transaction
+is transferred over the wire, and we can't get a head start on that while it's
+still running.
+
+Finally, the accumulation of a big transaction in the reorder buffer means that
+storage on the upstream must be sufficient to hold the entire transaction until
+it can be streamed to the replica and discarded. That is in addition to the
+copy in retained WAL, which cannot be purged until replay is confirmed past
+commit for that xact. The temporary copy serves no data safety purpose; it can
+be regenerated from retained WAL is just a spool file.
+
+There are big upsides to waiting until commit. Rolled-back transactions and
+subtransactions are never sent at all. The apply/downstream side is greatly
+simplified by not needing to do transaction ordering, worry about
+interdependencies and conflicts during apply. The commit timestamp is known
+from the beginning of replay, allowing for smarter conflict resolution
+behaviour in multi-master scenarios. Nonetheless sometimes we want to be able
+to stream changes in advance of commit.
+
+So we need the ability to start streaming a transaction from the upstream as
+its changes are seen in WAL, either applying it immediately on the downstream
+or spooling it on the downstream until it's committed. This requires changes
+to the logical decoding facilities themselves, it isn't something pglogical_output
+can do alone. However, we've left room in pglogical_output to support this
+when support is added to logical decoding:
+
+* Flags in most message types let us add fields if we need to, like a
+  HAS_XID flag and an extra field for the transaction ID so we can
+  differentiate between concurrent transactions when streaming. The space
+  isn't wasted the rest of the time.
+
+* The upstream isn't allowed to send new message types, etc, without a
+  capability flag being set by the client. So for interleaved xacts we won't
+  enable them in logical decoding unless the client tells us the client is
+  prepared to cope with them by sending additional startup parameters.
+
+Note that for consistency reasons we still have to commit things in the same
+order on the downstream. The purpose of transaction streaming is to reduce the
+latency between the commit of the last xact before a big one and the first xact
+after the big one, minimising the duration of the stall in the flow of smaller
+xacts perceptible on the downstream.
+
+Transaction streaming also makes parallel apply on the downstream possible,
+though it is not necessary to have parallel apply to benefit from transaction
+streaming. Parallel apply has further complexities that are outside the scope
+of the output plugin design.
diff --git a/contrib/pglogical_output/doc/protocol.txt b/contrib/pglogical_output/doc/protocol.txt
new file mode 100644
index 0000000..afe3bb3
--- /dev/null
+++ b/contrib/pglogical_output/doc/protocol.txt
@@ -0,0 +1,544 @@
+= Pg_logical protocol
+
+pglogical_output defines a libpq subprocotol for streaming tuples, metadata,
+etc, from the decoding plugin to receivers.
+
+This protocol is an inner layer in a stack:
+
+ * tcp or unix sockets
+ ** libpq protocol
+ *** libpq replication subprotocol (COPY BOTH etc)
+ **** pg_logical output plugin => consumer protocol
+
+so clients can simply use libpq's existing replication protocol support,
+directly or via their libpq-wrapper driver.
+
+This is a binary protocol intended for compact representation.
+
+`pglogical_output` also supports a json-based text protocol with json
+representations of the same changesets, supporting all the same hooks etc,
+intended mainly for tracing/debugging/diagnostics. That protocol is not
+discussed here.
+
+== ToC
+
+== Protocol flow
+
+The protocol flow is primarily from upstream walsender/decoding plugin to the
+downstream receiver.
+
+The only information the flows downstream-to-upstream is:
+
+ * The initial parameter list sent to `START_REPLICATION`; and
+ * replay progress messages
+
+We can accept an arbitrary list of params to `START_REPLICATION`. After
+that we have no general purpose channel for information to flow upstream. That
+means we can't do a multi-step negotiation/handshake for determining the
+replication options to use, binary protocol, etc.
+
+The main form of negotiation is the client getting a "take it or leave it" set
+of settings from the server in an initial startup message sent before any
+replication data (see below) and, if it doesn't like them, reconnecting with
+different startup options.
+
+Except for the negotiation via initial parameter list and then startup message
+the protocol flow is the same as any other walsender-based logical replication
+plugin. The data stream is sent in COPY BOTH mode as a series of CopyData
+messages encapsulating replication data, and ends when the client disconnects.
+There's no facility for ending the COPY BOTH mode and returning to the
+walsender command parser to issue new commands. This is a limiation of the
+walsender interface, not pglogical_output.
+
+== Protocol messages
+
+The individual protocol messages are discussed in the following sub-sections.
+Protocol flow and logic comes in the next major section.
+
+Absolutely all top-level protocol messages begin with a message type byte.
+While represented in code as a character, this is a signed byte with no
+associated encoding.
+
+Since the PostgreSQL libpq COPY protocol supplies a message length there’s no
+need for top-level protocol messages to embed a length in their header.
+
+=== BEGIN message
+
+A stream of rows starts with a `BEGIN` message. Rows may only be sent after a
+`BEGIN` and before a `COMMIT`.
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|Message type|signed char|Literal ‘**B**’ (0x42)
+|flags|uint8| * 0-3: Reserved, client _must_ ERROR if set and not recognised.
+|lsn|uint64|“final_lsn” in decoding context - currently it means lsn of commit
+|commit time|uint64|“commit_time” in decoding context
+|remote XID|uint32|“xid” in decoding context
+|===
+
+=== Forwarded transaction origin message
+
+The message after the `BEGIN` may be a _forwarded transaction origin_ message
+indicating what upstream node the transaction came from.
+
+Sent if the immediately prior message was a `BEGIN` message, the upstream
+transaction was forwarded from another node, and replication origin forwarding
+is enabled, i.e. `forward_changeset_origins` is `t` in the startup reply
+message.
+
+A "node" could be another host, another DB on the same host, or pretty much
+anything. Whatever origin name is found gets forwarded.  The origin identifier
+is of arbitrary and application-defined format.  Applications _should_ prefix
+their origin identifier with a fixed application name part, like `bdr_`,
+`myapp_`, etc. It is application-defined what an application does with
+forwarded transactions from other applications.
+
+An origin message with a zero-length origin name indicates that the origin
+could not be identified but was (probably) not the local node. It is
+client-defined what action is taken in this case.
+
+It is a protocol error to send/receive a forwarded transaction origin message
+at any time other than immediately after a `BEGIN` message.
+
+The origin identifier is typically closely related to replication slot names
+and replication origins’ names in an application system.
+
+For more detail see _Changeset Forwarding_ in the README.
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|Message type|signed char|Literal ‘**O**’ (0x4f)
+|flags|uint8| * 0-3: Reserved, application _must_ ERROR if set and not recognised
+|origin_lsn|uint64|Log sequence number (LSN, XLogRecPtr) of the transaction’s commit record on its origin node (as opposed to the forwarding node’s commit LSN, which is ‘lsn’ in the BEGIN message)
+|origin_identifier_length|uint8|Length in bytes of origin_identifier
+|origin_identifier|signed char[origin_identifier_length]|An origin identifier of arbitrary, upstream-application-defined structure. _Should_ be text in the same encoding as the upstream database. NULL-terminated. _Should_ be 7-bit ASCII.
+|===
+
+=== COMMIT message
+A stream of rows ends with a `COMMIT` message.
+
+There is no `ROLLBACK` message because aborted transactions are not sent by the
+upstream.
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|Message type|signed char|Literal ‘**C**’ (0x43)
+|Flags|uint8| * 0-3: Reserved, client _must_ ERROR if set and not recognised
+|Commit LSN|uint64|commit_lsn in decoding commit decode callback. This is the same value as in the BEGIN message, and marks the end of the transaction.
+|End LSN|uint64|end_lsn in decoding transaction context
+|Commit time|uint64|commit_time in decoding transaction context
+|===
+
+=== INSERT, UPDATE or DELETE message
+
+After a `BEGIN` or metadata message, the downstream should expect to receive
+zero or more row change messages, composed of an insert/update/delete message
+with zero or more tuple fields, each of which has one or more tuple field
+values.
+
+The row’s relidentifier _must_ match that of the most recently preceding
+metadata message. All consecutive row messages must currently have the same
+relidentifier. (_Later extensions to add metadata caching will relax these
+requirements for clients that advertise caching support; see the documentation
+on metadata messages for more detail_).
+
+It is an error to decode rows using metadata received after the row was
+received, or using metadata that is not the most recently received metadata
+revision that still predates the row. I.e. in the sequence M1, R1, R2, M2, R3,
+M4: R1 and R2 must be decoded using M1, and R3  must be decoded using M2. It is
+an error to use M4 to decode any of the rows, to use M1 to decode R3, or to use
+M2 to decode R1 and R2.
+
+Row messages _may not_ arrive except during a transaction as delimited by `BEGIN`
+and `COMMIT` messages. It is an error to receive a row message outside a
+transaction.
+
+Any unrecognised tuple type or tuple part type is an error on the downstream
+that must result in a client disconnect and error message. Downstreams are
+expected to negotiate compatibility, and upstreams must not add new tuple types
+or tuple field types without negotiation.
+
+The downstream reads rows until the next non-row message is received. There is
+no other end marker or any indication of how many rows to expect in a sequence.
+
+==== Row message header
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|Message type|signed char|Literal ‘**I**’nsert (0x49), ‘**U**’pdate’ (0x55) or ‘**D**’elete (0x44)
+|flags|uint8|Row flags (reserved)
+|relidentifier|uint32|relidentifier that matches the table metadata message sent for this row.
+(_Not present in BDR, which sends nspname and relname instead_)
+|[tuple parts]|[composite]|
+|===
+
+One or more tuple-parts fields follow.
+
+==== Tuple fields
+
+|===
+|Tuple type|signed char|Identifies the kind of tuple being sent.
+
+|tupleformat|signed char|‘**T**’ (0x54)
+|natts|uint16|Number of fields sent in this tuple part.
+(_Present in BDR, but meaning significantly different here)_
+|[tuple field values]|[composite]|
+|===
+
+===== Tuple tupleformat compatibility
+
+Unrecognised _tupleformat_ kinds are a protocol error for the downstream.
+
+==== Tuple field value fields
+
+These message parts describe individual fields within a tuple.
+
+There are two kinds of tuple value fields, abbreviated and full. Which is being
+read is determined based on the first field, _kind_.
+
+Abbreviated tuple value fields are nothing but the message kind:
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|kind|signed char| * ‘**n**’ull (0x6e) field
+|===
+
+Full tuple value fields have a length and datum:
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|kind|signed char| * ‘**i**’nternal binary (0x62) field
+|length|int4|Only defined for kind = i\|b\|t
+|data|[length]|Data in a format defined by the table metadata and column _kind_.
+|===
+
+===== Tuple field values kind compatibility
+
+Unrecognised field _kind_ values are a protocol error for the downstream. The
+downstream may not continue processing the protocol stream after this
+point**.**
+
+The upstream may not send ‘**i**’nternal or ‘**b**’inary format values to the
+downstream without the downstream negotiating acceptance of such values. The
+downstream will also generally negotiate to receive type information to use to
+decode the values. See the section on startup parameters and the startup
+message for details.
+
+=== Table/row metadata messages
+
+Before sending changed rows for a relation, a metadata message for the relation
+must be sent so the downstream knows the namespace, table name, column names,
+optional column types, etc. A relidentifier field, an arbitrary numeric value
+unique for that relation on that upstream connection, maps the metadata to
+following rows.
+
+A client should not assume that relation metadata will be followed immediately
+(or at all) by rows, since future changes may lead to metadata messages being
+delivered at other times. Metadata messages may arrive during or between
+transactions.
+
+The upstream may not assume that the downstream retains more metadata than the
+one most recent table metadata message. This applies across all tables, so a
+client is permitted to discard metadata for table x when getting metadata for
+table y. The upstream must send a new metadata message before sending rows for
+a different table, even if that metadata was already sent in the same session
+or even same transaction. _This requirement will later be weakened by the
+addition of client metadata caching, which will be advertised to the upstream
+with an output plugin parameter._
+
+Columns in metadata messages are numbered from 0 to natts-1, reading
+consecutively from start to finish. The column numbers do not have to be a
+complete description of the columns in the upstream relation, so long as all
+columns that will later have row values sent are described. The upstream may
+choose to omit columns it doesn’t expect to send changes for in any given
+series of rows. Column numbers are not necessarily stable across different sets
+of metadata for the same table, even if the table hasn’t changed structurally.
+
+A metadata message may not be used to decode rows received before that metadata
+message.
+
+==== Table metadata header
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|Message type|signed char|Literal ‘**R**’ (0x52)
+|flags|uint8| * 0-6: Reserved, client _must_ ERROR if set and not recognised.
+|relidentifier|uint32|Arbitrary relation id, unique for this upstream. In practice this will probably be the upstream table’s oid, but the downstream can’t assume anything.
+|nspnamelength|uint8|Length of namespace name
+|nspname|signed char[nspnamelength]|Relation namespace (null terminated)
+|relnamelength|uint8|Length of relation name
+|relname|char[relname]|Relation name (null terminated)
+|attrs block|signed char|Literal: ‘**A**’ (0x41)
+|natts|uint16|number of attributes
+|[fields]|[composite]|Sequence of ‘natts’ column metadata blocks, each of which begins with a column delimiter followed by zero or more column metadata blocks, each with the same column metadata block header.
+
+This chunked format is used so that new metadata messages can be added without breaking existing clients.
+|===
+
+==== Column delimiter
+
+Each column’s metadata begins with a column metadata header. This comes
+immediately after the natts field in the table metadata header or after the
+last metadata block in the prior column.
+
+It has the same char header as all the others, and the flags field is the same
+size as the length field in other blocks, so it’s safe to read this as a column
+metadata block header.
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|blocktype|signed char|‘**C**’ (0x43) - column
+|flags|uint8|Column info flags
+|===
+
+==== Column metadata block header
+
+All column metadata blocks share the same header, which is the same length as a
+column delimiter:
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|blocktype|signed char|Identifies the kind of metadata block that follows.
+|blockbodylength|uint16|Length of block in bytes, excluding blocktype char and length field.
+|===
+
+==== Column name block
+
+This block just carries the name of the column, nothing more. It begins with a
+column metadata block, and the rest of the message is the column name.
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|[column metadata block header]|[composite]|blocktype = ‘**N**’ (0x4e)
+|colname|char[blockbodylength]|Column name.
+|===
+
+
+==== Column type block
+
+T.B.D.
+
+Not defined in first protocol revision.
+
+Likely to send a type identifier (probably the upstream oid) as a reference to
+a “type info” protocol message to be delivered before. Then we can cache the
+type descriptions and avoid repeating long schemas and names, just using the
+oids.
+
+Needs to have room to handle:
+
+ * built-in core types
+ * extension types (ext version may vary)
+ * enum types (CREATE TYPE … AS ENUM)
+ * range types (CREATE TYPE … AS RANGE)
+ * composite types (CREATE TYPE … AS (...))
+ * custom types (CREATE TYPE ( input = x_in, output = x_out ))
+
+… some of which can be nested
+
+== Startup message
+
+After processing output plugin arguments, the upstream output plugin must send
+a startup message as its first message on the wire. It is a trivial header
+followed by alternating key and value strings represented as null-terminated
+unsigned char strings.
+
+This message specifies the capabilities the output plugin enabled and describes
+the upstream server and plugin. This may change how the client decodes the data
+stream, and/or permit the client to disconnect and report an error to the user
+if the result isn’t acceptable.
+
+If replication is rejected because the client is incompatible or the server is
+unable to satisfy required options, the startup message may be followed by a
+libpq protocol FATAL message that terminates the session. See “Startup errors”
+below.
+
+The parameter names and values are sent as alternating key/value pairs as
+null-terminated strings, e.g.
+
++“key1\0parameter1\0key2\0value2\0”+
+
+|===
+|*Message*|*Type/Size*|*Notes*
+
+|Message type|signed char|‘**S**’ (0x53) - startup
+|Startup message version|uint8|Value is always “1”.
+|(parameters)|null-terminated key/value pairs|See table below for parameter definitions.
+|===
+
+=== Startup message parameters 
+
+Since all parameter values are sent as strings, the value types given below specify what the value must be reasonably interpretable as.
+
+|===
+|*Key name*|*Value type*|*Description*
+
+|max_proto_version|integer|Newest version of the protocol supported by output plugin.
+|min_proto_version|integer|Oldest protocol version supported by server.
+|proto_format|text|Protocol format requested. native (documented here) or json. Default is native.
+|coltypes|boolean|Column types will be sent in table metadata.
+|pg_version_num|integer|PostgreSQL server_version_num of server, if it’s PostgreSQL. e.g. 090400
+|pg_version|string|PostgreSQL server_version of server, if it’s PostgreSQL.
+|pg_catversion|uint32|Version of the PostgreSQL system catalogs on the upstream server, if it’s PostgreSQL.
+|binary|_set of parameters, specified separately_|See “_the __‘binary’__ parameters_” below, and “_Parameters relating to exchange of binary values_”
+|database_encoding|string|The native text encoding of the database the plugin is running in
+|encoding|string|Field values for textual data will be in this encoding in native protocol text, binary or internal representation. For the native protocol this is currently always the same as `database_encoding`. For text-mode json protocol this is always the same as `client_encoding`.
+|forward_changeset_origins|bool|Tells the client that the server will send changeset origin information. See “_Changeset forwarding_” for details.
+|no_txinfo|bool|Requests that variable transaction info such as XIDs, LSNs, and timestamps be omitted from output. Mainly for tests. Currently ignored for protos other than json.
+|===
+
+
+The ‘binary’ parameter set:
+== 
+|===
+|*Key name*|*Value type*|*Description*
+
+|binary.internal_basetypes|boolean|If true, PostgreSQL internal binary representations for row field data may be used for some or all row fields, if here the type is appropriate and the binary compatibility parameters of upstream and downstream match. See binary.want_internal_basetypes in the output plugin parameters for details.
+
+May only be true if _binary.want_internal_basetypes_ was set to true by the client in the parameters and the client’s accepted binary format matches that of the server.
+|binary.binary_basetypes|boolean|If true, external binary format (send/recv format) may be used for some or all row field data where the field type is a built-in base type whose send/recv format is compatible with binary.binary_pg_version .
+
+May only be set if _binary.want_binary_basetypes_ was set to true by the client in the parameters and the client’s accepted send/recv format matches that of the server.
+|binary.binary_pg_version|uint16|The PostgreSQL major version that send/recv format values will be compatible with. This is not necessarily the actual upstream PostgreSQL version.
+|binary.sizeof_int|uint8|sizeof(int) on the upstream.
+|binary.sizeof_long|uint8|sizeof(long) on the upstream.
+|binary.sizeof_datum|uint8|Same as sizeof_int, but for the PostgreSQL Datum typedef.
+|binary.maxalign|uint8|Upstream PostgreSQL server’s MAXIMUM_ALIGNOF value - platform dependent, determined at build time.
+|binary.bigendian|bool|True iff the upstream is big-endian.
+|binary.float4_byval|bool|Upstream PostgreSQL’s float4_byval compile option.
+|binary.float8_byval|bool|Upstream PostgreSQL’s float8_byval compile option.
+|binary.integer_datetimes|bool|Whether TIME, TIMESTAMP and TIMESTAMP WITH TIME ZONE will be sent using integer or floating point representation.
+
+Usually this is the value of the upstream PostgreSQL’s integer_datetimes compile option.
+|===
+== Startup errors
+
+If the server rejects the client’s connection - due to non-overlapping protocol
+support, unrecognised parameter formats, unsupported required parameters like
+hooks, etc - then it will follow the startup reply message with a
++++<u>+++normal libpq protocol error message+++</u>+++. (Current versions send
+this before the startup message).
+
+== Arguments client supplies to output plugin
+
+The one opportunity for the downstream client to send information (other than replay feedback) to the upstream is at connect-time, as an array of arguments to the output plugin supplied to START LOGICAL REPLICATION.
+
+There is no back-and-forth, no handshake.
+
+As a result, the client mainly announces capabilities and makes requests of the output plugin. The output plugin will ERROR if required parameters are unset, or where incompatibilities that cannot be resolved are found. Otherwise the output plugin reports what it could and could not honour in the startup message it sends as the first message on the wire down to the client. The client chooses whether to continue replay or to disconnect and report an error to the user, then possibly reconnect with different options.
+
+=== Output plugin arguments
+
+The output plugin’s key/value arguments are specified in pairs, as key and value. They’re what’s passed to START_REPLICATION, etc.
+
+All parameters are passed in text form. They _should_ be limited to 7-bit ASCII, since the server’s text encoding is not known, but _may_ be normalized precomposed UTF-8. The types specified for parameters indicate what the output plugin should attempt to convert the text into. Clients should not send text values that are outside the range for that type.
+
+==== Capabilities
+
+Many values are capabilities flags for the client, indicating that it understands optional features like metadata caching, binary format transfers, etc. In general the output plugin _may_ disregard capabilities the client advertises as supported and act as if they are not supported. If a capability is advertised as unsupported or is not advertised the output plugin _must not_ enable the corresponding features.
+
+In other words, don’t send the client something it’s not expecting.
+
+==== Protocol versioning
+
+Two parameters max_proto_version and min_proto_version, which clients must always send, allow negotiation of the protocol version. The output plugin must ERROR if the client protocol support does not overlap its own protocol support range.
+
+The protocol version is only incremented when there are major breaking changes that all or most clients must be modified to accommodate. Most changes are done by adding new optional messages and/or by having clients advertise capabilities to opt in to features.
+
+Because these versions are expected to be incremented, to make it clear that the format of the startup parameters themselves haven’t changed, the first key/value pair _must_ be the parameter startup_params_format with value “1”.
+
+|===
+|*Key*|*Type*|*Value(s)*|*Notes*
+
+|startup_params_format|int8|1|The format version of this startup parameter set. Always the digit 1 (0x31), null terminated.
+|max_proto_version|int32|1|Newest version of the protocol supported by client. Output plugin must ERROR if supported version too old. *Required*, ERROR if missing.
+|min_proto_version|int32|1|Oldest version of the protocol supported by client. Output plugin must ERROR if supported version too old. *Required*, ERROR if missing.
+|===
+
+==== Client requirements and capabilities
+
+|===
+|*Key*|*Type*|*Default*|*Notes*
+
+|expected_encoding|string|null|The text encoding the downstream expects field values to be in. Applies to text, binary and internal representations of field values in native format. Has no effect on other protocol content. If specified, the upstream must honour it. For json protocol, must be unset or match `client_encoding`. (Current plugin versions ERROR if this is set for the native protocol and not equal to the upstream database's encoding).
+|want_coltypes|boolean|false|The client wants to receive data type information about columns.
+|===
+
+==== General client information
+
+These keys tell the output plugin about the client. They’re mainly for informational purposes. In particular, the versions must _not_ be used to determine compatibility for binary or send/recv format, as non-PostgreSQL clients will simply not send them at all but may still understand binary or send/recv format fields.
+
+|===
+|*Key*|*Type*|*Default*|*Notes*
+
+|pg_version_num|integer|null|PostgreSQL server_version_num of client, if it’s PostgreSQL. e.g. 090400
+|pg_version|string|null|PostgreSQL server_version of client, if it’s PostgreSQL.
+|===
+
+
+==== Parameters relating to exchange of binary values
+
+The downstream may specify to the upstream that it is capable of understanding binary (PostgreSQL internal binary datum format), and/or send/recv (PostgreSQL binary interchange) format data by setting the binary.want_binary_basetypes and/or binary.want_internal_basetypes options, or other yet-to-be-defined options.
+
+An upstream output plugin that does not support one or both formats _may_ ignore the downstream’s binary support and send text format, in which case it may ignore all binary. parameters. All downstreams _must_ support text format. An upstream output plugin _must not_ send binary or send/recv format unless the downstream has announced it can receive it. If both upstream and downstream support both formats an upstream should prefer binary format and fall back to send/recv, then to text, if compatibility requires.
+
+Internal and binary format selection should be done on a type-by-type basis. It is quite normal to send ‘text’ format for extension types while sending binary for built-in types.
+
+The downstream _must_ specify its compatibility requirements for internal and binary data if it requests either or both formats. The upstream _must_ honour these by falling back from binary to send/recv, and from send/recv to text, where the upstream and downstream are not compatible.
+
+An unspecified compatibility field _must_ presumed to be unsupported by the downstream so that older clients that don’t know about a change in a newer version don’t receive unexpected data. For example, in the unlikely event that PostgreSQL 99.8 switched to 128-bit DPD (Densely Packed Decimal) representations of NUMERIC instead of the current arbitrary-length BCD (Binary Coded Decimal) format, a new binary.dpd_numerics parameter would be added. Clients that didn’t know about the change wouldn’t know to set it, so the upstream would presume it unsupported and send text format NUMERIC to those clients. This also means that clients that support the new format wouldn’t be able to receive the old format in binary from older servers since they’d specify dpd_numerics = true in their compatibility parameters.
+
+At this time a downstream may specify compatibility with only one value for a given option; i.e. a downstream cannot say it supports both 4-byte and 8-byte sizeof(int).  Leaving it unspecified means the upstream must assume the downstream supports neither. (A future protocol extension may allow clients to specify alternative sets of supported formats).
+
+The `pg_version` option _must not_ be used to decide compatibility. Use `binary.basetypes_major_version` instead.
+
+|===
+|*Key name*|*Value type*|*Default*|*Description*
+
+|binary.want_binary_basetypes|boolean|false|True if the client accepts binary interchange (send/recv) format rows for PostgreSQL built-in base types.
+|binary.want_internal_basetypes|boolean|false|True if the client accepts PostgreSQL internal-format binary output for base PostgreSQL types not otherwise specified elsewhere.
+|binary.basetypes_major_version|uint16|null|The PostgreSQL major version (x.y) the downstream expects binary and send/recv format values to be in. Represented as an integer in XXYY format (no leading zero since it’s an integer), e.g. 9.5 is 905. This corresponds to PG_VERSION_NUM/100 in PostgreSQL.
+|binary.sizeof_int|uint8|+null+|sizeof(int) on the downstream.
+|binary.sizeof_long|uint8|null|sizeof(long) on the downstream.
+|binary.sizeof_datum|uint8|null|Same as sizeof_int, but for the PostgreSQL Datum typedef.
+|binary.maxalign|uint8|null|Downstream PostgreSQL server’s maxalign value - platform dependent, determined at build time.
+|binary.bigendian|bool|null|True iff the downstream is big-endian.
+|binary.float4_byval|bool|null|Downstream PostgreSQL’s float4_byval compile option.
+|binary.float8_byval|bool|null|Downstream PostgreSQL’s float8_byval compile option.
+|binary.integer_datetimes|bool|null|Downstream PostgreSQL’s integer_datetimes compile option.
+|===
+
+== Extensibility
+
+Because of the use of optional parameters in output plugin arguments, and the
+confirmation/response sent in the startup packet, a basic handshake is possible
+between upstream and downstream, allowing negotiation of capabilities.
+
+The output plugin must never send non-optional data or change its wire format
+without confirmation from the client that it can understand the new data. It
+may send optional data without negotiation.
+
+When extending the output plugin arguments, add-ons are expected to prefix all
+keys with the extension name, and should preferably use a single top level key
+with a json object value to carry their extension information. Additions to the
+startup message should follow the same pattern.
+
+Hooks and plugins can be used to add functionality specific to a client.
+
+== JSON protocol
+
+If `proto_format` is set to `json` then the output plugin will emit JSON
+instead of the custom binary protocol. JSON support is intended mainly for
+debugging and diagnostics.
+
+The JSON format supports all the same hooks.
diff --git a/contrib/pglogical_output/expected/basic_json.out b/contrib/pglogical_output/expected/basic_json.out
new file mode 100644
index 0000000..c48948e
--- /dev/null
+++ b/contrib/pglogical_output/expected/basic_json.out
@@ -0,0 +1,140 @@
+\i sql/basic_setup.sql
+SET synchronous_commit = on;
+-- Schema setup
+CREATE TABLE demo (
+	seq serial primary key,
+	tx text,
+	ts timestamp,
+	jsb jsonb,
+	js json,
+	ba bytea
+);
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+ ?column? 
+----------
+ init
+(1 row)
+
+-- Queue up some work to decode with a variety of types
+INSERT INTO demo(tx) VALUES ('textval');
+INSERT INTO demo(ba) VALUES (BYTEA '\xDEADBEEF0001');
+INSERT INTO demo(ts, tx) VALUES (TIMESTAMP '2045-09-12 12:34:56.00', 'blah');
+INSERT INTO demo(js, jsb) VALUES ('{"key":"value"}', '{"key":"value"}');
+-- Rolled back txn
+BEGIN;
+DELETE FROM demo;
+INSERT INTO demo(tx) VALUES ('blahblah');
+ROLLBACK;
+-- Multi-statement transaction with subxacts
+BEGIN;
+SAVEPOINT sp1;
+INSERT INTO demo(tx) VALUES ('row1');
+RELEASE SAVEPOINT sp1;
+SAVEPOINT sp2;
+UPDATE demo SET tx = 'update-rollback' WHERE tx = 'row1';
+ROLLBACK TO SAVEPOINT sp2;
+SAVEPOINT sp3;
+INSERT INTO demo(tx) VALUES ('row2');
+INSERT INTO demo(tx) VALUES ('row3');
+RELEASE SAVEPOINT sp3;
+SAVEPOINT sp4;
+DELETE FROM demo WHERE tx = 'row2';
+RELEASE SAVEPOINT sp4;
+SAVEPOINT sp5;
+UPDATE demo SET tx = 'updated' WHERE tx = 'row1';
+COMMIT;
+-- txn with catalog changes
+BEGIN;
+CREATE TABLE cat_test(id integer);
+INSERT INTO cat_test(id) VALUES (42);
+COMMIT;
+-- Aborted subxact with catalog changes
+BEGIN;
+INSERT INTO demo(tx) VALUES ('1');
+SAVEPOINT sp1;
+ALTER TABLE demo DROP COLUMN tx;
+ROLLBACK TO SAVEPOINT sp1;
+INSERT INTO demo(tx) VALUES ('2');
+COMMIT;
+-- Simple decode with text-format tuples
+TRUNCATE TABLE json_decoding_output;
+INSERT INTO json_decoding_output(ch, rn)
+SELECT
+  data::jsonb,
+  row_number() OVER ()
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+SELECT * FROM get_startup_params();
+               key                |  value  
+----------------------------------+---------
+ binary.binary_basetypes          | "f"
+ binary.float4_byval              | "t"
+ binary.float8_byval              | "t"
+ binary.internal_basetypes        | "f"
+ binary.sizeof_datum              | "8"
+ binary.sizeof_int                | "4"
+ binary.sizeof_long               | "8"
+ coltypes                         | "f"
+ database_encoding                | "UTF8"
+ encoding                         | "UTF8"
+ forward_changeset_origins        | "t"
+ hooks.row_filter_enabled         | "f"
+ hooks.shutdown_hook_enabled      | "f"
+ hooks.startup_hook_enabled       | "f"
+ hooks.transaction_filter_enabled | "f"
+ max_proto_version                | "1"
+ min_proto_version                | "1"
+ no_txinfo                        | "t"
+ pglogical_output_version         | "10000"
+ relmeta_cache_size               | "0"
+(20 rows)
+
+SELECT * FROM get_queued_data();
+                                                                             data                                                                             
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "textval", "jsb": null, "seq": 1}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": "\\xdeadbeef0001", "js": null, "ts": null, "tx": null, "jsb": null, "seq": 2}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": "2045-09-12T12:34:56", "tx": "blah", "jsb": null, "seq": 3}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": null, "js": {"key": "value"}, "ts": null, "tx": null, "jsb": {"key": "value"}, "seq": 4}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "row1", "jsb": null, "seq": 6}, "relation": ["public", "demo"]}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "row2", "jsb": null, "seq": 7}, "relation": ["public", "demo"]}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "row3", "jsb": null, "seq": 8}, "relation": ["public", "demo"]}
+ {"action": "D", "oldtuple": {"ba": null, "js": null, "ts": null, "tx": null, "jsb": null, "seq": 7}, "relation": ["public", "demo"]}
+ {"action": "U", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "updated", "jsb": null, "seq": 6}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "t"}
+ {"action": "I", "newtuple": {"id": 42}, "relation": ["public", "cat_test"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "1", "jsb": null, "seq": 9}, "relation": ["public", "demo"]}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "2", "jsb": null, "seq": 10}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "t"}
+ {"action": "C"}
+(28 rows)
+
+TRUNCATE TABLE json_decoding_output;
+\i sql/basic_teardown.sql
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+ ?column? 
+----------
+ drop
+(1 row)
+
+DROP TABLE demo;
+DROP TABLE cat_test;
diff --git a/contrib/pglogical_output/expected/basic_json_1.out b/contrib/pglogical_output/expected/basic_json_1.out
new file mode 100644
index 0000000..85da990
--- /dev/null
+++ b/contrib/pglogical_output/expected/basic_json_1.out
@@ -0,0 +1,139 @@
+\i sql/basic_setup.sql
+SET synchronous_commit = on;
+-- Schema setup
+CREATE TABLE demo (
+	seq serial primary key,
+	tx text,
+	ts timestamp,
+	jsb jsonb,
+	js json,
+	ba bytea
+);
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+ ?column? 
+----------
+ init
+(1 row)
+
+-- Queue up some work to decode with a variety of types
+INSERT INTO demo(tx) VALUES ('textval');
+INSERT INTO demo(ba) VALUES (BYTEA '\xDEADBEEF0001');
+INSERT INTO demo(ts, tx) VALUES (TIMESTAMP '2045-09-12 12:34:56.00', 'blah');
+INSERT INTO demo(js, jsb) VALUES ('{"key":"value"}', '{"key":"value"}');
+-- Rolled back txn
+BEGIN;
+DELETE FROM demo;
+INSERT INTO demo(tx) VALUES ('blahblah');
+ROLLBACK;
+-- Multi-statement transaction with subxacts
+BEGIN;
+SAVEPOINT sp1;
+INSERT INTO demo(tx) VALUES ('row1');
+RELEASE SAVEPOINT sp1;
+SAVEPOINT sp2;
+UPDATE demo SET tx = 'update-rollback' WHERE tx = 'row1';
+ROLLBACK TO SAVEPOINT sp2;
+SAVEPOINT sp3;
+INSERT INTO demo(tx) VALUES ('row2');
+INSERT INTO demo(tx) VALUES ('row3');
+RELEASE SAVEPOINT sp3;
+SAVEPOINT sp4;
+DELETE FROM demo WHERE tx = 'row2';
+RELEASE SAVEPOINT sp4;
+SAVEPOINT sp5;
+UPDATE demo SET tx = 'updated' WHERE tx = 'row1';
+COMMIT;
+-- txn with catalog changes
+BEGIN;
+CREATE TABLE cat_test(id integer);
+INSERT INTO cat_test(id) VALUES (42);
+COMMIT;
+-- Aborted subxact with catalog changes
+BEGIN;
+INSERT INTO demo(tx) VALUES ('1');
+SAVEPOINT sp1;
+ALTER TABLE demo DROP COLUMN tx;
+ROLLBACK TO SAVEPOINT sp1;
+INSERT INTO demo(tx) VALUES ('2');
+COMMIT;
+-- Simple decode with text-format tuples
+TRUNCATE TABLE json_decoding_output;
+INSERT INTO json_decoding_output(ch, rn)
+SELECT
+  data::jsonb,
+  row_number() OVER ()
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+SELECT * FROM get_startup_params();
+               key                | value  
+----------------------------------+--------
+ binary.binary_basetypes          | "f"
+ binary.float4_byval              | "t"
+ binary.float8_byval              | "t"
+ binary.internal_basetypes        | "f"
+ binary.sizeof_datum              | "8"
+ binary.sizeof_int                | "4"
+ binary.sizeof_long               | "8"
+ coltypes                         | "f"
+ database_encoding                | "UTF8"
+ encoding                         | "UTF8"
+ forward_changeset_origins        | "f"
+ hooks.row_filter_enabled         | "f"
+ hooks.shutdown_hook_enabled      | "f"
+ hooks.startup_hook_enabled       | "f"
+ hooks.transaction_filter_enabled | "f"
+ max_proto_version                | "1"
+ min_proto_version                | "1"
+ no_txinfo                        | "t"
+ relmeta_cache_size               | "0"
+(19 rows)
+
+SELECT * FROM get_queued_data();
+                                                                             data                                                                             
+--------------------------------------------------------------------------------------------------------------------------------------------------------------
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "textval", "jsb": null, "seq": 1}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": "\\xdeadbeef0001", "js": null, "ts": null, "tx": null, "jsb": null, "seq": 2}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": "2045-09-12T12:34:56", "tx": "blah", "jsb": null, "seq": 3}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": null, "js": {"key": "value"}, "ts": null, "tx": null, "jsb": {"key": "value"}, "seq": 4}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "row1", "jsb": null, "seq": 6}, "relation": ["public", "demo"]}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "row2", "jsb": null, "seq": 7}, "relation": ["public", "demo"]}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "row3", "jsb": null, "seq": 8}, "relation": ["public", "demo"]}
+ {"action": "D", "oldtuple": {"ba": null, "js": null, "ts": null, "tx": null, "jsb": null, "seq": 7}, "relation": ["public", "demo"]}
+ {"action": "U", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "updated", "jsb": null, "seq": 6}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "t"}
+ {"action": "I", "newtuple": {"id": 42}, "relation": ["public", "cat_test"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "1", "jsb": null, "seq": 9}, "relation": ["public", "demo"]}
+ {"action": "I", "newtuple": {"ba": null, "js": null, "ts": null, "tx": "2", "jsb": null, "seq": 10}, "relation": ["public", "demo"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "t"}
+ {"action": "C"}
+(28 rows)
+
+TRUNCATE TABLE json_decoding_output;
+\i sql/basic_teardown.sql
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+ ?column? 
+----------
+ drop
+(1 row)
+
+DROP TABLE demo;
+DROP TABLE cat_test;
diff --git a/contrib/pglogical_output/expected/basic_native.out b/contrib/pglogical_output/expected/basic_native.out
new file mode 100644
index 0000000..e9d7a6e
--- /dev/null
+++ b/contrib/pglogical_output/expected/basic_native.out
@@ -0,0 +1,113 @@
+\i sql/basic_setup.sql
+SET synchronous_commit = on;
+-- Schema setup
+CREATE TABLE demo (
+	seq serial primary key,
+	tx text,
+	ts timestamp,
+	jsb jsonb,
+	js json,
+	ba bytea
+);
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+ ?column? 
+----------
+ init
+(1 row)
+
+-- Queue up some work to decode with a variety of types
+INSERT INTO demo(tx) VALUES ('textval');
+INSERT INTO demo(ba) VALUES (BYTEA '\xDEADBEEF0001');
+INSERT INTO demo(ts, tx) VALUES (TIMESTAMP '2045-09-12 12:34:56.00', 'blah');
+INSERT INTO demo(js, jsb) VALUES ('{"key":"value"}', '{"key":"value"}');
+-- Rolled back txn
+BEGIN;
+DELETE FROM demo;
+INSERT INTO demo(tx) VALUES ('blahblah');
+ROLLBACK;
+-- Multi-statement transaction with subxacts
+BEGIN;
+SAVEPOINT sp1;
+INSERT INTO demo(tx) VALUES ('row1');
+RELEASE SAVEPOINT sp1;
+SAVEPOINT sp2;
+UPDATE demo SET tx = 'update-rollback' WHERE tx = 'row1';
+ROLLBACK TO SAVEPOINT sp2;
+SAVEPOINT sp3;
+INSERT INTO demo(tx) VALUES ('row2');
+INSERT INTO demo(tx) VALUES ('row3');
+RELEASE SAVEPOINT sp3;
+SAVEPOINT sp4;
+DELETE FROM demo WHERE tx = 'row2';
+RELEASE SAVEPOINT sp4;
+SAVEPOINT sp5;
+UPDATE demo SET tx = 'updated' WHERE tx = 'row1';
+COMMIT;
+-- txn with catalog changes
+BEGIN;
+CREATE TABLE cat_test(id integer);
+INSERT INTO cat_test(id) VALUES (42);
+COMMIT;
+-- Aborted subxact with catalog changes
+BEGIN;
+INSERT INTO demo(tx) VALUES ('1');
+SAVEPOINT sp1;
+ALTER TABLE demo DROP COLUMN tx;
+ROLLBACK TO SAVEPOINT sp1;
+INSERT INTO demo(tx) VALUES ('2');
+COMMIT;
+-- Simple decode with text-format tuples
+--
+-- It's still the logical decoding binary protocol and as such it has
+-- embedded timestamps, and pglogical its self has embedded LSNs, xids,
+-- etc. So all we can really do is say "yup, we got the expected number
+-- of messages".
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ count 
+-------
+    39
+(1 row)
+
+-- ... and send/recv binary format
+-- The main difference visible is that the bytea fields aren't encoded
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'binary.want_binary_basetypes', '1',
+	'binary.basetypes_major_version', (current_setting('server_version_num')::integer / 100)::text);
+ count 
+-------
+    39
+(1 row)
+
+-- Now enable the relation metadata cache and verify that we get the expected
+-- reduction in number of messages. Not much else we can look for.
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'relmeta_cache_size', '-1');
+ count 
+-------
+    29
+(1 row)
+
+\i sql/basic_teardown.sql
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+ ?column? 
+----------
+ drop
+(1 row)
+
+DROP TABLE demo;
+DROP TABLE cat_test;
diff --git a/contrib/pglogical_output/expected/cleanup.out b/contrib/pglogical_output/expected/cleanup.out
new file mode 100644
index 0000000..e7a02c8
--- /dev/null
+++ b/contrib/pglogical_output/expected/cleanup.out
@@ -0,0 +1,4 @@
+DROP TABLE excluded_startup_keys;
+DROP TABLE json_decoding_output;
+DROP FUNCTION get_queued_data();
+DROP FUNCTION get_startup_params();
diff --git a/contrib/pglogical_output/expected/encoding_json.out b/contrib/pglogical_output/expected/encoding_json.out
new file mode 100644
index 0000000..82c719a
--- /dev/null
+++ b/contrib/pglogical_output/expected/encoding_json.out
@@ -0,0 +1,59 @@
+SET synchronous_commit = on;
+-- This file doesn't share common setup with the native tests,
+-- since it's specific to how the text protocol handles encodings.
+CREATE TABLE enctest(blah text);
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+ ?column? 
+----------
+ init
+(1 row)
+
+SET client_encoding = 'UTF-8';
+INSERT INTO enctest(blah)
+VALUES
+('áàä'),('fl'), ('½⅓'), ('カンジ');
+RESET client_encoding;
+SET client_encoding = 'LATIN-1';
+-- Will ERROR, explicit encoding request doesn't match client_encoding
+SELECT data
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+ERROR:  expected_encoding must be unset or match client_encoding in text protocols
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- Will succeed since we don't request any encoding
+-- then ERROR because it can't turn the kanjii into latin-1
+SELECT data
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+ERROR:  character with byte sequence 0xef 0xac 0x82 in encoding "UTF8" has no equivalent in encoding "LATIN1"
+-- Will succeed since it matches the current encoding
+-- then ERROR because it can't turn the kanjii into latin-1
+SELECT data
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'LATIN-1',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+ERROR:  character with byte sequence 0xef 0xac 0x82 in encoding "UTF8" has no equivalent in encoding "LATIN1"
+RESET client_encoding;
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+ ?column? 
+----------
+ drop
+(1 row)
+
+DROP TABLE enctest;
diff --git a/contrib/pglogical_output/expected/extension.out b/contrib/pglogical_output/expected/extension.out
new file mode 100644
index 0000000..af5cda4
--- /dev/null
+++ b/contrib/pglogical_output/expected/extension.out
@@ -0,0 +1,14 @@
+CREATE EXTENSION pglogical_output;
+SELECT pglogical_output_proto_version();
+ pglogical_output_proto_version 
+--------------------------------
+                              1
+(1 row)
+
+SELECT pglogical_output_min_proto_version();
+ pglogical_output_min_proto_version 
+------------------------------------
+                                  1
+(1 row)
+
+DROP EXTENSION pglogical_output;
diff --git a/contrib/pglogical_output/expected/hooks_json.out b/contrib/pglogical_output/expected/hooks_json.out
new file mode 100644
index 0000000..1d20f5e
--- /dev/null
+++ b/contrib/pglogical_output/expected/hooks_json.out
@@ -0,0 +1,204 @@
+\i sql/hooks_setup.sql
+CREATE EXTENSION pglogical_output_plhooks;
+CREATE FUNCTION test_filter(relid regclass, action "char", nodeid text)
+returns bool stable language plpgsql AS $$
+BEGIN
+	IF nodeid <> 'foo' THEN
+	    RAISE EXCEPTION 'Expected nodeid <foo>, got <%>',nodeid;
+	END IF;
+	RETURN relid::regclass::text NOT LIKE '%_filter%';
+END
+$$;
+CREATE FUNCTION test_action_filter(relid regclass, action "char", nodeid text)
+returns bool stable language plpgsql AS $$
+BEGIN
+    RETURN action NOT IN ('U', 'D');
+END
+$$;
+CREATE FUNCTION wrong_signature_fn(relid regclass)
+returns bool stable language plpgsql as $$
+BEGIN
+END;
+$$;
+CREATE TABLE test_filter(id integer);
+CREATE TABLE test_nofilt(id integer);
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+ ?column? 
+----------
+ init
+(1 row)
+
+INSERT INTO test_filter(id) SELECT generate_series(1,10);
+INSERT INTO test_nofilt(id) SELECT generate_series(1,10);
+DELETE FROM test_filter WHERE id % 2 = 0;
+DELETE FROM test_nofilt WHERE id % 2 = 0;
+UPDATE test_filter SET id = id*100 WHERE id = 5;
+UPDATE test_nofilt SET id = id*100 WHERE id = 5;
+-- Test table filter
+TRUNCATE TABLE json_decoding_output;
+INSERT INTO json_decoding_output(ch, rn)
+SELECT
+  data::jsonb,
+  row_number() OVER ()
+FROM pg_logical_slot_peek_changes('regression_slot',
+ 	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.test_filter',
+	'pglo_plhooks.client_hook_arg', 'foo',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+SELECT * FROM get_startup_params();
+               key                |  value  
+----------------------------------+---------
+ binary.binary_basetypes          | "f"
+ binary.float4_byval              | "t"
+ binary.float8_byval              | "t"
+ binary.internal_basetypes        | "f"
+ binary.sizeof_datum              | "8"
+ binary.sizeof_int                | "4"
+ binary.sizeof_long               | "8"
+ coltypes                         | "f"
+ database_encoding                | "UTF8"
+ encoding                         | "UTF8"
+ forward_changeset_origins        | "t"
+ hooks.row_filter_enabled         | "t"
+ hooks.shutdown_hook_enabled      | "t"
+ hooks.startup_hook_enabled       | "t"
+ hooks.transaction_filter_enabled | "t"
+ max_proto_version                | "1"
+ min_proto_version                | "1"
+ no_txinfo                        | "t"
+ pglogical_output_version         | "10000"
+ relmeta_cache_size               | "0"
+(20 rows)
+
+SELECT * FROM get_queued_data();
+                                      data                                       
+---------------------------------------------------------------------------------
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"id": 1}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 2}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 3}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 4}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 5}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 6}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 7}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 8}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 9}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 10}, "relation": ["public", "test_nofilt"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "U", "newtuple": {"id": 500}, "relation": ["public", "test_nofilt"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "t"}
+ {"action": "C"}
+(25 rows)
+
+-- test action filter
+TRUNCATE TABLE json_decoding_output;
+INSERT INTO json_decoding_output (ch, rn)
+SELECT
+  data::jsonb,
+  row_number() OVER ()
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.test_action_filter',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+SELECT * FROM get_startup_params();
+               key                |  value  
+----------------------------------+---------
+ binary.binary_basetypes          | "f"
+ binary.float4_byval              | "t"
+ binary.float8_byval              | "t"
+ binary.internal_basetypes        | "f"
+ binary.sizeof_datum              | "8"
+ binary.sizeof_int                | "4"
+ binary.sizeof_long               | "8"
+ coltypes                         | "f"
+ database_encoding                | "UTF8"
+ encoding                         | "UTF8"
+ forward_changeset_origins        | "t"
+ hooks.row_filter_enabled         | "t"
+ hooks.shutdown_hook_enabled      | "t"
+ hooks.startup_hook_enabled       | "t"
+ hooks.transaction_filter_enabled | "t"
+ max_proto_version                | "1"
+ min_proto_version                | "1"
+ no_txinfo                        | "t"
+ pglogical_output_version         | "10000"
+ relmeta_cache_size               | "0"
+(20 rows)
+
+SELECT * FROM get_queued_data();
+                                      data                                      
+--------------------------------------------------------------------------------
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"id": 1}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 2}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 3}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 4}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 5}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 6}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 7}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 8}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 9}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 10}, "relation": ["public", "test_filter"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"id": 1}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 2}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 3}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 4}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 5}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 6}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 7}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 8}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 9}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 10}, "relation": ["public", "test_nofilt"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "t"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "t"}
+ {"action": "C"}
+(36 rows)
+
+TRUNCATE TABLE json_decoding_output;
+\i sql/hooks_teardown.sql
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+ ?column? 
+----------
+ drop
+(1 row)
+
+DROP TABLE test_filter;
+DROP TABLE test_nofilt;
+DROP FUNCTION test_filter(relid regclass, action "char", nodeid text);
+DROP FUNCTION test_action_filter(relid regclass, action "char", nodeid text);
+DROP FUNCTION wrong_signature_fn(relid regclass);
+DROP EXTENSION pglogical_output_plhooks;
diff --git a/contrib/pglogical_output/expected/hooks_json_1.out b/contrib/pglogical_output/expected/hooks_json_1.out
new file mode 100644
index 0000000..6e2aa5f
--- /dev/null
+++ b/contrib/pglogical_output/expected/hooks_json_1.out
@@ -0,0 +1,202 @@
+\i sql/hooks_setup.sql
+CREATE EXTENSION pglogical_output_plhooks;
+CREATE FUNCTION test_filter(relid regclass, action "char", nodeid text)
+returns bool stable language plpgsql AS $$
+BEGIN
+	IF nodeid <> 'foo' THEN
+	    RAISE EXCEPTION 'Expected nodeid <foo>, got <%>',nodeid;
+	END IF;
+	RETURN relid::regclass::text NOT LIKE '%_filter%';
+END
+$$;
+CREATE FUNCTION test_action_filter(relid regclass, action "char", nodeid text)
+returns bool stable language plpgsql AS $$
+BEGIN
+    RETURN action NOT IN ('U', 'D');
+END
+$$;
+CREATE FUNCTION wrong_signature_fn(relid regclass)
+returns bool stable language plpgsql as $$
+BEGIN
+END;
+$$;
+CREATE TABLE test_filter(id integer);
+CREATE TABLE test_nofilt(id integer);
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+ ?column? 
+----------
+ init
+(1 row)
+
+INSERT INTO test_filter(id) SELECT generate_series(1,10);
+INSERT INTO test_nofilt(id) SELECT generate_series(1,10);
+DELETE FROM test_filter WHERE id % 2 = 0;
+DELETE FROM test_nofilt WHERE id % 2 = 0;
+UPDATE test_filter SET id = id*100 WHERE id = 5;
+UPDATE test_nofilt SET id = id*100 WHERE id = 5;
+-- Test table filter
+TRUNCATE TABLE json_decoding_output;
+INSERT INTO json_decoding_output(ch, rn)
+SELECT
+  data::jsonb,
+  row_number() OVER ()
+FROM pg_logical_slot_peek_changes('regression_slot',
+ 	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.test_filter',
+	'pglo_plhooks.client_hook_arg', 'foo',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+SELECT * FROM get_startup_params();
+               key                | value  
+----------------------------------+--------
+ binary.binary_basetypes          | "f"
+ binary.float4_byval              | "t"
+ binary.float8_byval              | "t"
+ binary.internal_basetypes        | "f"
+ binary.sizeof_datum              | "8"
+ binary.sizeof_int                | "4"
+ binary.sizeof_long               | "8"
+ coltypes                         | "f"
+ database_encoding                | "UTF8"
+ encoding                         | "UTF8"
+ forward_changeset_origins        | "f"
+ hooks.row_filter_enabled         | "t"
+ hooks.shutdown_hook_enabled      | "t"
+ hooks.startup_hook_enabled       | "t"
+ hooks.transaction_filter_enabled | "t"
+ max_proto_version                | "1"
+ min_proto_version                | "1"
+ no_txinfo                        | "t"
+ relmeta_cache_size               | "0"
+(19 rows)
+
+SELECT * FROM get_queued_data();
+                                      data                                       
+---------------------------------------------------------------------------------
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"id": 1}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 2}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 3}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 4}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 5}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 6}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 7}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 8}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 9}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 10}, "relation": ["public", "test_nofilt"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "U", "newtuple": {"id": 500}, "relation": ["public", "test_nofilt"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "t"}
+ {"action": "C"}
+(25 rows)
+
+-- test action filter
+TRUNCATE TABLE json_decoding_output;
+INSERT INTO json_decoding_output (ch, rn)
+SELECT
+  data::jsonb,
+  row_number() OVER ()
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.test_action_filter',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+SELECT * FROM get_startup_params();
+               key                | value  
+----------------------------------+--------
+ binary.binary_basetypes          | "f"
+ binary.float4_byval              | "t"
+ binary.float8_byval              | "t"
+ binary.internal_basetypes        | "f"
+ binary.sizeof_datum              | "8"
+ binary.sizeof_int                | "4"
+ binary.sizeof_long               | "8"
+ coltypes                         | "f"
+ database_encoding                | "UTF8"
+ encoding                         | "UTF8"
+ forward_changeset_origins        | "f"
+ hooks.row_filter_enabled         | "t"
+ hooks.shutdown_hook_enabled      | "t"
+ hooks.startup_hook_enabled       | "t"
+ hooks.transaction_filter_enabled | "t"
+ max_proto_version                | "1"
+ min_proto_version                | "1"
+ no_txinfo                        | "t"
+ relmeta_cache_size               | "0"
+(19 rows)
+
+SELECT * FROM get_queued_data();
+                                      data                                      
+--------------------------------------------------------------------------------
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"id": 1}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 2}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 3}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 4}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 5}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 6}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 7}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 8}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 9}, "relation": ["public", "test_filter"]}
+ {"action": "I", "newtuple": {"id": 10}, "relation": ["public", "test_filter"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "I", "newtuple": {"id": 1}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 2}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 3}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 4}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 5}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 6}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 7}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 8}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 9}, "relation": ["public", "test_nofilt"]}
+ {"action": "I", "newtuple": {"id": 10}, "relation": ["public", "test_nofilt"]}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "f"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "t"}
+ {"action": "C"}
+ {"action": "B", "has_catalog_changes": "t"}
+ {"action": "C"}
+(36 rows)
+
+TRUNCATE TABLE json_decoding_output;
+\i sql/hooks_teardown.sql
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+ ?column? 
+----------
+ drop
+(1 row)
+
+DROP TABLE test_filter;
+DROP TABLE test_nofilt;
+DROP FUNCTION test_filter(relid regclass, action "char", nodeid text);
+DROP FUNCTION test_action_filter(relid regclass, action "char", nodeid text);
+DROP FUNCTION wrong_signature_fn(relid regclass);
+DROP EXTENSION pglogical_output_plhooks;
diff --git a/contrib/pglogical_output/expected/hooks_native.out b/contrib/pglogical_output/expected/hooks_native.out
new file mode 100644
index 0000000..4a547cb
--- /dev/null
+++ b/contrib/pglogical_output/expected/hooks_native.out
@@ -0,0 +1,104 @@
+\i sql/hooks_setup.sql
+CREATE EXTENSION pglogical_output_plhooks;
+CREATE FUNCTION test_filter(relid regclass, action "char", nodeid text)
+returns bool stable language plpgsql AS $$
+BEGIN
+	IF nodeid <> 'foo' THEN
+	    RAISE EXCEPTION 'Expected nodeid <foo>, got <%>',nodeid;
+	END IF;
+	RETURN relid::regclass::text NOT LIKE '%_filter%';
+END
+$$;
+CREATE FUNCTION test_action_filter(relid regclass, action "char", nodeid text)
+returns bool stable language plpgsql AS $$
+BEGIN
+    RETURN action NOT IN ('U', 'D');
+END
+$$;
+CREATE FUNCTION wrong_signature_fn(relid regclass)
+returns bool stable language plpgsql as $$
+BEGIN
+END;
+$$;
+CREATE TABLE test_filter(id integer);
+CREATE TABLE test_nofilt(id integer);
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+ ?column? 
+----------
+ init
+(1 row)
+
+INSERT INTO test_filter(id) SELECT generate_series(1,10);
+INSERT INTO test_nofilt(id) SELECT generate_series(1,10);
+DELETE FROM test_filter WHERE id % 2 = 0;
+DELETE FROM test_nofilt WHERE id % 2 = 0;
+UPDATE test_filter SET id = id*100 WHERE id = 5;
+UPDATE test_nofilt SET id = id*100 WHERE id = 5;
+-- Regular hook setup
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.test_filter',
+	'pglo_plhooks.client_hook_arg', 'foo'
+	);
+ count 
+-------
+    40
+(1 row)
+
+-- Test action filter
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.test_action_filter'
+	);
+ count 
+-------
+    53
+(1 row)
+
+-- Invalid row fiter hook function
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.nosuchfunction'
+	);
+ERROR:  function public.nosuchfunction(regclass, "char", text) does not exist
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- Hook filter functoin with wrong signature
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.wrong_signature_fn'
+	);
+ERROR:  function public.wrong_signature_fn(regclass, "char", text) does not exist
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+\i sql/hooks_teardown.sql
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+ ?column? 
+----------
+ drop
+(1 row)
+
+DROP TABLE test_filter;
+DROP TABLE test_nofilt;
+DROP FUNCTION test_filter(relid regclass, action "char", nodeid text);
+DROP FUNCTION test_action_filter(relid regclass, action "char", nodeid text);
+DROP FUNCTION wrong_signature_fn(relid regclass);
+DROP EXTENSION pglogical_output_plhooks;
diff --git a/contrib/pglogical_output/expected/params_native.out b/contrib/pglogical_output/expected/params_native.out
new file mode 100644
index 0000000..8e0bc7d
--- /dev/null
+++ b/contrib/pglogical_output/expected/params_native.out
@@ -0,0 +1,133 @@
+SET synchronous_commit = on;
+-- no need to CREATE EXTENSION as we intentionally don't have any catalog presence
+-- Instead, just create a slot.
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+ ?column? 
+----------
+ init
+(1 row)
+
+-- Minimal invocation with no data
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ data 
+------
+(0 rows)
+
+--
+-- Various invalid parameter combos:
+--
+-- Text mode is not supported for native protocol
+SELECT data FROM pg_logical_slot_get_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ERROR:  logical decoding output plugin "pglogical_output" produces binary output, but function "pg_logical_slot_get_changes(name,pg_lsn,integer,text[])" expects textual data
+-- error, only supports proto v1
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '2',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ERROR:  client sent min_proto_version=2 but we only support protocol 1 or lower
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- error, only supports proto v1
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '2',
+	'max_proto_version', '2',
+	'startup_params_format', '1');
+ERROR:  client sent min_proto_version=2 but we only support protocol 1 or lower
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- error, unrecognised startup params format
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '2');
+ERROR:  client sent startup parameters in format 2 but we only support format 1
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- Should be OK and result in proto version 1 selection, though we won't
+-- see that here.
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '2',
+	'startup_params_format', '1');
+ data 
+------
+(0 rows)
+
+-- no such encoding / encoding mismatch
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'bork',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ERROR:  unrecognised encoding name bork passed to expected_encoding
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- Different spellings of encodings are OK too
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF-8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ data 
+------
+(0 rows)
+
+-- bogus param format
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'invalid');
+ERROR:  client requested protocol invalid but only "json" or "native" are supported
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- native params format explicitly
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'native');
+ data 
+------
+(0 rows)
+
+-- relmeta cache with fixed size (not supported yet, so error)
+SELECT count(data) FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'relmeta_cache_size', '200');
+INFO:  fixed size cache not supported, forced to off
+DETAIL:  only relmeta_cache_size=0 (off) or relmeta_cache_size=-1 (unlimited) supported
+ count 
+-------
+     0
+(1 row)
+
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+ ?column? 
+----------
+ drop
+(1 row)
+
diff --git a/contrib/pglogical_output/expected/params_native_1.out b/contrib/pglogical_output/expected/params_native_1.out
new file mode 100644
index 0000000..31d9486
--- /dev/null
+++ b/contrib/pglogical_output/expected/params_native_1.out
@@ -0,0 +1,134 @@
+SET synchronous_commit = on;
+-- no need to CREATE EXTENSION as we intentionally don't have any catalog presence
+-- Instead, just create a slot.
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+ ?column? 
+----------
+ init
+(1 row)
+
+-- Minimal invocation with no data
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ data 
+------
+(0 rows)
+
+--
+-- Various invalid parameter combos:
+--
+-- Text mode is not supported for native protocol
+SELECT data FROM pg_logical_slot_get_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ERROR:  logical decoding output plugin "pglogical_output" produces binary output, but "pg_logical_slot_get_changes(name,pg_lsn,integer,text[])" expects textual data
+-- error, only supports proto v1
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '2',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ERROR:  client sent min_proto_version=2 but we only support protocol 1 or lower
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- error, only supports proto v1
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '2',
+	'max_proto_version', '2',
+	'startup_params_format', '1');
+ERROR:  client sent min_proto_version=2 but we only support protocol 1 or lower
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- error, unrecognised startup params format
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '2');
+ERROR:  client sent startup parameters in format 2 but we only support format 1
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- Should be OK and result in proto version 1 selection, though we won't
+-- see that here.
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '2',
+	'startup_params_format', '1');
+ data 
+------
+(0 rows)
+
+-- no such encoding / encoding mismatch
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'bork',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ERROR:  unrecognised encoding name bork passed to expected_encoding
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- Different spellings of encodings are OK too
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF-8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+ data 
+------
+(0 rows)
+
+-- bogus param format
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'invalid');
+ERROR:  client requested protocol invalid but only "json" or "native" are supported
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+-- native params format explicitly
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'native');
+ data 
+------
+(0 rows)
+
+-- relmeta cache with fixed size (not supported yet, so error)
+SELECT count(data) FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'relmeta_cache_size', '200');
+INFO:  fixed size cache not supported, forced to off
+DETAIL:  only relmeta_cache_size=0 (off) or relmeta_cache_size=-1 (unlimited) supported
+CONTEXT:  slot "regression_slot", output plugin "pglogical_output", in the startup callback
+ count 
+-------
+     0
+(1 row)
+
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+ ?column? 
+----------
+ drop
+(1 row)
+
diff --git a/contrib/pglogical_output/expected/prep.out b/contrib/pglogical_output/expected/prep.out
new file mode 100644
index 0000000..6501951
--- /dev/null
+++ b/contrib/pglogical_output/expected/prep.out
@@ -0,0 +1,26 @@
+CREATE TABLE excluded_startup_keys (key_name text primary key);
+INSERT INTO excluded_startup_keys
+VALUES
+('pg_version_num'),('pg_version'),('pg_catversion'),('binary.basetypes_major_version'),('binary.integer_datetimes'),('binary.bigendian'),('binary.maxalign'),('binary.binary_pg_version'),('sizeof_int'),('sizeof_long'),('sizeof_datum');
+CREATE UNLOGGED TABLE json_decoding_output(ch jsonb, rn integer);
+CREATE OR REPLACE FUNCTION get_startup_params()
+RETURNS TABLE ("key" text, "value" jsonb)
+LANGUAGE sql
+AS $$
+SELECT key, value
+FROM json_decoding_output
+CROSS JOIN LATERAL jsonb_each(ch -> 'params')
+WHERE rn = 1
+  AND key NOT IN (SELECT * FROM excluded_startup_keys)
+  AND ch ->> 'action' = 'S'
+ORDER BY key;
+$$;
+CREATE OR REPLACE FUNCTION get_queued_data()
+RETURNS TABLE (data jsonb)
+LANGUAGE sql
+AS $$
+SELECT ch
+FROM json_decoding_output
+WHERE rn > 1
+ORDER BY rn ASC;
+$$;
diff --git a/contrib/pglogical_output/pglogical_config.c b/contrib/pglogical_output/pglogical_config.c
new file mode 100644
index 0000000..a9c9921
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_config.c
@@ -0,0 +1,526 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_config.c
+ *		  Logical Replication output plugin
+ *
+ * Copyright (c) 2012-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  pglogical_config.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "pglogical_output/compat.h"
+#include "pglogical_config.h"
+#include "pglogical_output.h"
+
+#include "catalog/catversion.h"
+#include "catalog/namespace.h"
+
+#include "mb/pg_wchar.h"
+
+#include "nodes/makefuncs.h"
+
+#include "utils/builtins.h"
+#include "utils/int8.h"
+#include "utils/inval.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/relcache.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+typedef enum PGLogicalOutputParamType
+{
+	OUTPUT_PARAM_TYPE_BOOL,
+	OUTPUT_PARAM_TYPE_UINT32,
+	OUTPUT_PARAM_TYPE_INT32,
+	OUTPUT_PARAM_TYPE_STRING,
+	OUTPUT_PARAM_TYPE_QUALIFIED_NAME
+} PGLogicalOutputParamType;
+
+/* param parsing */
+static Datum get_param_value(DefElem *elem, bool null_ok,
+		PGLogicalOutputParamType type);
+
+static Datum get_param(List *options, const char *name, bool missing_ok,
+					   bool null_ok, PGLogicalOutputParamType type,
+					   bool *found);
+static bool parse_param_bool(DefElem *elem);
+static uint32 parse_param_uint32(DefElem *elem);
+static int32 parse_param_int32(DefElem *elem);
+
+static void
+process_parameters_v1(List *options, PGLogicalOutputData *data);
+
+enum {
+	PARAM_UNRECOGNISED,
+	PARAM_MAX_PROTOCOL_VERSION,
+	PARAM_MIN_PROTOCOL_VERSION,
+	PARAM_PROTOCOL_FORMAT,
+	PARAM_EXPECTED_ENCODING,
+	PARAM_BINARY_BIGENDIAN,
+	PARAM_BINARY_SIZEOF_DATUM,
+	PARAM_BINARY_SIZEOF_INT,
+	PARAM_BINARY_SIZEOF_LONG,
+	PARAM_BINARY_FLOAT4BYVAL,
+	PARAM_BINARY_FLOAT8BYVAL,
+	PARAM_BINARY_INTEGER_DATETIMES,
+	PARAM_BINARY_WANT_INTERNAL_BASETYPES,
+	PARAM_BINARY_WANT_BINARY_BASETYPES,
+	PARAM_BINARY_BASETYPES_MAJOR_VERSION,
+	PARAM_PG_VERSION,
+	PARAM_HOOKS_SETUP_FUNCTION,
+	PARAM_NO_TXINFO,
+	PARAM_RELMETA_CACHE_SIZE
+} OutputPluginParamKey;
+
+typedef struct {
+	const char * const paramname;
+	int paramkey;
+} OutputPluginParam;
+
+/* Oh, if only C had switch on strings */
+static OutputPluginParam param_lookup[] = {
+	{"max_proto_version", PARAM_MAX_PROTOCOL_VERSION},
+	{"min_proto_version", PARAM_MIN_PROTOCOL_VERSION},
+	{"proto_format", PARAM_PROTOCOL_FORMAT},
+	{"expected_encoding", PARAM_EXPECTED_ENCODING},
+	{"binary.bigendian", PARAM_BINARY_BIGENDIAN},
+	{"binary.sizeof_datum", PARAM_BINARY_SIZEOF_DATUM},
+	{"binary.sizeof_int", PARAM_BINARY_SIZEOF_INT},
+	{"binary.sizeof_long", PARAM_BINARY_SIZEOF_LONG},
+	{"binary.float4_byval", PARAM_BINARY_FLOAT4BYVAL},
+	{"binary.float8_byval", PARAM_BINARY_FLOAT8BYVAL},
+	{"binary.integer_datetimes", PARAM_BINARY_INTEGER_DATETIMES},
+	{"binary.want_internal_basetypes", PARAM_BINARY_WANT_INTERNAL_BASETYPES},
+	{"binary.want_binary_basetypes", PARAM_BINARY_WANT_BINARY_BASETYPES},
+	{"binary.basetypes_major_version", PARAM_BINARY_BASETYPES_MAJOR_VERSION},
+	{"pg_version", PARAM_PG_VERSION},
+	{"hooks.setup_function", PARAM_HOOKS_SETUP_FUNCTION},
+	{"no_txinfo", PARAM_NO_TXINFO},
+	{"relmeta_cache_size", PARAM_RELMETA_CACHE_SIZE},
+	{NULL, PARAM_UNRECOGNISED}
+};
+
+/*
+ * Look up a param name to find the enum value for the
+ * param, or PARAM_UNRECOGNISED if not found.
+ */
+static int
+get_param_key(const char * const param_name)
+{
+	OutputPluginParam *param = &param_lookup[0];
+
+	do {
+		if (strcmp(param->paramname, param_name) == 0)
+			return param->paramkey;
+		param++;
+	} while (param->paramname != NULL);
+
+	return PARAM_UNRECOGNISED;
+}
+
+
+void
+process_parameters_v1(List *options, PGLogicalOutputData *data)
+{
+	Datum		val;
+	bool    	found;
+	ListCell	*lc;
+
+	/*
+	 * max_proto_version and min_proto_version are specified
+	 * as required, and must be parsed before anything else.
+	 *
+	 * TODO: We should still parse them as optional and
+	 * delay the ERROR until after the startup reply.
+	 */
+	val = get_param(options, "max_proto_version", false, false,
+					OUTPUT_PARAM_TYPE_UINT32, &found);
+	data->client_max_proto_version = DatumGetUInt32(val);
+
+	val = get_param(options, "min_proto_version", false, false,
+					OUTPUT_PARAM_TYPE_UINT32, &found);
+	data->client_min_proto_version = DatumGetUInt32(val);
+
+	/* Examine all the other params in the v1 message. */
+	foreach(lc, options)
+	{
+		DefElem    *elem = lfirst(lc);
+
+		Assert(elem->arg == NULL || IsA(elem->arg, String));
+
+		/* Check each param, whether or not we recognise it */
+		switch(get_param_key(elem->defname))
+		{
+			val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_UINT32);
+
+			case PARAM_BINARY_BIGENDIAN:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_BOOL);
+				data->client_binary_bigendian_set = true;
+				data->client_binary_bigendian = DatumGetBool(val);
+				break;
+
+			case PARAM_BINARY_SIZEOF_DATUM:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_UINT32);
+				data->client_binary_sizeofdatum = DatumGetUInt32(val);
+				break;
+
+			case PARAM_BINARY_SIZEOF_INT:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_UINT32);
+				data->client_binary_sizeofint = DatumGetUInt32(val);
+				break;
+
+			case PARAM_BINARY_SIZEOF_LONG:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_UINT32);
+				data->client_binary_sizeoflong = DatumGetUInt32(val);
+				break;
+
+			case PARAM_BINARY_FLOAT4BYVAL:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_BOOL);
+				data->client_binary_float4byval_set = true;
+				data->client_binary_float4byval = DatumGetBool(val);
+				break;
+
+			case PARAM_BINARY_FLOAT8BYVAL:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_BOOL);
+				data->client_binary_float4byval_set = true;
+				data->client_binary_float4byval = DatumGetBool(val);
+				break;
+
+			case PARAM_BINARY_INTEGER_DATETIMES:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_BOOL);
+				data->client_binary_intdatetimes_set = true;
+				data->client_binary_intdatetimes = DatumGetBool(val);
+				break;
+
+			case PARAM_PROTOCOL_FORMAT:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_STRING);
+				data->client_protocol_format = DatumGetCString(val);
+				break;
+
+			case PARAM_EXPECTED_ENCODING:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_STRING);
+				data->client_expected_encoding = DatumGetCString(val);
+				break;
+
+			case PARAM_PG_VERSION:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_UINT32);
+				data->client_pg_version = DatumGetUInt32(val);
+				break;
+
+			case PARAM_BINARY_WANT_INTERNAL_BASETYPES:
+				/* check if we want to use internal data representation */
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_BOOL);
+				data->client_want_internal_basetypes_set = true;
+				data->client_want_internal_basetypes = DatumGetBool(val);
+				break;
+
+			case PARAM_BINARY_WANT_BINARY_BASETYPES:
+				/* check if we want to use binary data representation */
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_BOOL);
+				data->client_want_binary_basetypes_set = true;
+				data->client_want_binary_basetypes = DatumGetBool(val);
+				break;
+
+			case PARAM_BINARY_BASETYPES_MAJOR_VERSION:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_UINT32);
+				data->client_binary_basetypes_major_version = DatumGetUInt32(val);
+				break;
+
+			case PARAM_HOOKS_SETUP_FUNCTION:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_QUALIFIED_NAME);
+				data->hooks_setup_funcname = (List*) PointerGetDatum(val);
+				break;
+
+			case PARAM_NO_TXINFO:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_BOOL);
+				data->client_no_txinfo = DatumGetBool(val);
+				break;
+
+			case PARAM_RELMETA_CACHE_SIZE:
+				val = get_param_value(elem, false, OUTPUT_PARAM_TYPE_INT32);
+				data->client_relmeta_cache_size = DatumGetInt32(val);
+				break;
+
+			case PARAM_UNRECOGNISED:
+				ereport(DEBUG1,
+						(errmsg("Unrecognised pglogical parameter %s ignored", elem->defname)));
+				break;
+		}
+	}
+}
+
+/*
+ * Read parameters sent by client at startup and store recognised
+ * ones in the parameters PGLogicalOutputData.
+ *
+ * The PGLogicalOutputData must have all client-supplied parameter fields
+ * zeroed, such as by memset or palloc0, since values not supplied
+ * by the client are not set.
+ */
+int
+process_parameters(List *options, PGLogicalOutputData *data)
+{
+	Datum	val;
+	bool    found;
+	int		params_format;
+
+	val = get_param(options, "startup_params_format", false, false,
+					OUTPUT_PARAM_TYPE_UINT32, &found);
+
+	params_format = DatumGetUInt32(val);
+
+	if (params_format == 1)
+	{
+		process_parameters_v1(options, data);
+	}
+
+	return params_format;
+}
+
+static Datum
+get_param_value(DefElem *elem, bool null_ok, PGLogicalOutputParamType type)
+{
+	/* Check for NULL value */
+	if (elem->arg == NULL || strVal(elem->arg) == NULL)
+	{
+		if (null_ok)
+			return (Datum) 0;
+		else
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("parameter \"%s\" cannot be NULL", elem->defname)));
+	}
+
+	switch (type)
+	{
+		case OUTPUT_PARAM_TYPE_UINT32:
+			return UInt32GetDatum(parse_param_uint32(elem));
+		case OUTPUT_PARAM_TYPE_INT32:
+			return Int32GetDatum(parse_param_int32(elem));
+		case OUTPUT_PARAM_TYPE_BOOL:
+			return BoolGetDatum(parse_param_bool(elem));
+		case OUTPUT_PARAM_TYPE_STRING:
+			return PointerGetDatum(pstrdup(strVal(elem->arg)));
+		case OUTPUT_PARAM_TYPE_QUALIFIED_NAME:
+			return PointerGetDatum(textToQualifiedNameList(cstring_to_text(pstrdup(strVal(elem->arg)))));
+		default:
+			elog(ERROR, "unknown parameter type %d", type);
+	}
+}
+
+/*
+ * Param parsing
+ *
+ * This is not exactly fast but since it's only called on replication start
+ * we'll leave it for now.
+ */
+static Datum
+get_param(List *options, const char *name, bool missing_ok, bool null_ok,
+		  PGLogicalOutputParamType type, bool *found)
+{
+	ListCell	   *option;
+
+	*found = false;
+
+	foreach(option, options)
+	{
+		DefElem    *elem = lfirst(option);
+
+		Assert(elem->arg == NULL || IsA(elem->arg, String));
+
+		/* Search until matching parameter found */
+		if (pg_strcasecmp(name, elem->defname))
+			continue;
+
+		*found = true;
+
+		return get_param_value(elem, null_ok, type);
+	}
+
+	if (!missing_ok)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("missing required parameter \"%s\"", name)));
+
+	return (Datum) 0;
+}
+
+static bool
+parse_param_bool(DefElem *elem)
+{
+	bool		res;
+
+	if (!parse_bool(strVal(elem->arg), &res))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("could not parse boolean value \"%s\" for parameter \"%s\"",
+						strVal(elem->arg), elem->defname)));
+
+	return res;
+}
+
+static uint32
+parse_param_uint32(DefElem *elem)
+{
+	int64		res;
+
+	if (!scanint8(strVal(elem->arg), true, &res))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("could not parse integer value \"%s\" for parameter \"%s\"",
+						strVal(elem->arg), elem->defname)));
+
+	if (res > PG_UINT32_MAX || res < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("value \"%s\" out of range for parameter \"%s\"",
+						strVal(elem->arg), elem->defname)));
+
+	return (uint32) res;
+}
+
+static int32
+parse_param_int32(DefElem *elem)
+{
+	int64		res;
+
+	if (!scanint8(strVal(elem->arg), true, &res))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("could not parse integer value \"%s\" for parameter \"%s\"",
+						strVal(elem->arg), elem->defname)));
+
+	if (res > PG_INT32_MAX || res < PG_INT32_MIN)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("value \"%s\" out of range for parameter \"%s\"",
+						strVal(elem->arg), elem->defname)));
+
+	return (int32) res;
+}
+
+static List*
+add_startup_msg_s(List *l, char *key, char *val)
+{
+	return lappend(l, makeDefElem(key, (Node*)makeString(val)));
+}
+
+static List*
+add_startup_msg_i(List *l, char *key, int val)
+{
+	return lappend(l, makeDefElem(key, (Node*)makeString(psprintf("%d", val))));
+}
+
+static List*
+add_startup_msg_b(List *l, char *key, bool val)
+{
+	return lappend(l, makeDefElem(key, (Node*)makeString(val ? "t" : "f")));
+}
+
+/*
+ * This builds the protocol startup message, which is always the first
+ * message on the wire after the client sends START_REPLICATION.
+ *
+ * It confirms to the client that we could apply requested options, and
+ * tells the client our capabilities.
+ *
+ * Any additional parameters provided by the startup hook are also output
+ * now.
+ *
+ * The output param 'msg' is a null-terminated char* palloc'd in the current
+ * memory context and the length 'len' of that string that is valid. The caller
+ * should pfree the result after use.
+ *
+ * This is a bit less efficient than direct pq_sendblah calls, but
+ * separates config handling from the protocol implementation, and
+ * it's not like startup msg performance matters much.
+ */
+List *
+prepare_startup_message(PGLogicalOutputData *data)
+{
+	ListCell *lc;
+	List *l = NIL;
+
+	l = add_startup_msg_s(l, "max_proto_version", "1");
+	l = add_startup_msg_s(l, "min_proto_version", "1");
+
+	/* We don't support understand column types yet */
+	l = add_startup_msg_b(l, "coltypes", false);
+
+	/* Info about our Pg host */
+	l = add_startup_msg_i(l, "pg_version_num", PG_VERSION_NUM);
+	l = add_startup_msg_s(l, "pg_version", PG_VERSION);
+	l = add_startup_msg_i(l, "pg_catversion", CATALOG_VERSION_NO);
+
+	l = add_startup_msg_s(l, "database_encoding", (char*)GetDatabaseEncodingName());
+
+	l = add_startup_msg_s(l, "encoding", (char*)pg_encoding_to_char(data->field_datum_encoding));
+
+	l = add_startup_msg_b(l, "forward_changeset_origins",
+			data->forward_changeset_origins);
+
+	/* and ourselves */
+	l = add_startup_msg_s(l, "pglogical_output_version",
+			PGLOGICAL_OUTPUT_VERSION);
+	l = add_startup_msg_i(l, "pglogical_output_version",
+			PGLOGICAL_OUTPUT_VERSION_NUM);
+
+	/* binary options enabled */
+	l = add_startup_msg_b(l, "binary.internal_basetypes",
+			data->allow_internal_basetypes);
+	l = add_startup_msg_b(l, "binary.binary_basetypes",
+			data->allow_binary_basetypes);
+
+	/* Binary format characteristics of server */
+	l = add_startup_msg_i(l, "binary.basetypes_major_version", PG_VERSION_NUM/100);
+	l = add_startup_msg_i(l, "binary.sizeof_int", sizeof(int));
+	l = add_startup_msg_i(l, "binary.sizeof_long", sizeof(long));
+	l = add_startup_msg_i(l, "binary.sizeof_datum", sizeof(Datum));
+	l = add_startup_msg_i(l, "binary.maxalign", MAXIMUM_ALIGNOF);
+	l = add_startup_msg_b(l, "binary.bigendian", server_bigendian());
+	l = add_startup_msg_b(l, "binary.float4_byval", server_float4_byval());
+	l = add_startup_msg_b(l, "binary.float8_byval", server_float8_byval());
+	l = add_startup_msg_b(l, "binary.integer_datetimes", server_integer_datetimes());
+	/* We don't know how to send in anything except our host's format */
+	l = add_startup_msg_i(l, "binary.binary_pg_version",
+			PG_VERSION_NUM/100);
+
+	l = add_startup_msg_b(l, "no_txinfo", data->client_no_txinfo);
+
+
+	/*
+	 * Confirm that we've enabled any requested hook functions.
+	 */
+	l = add_startup_msg_b(l, "hooks.startup_hook_enabled",
+			data->hooks.startup_hook != NULL);
+	l = add_startup_msg_b(l, "hooks.shutdown_hook_enabled",
+			data->hooks.shutdown_hook != NULL);
+	l = add_startup_msg_b(l, "hooks.row_filter_enabled",
+			data->hooks.row_filter_hook != NULL);
+	l = add_startup_msg_b(l, "hooks.transaction_filter_enabled",
+			data->hooks.txn_filter_hook != NULL);
+
+	/* Cache control and other misc options */
+	l = add_startup_msg_i(l, "relmeta_cache_size",
+			data->relmeta_cache_size);
+
+
+	/*
+	 * Output any extra params supplied by a startup hook by appending
+	 * them verbatim to the params list.
+	 */
+	foreach(lc, data->extra_startup_params)
+	{
+		DefElem *param = (DefElem*)lfirst(lc);
+		Assert(IsA(param->arg, String) && strVal(param->arg) != NULL);
+		l = lappend(l, param);
+	}
+
+	return l;
+}
diff --git a/contrib/pglogical_output/pglogical_config.h b/contrib/pglogical_output/pglogical_config.h
new file mode 100644
index 0000000..8f620bd
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_config.h
@@ -0,0 +1,55 @@
+#ifndef PG_LOGICAL_CONFIG_H
+#define PG_LOGICAL_CONFIG_H
+
+#ifndef PG_VERSION_NUM
+#error <postgres.h> must be included first
+#endif
+
+#include "nodes/pg_list.h"
+#include "pglogical_output.h"
+
+inline static bool
+server_float4_byval(void)
+{
+#ifdef USE_FLOAT4_BYVAL
+	return true;
+#else
+	return false;
+#endif
+}
+
+inline static bool
+server_float8_byval(void)
+{
+#ifdef USE_FLOAT8_BYVAL
+	return true;
+#else
+	return false;
+#endif
+}
+
+inline static bool
+server_integer_datetimes(void)
+{
+#ifdef USE_INTEGER_DATETIMES
+	return true;
+#else
+	return false;
+#endif
+}
+
+inline static bool
+server_bigendian(void)
+{
+#ifdef WORDS_BIGENDIAN
+	return true;
+#else
+	return false;
+#endif
+}
+
+extern int process_parameters(List *options, PGLogicalOutputData *data);
+
+extern List * prepare_startup_message(PGLogicalOutputData *data);
+
+#endif
diff --git a/contrib/pglogical_output/pglogical_hooks.c b/contrib/pglogical_output/pglogical_hooks.c
new file mode 100644
index 0000000..47aa9ba
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_hooks.c
@@ -0,0 +1,235 @@
+#include "postgres.h"
+
+#include "access/xact.h"
+
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+
+#ifdef HAVE_REPLICATION_ORIGINS
+#include "replication/origin.h"
+#endif
+
+#include "parser/parse_func.h"
+
+#include "utils/acl.h"
+#include "utils/lsyscache.h"
+
+#include "miscadmin.h"
+
+#include "pglogical_hooks.h"
+#include "pglogical_output.h"
+
+/*
+ * Returns Oid of the hooks function specified in funcname.
+ *
+ * Error is thrown if function doesn't exist or doen't return correct datatype
+ * or is volatile.
+ */
+static Oid
+get_hooks_function_oid(List *funcname)
+{
+	Oid			funcid;
+	Oid			funcargtypes[1];
+
+	funcargtypes[0] = INTERNALOID;
+
+	/* find the the function */
+	funcid = LookupFuncName(funcname, 1, funcargtypes, false);
+
+	/* Validate that the function returns void */
+	if (get_func_rettype(funcid) != VOIDOID)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("function %s must return void",
+						NameListToString(funcname))));
+	}
+
+	if (func_volatile(funcid) == PROVOLATILE_VOLATILE)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("function %s must not be VOLATILE",
+						NameListToString(funcname))));
+	}
+
+	if (pg_proc_aclcheck(funcid, GetUserId(), ACL_EXECUTE) != ACLCHECK_OK)
+	{
+		const char * username;
+#if PG_VERSION_NUM >= 90500
+		username = GetUserNameFromId(GetUserId(), false);
+#else
+		username = GetUserNameFromId(GetUserId());
+#endif
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("current user %s does not have permission to call function %s",
+					 username, NameListToString(funcname))));
+	}
+
+	return funcid;
+}
+
+/*
+ * If a hook setup function was specified in the startup parameters, look it up
+ * in the catalogs, check permissions, call it, and store the resulting hook
+ * info struct.
+ */
+void
+load_hooks(PGLogicalOutputData *data)
+{
+	Oid hooks_func;
+	MemoryContext old_ctxt;
+	bool txn_started = false;
+
+	if (!IsTransactionState())
+	{
+		txn_started = true;
+		StartTransactionCommand();
+	}
+
+	if (data->hooks_setup_funcname != NIL)
+	{
+		hooks_func = get_hooks_function_oid(data->hooks_setup_funcname);
+
+		old_ctxt = MemoryContextSwitchTo(data->hooks_mctxt);
+		(void) OidFunctionCall1(hooks_func, PointerGetDatum(&data->hooks));
+		MemoryContextSwitchTo(old_ctxt);
+
+		elog(DEBUG3, "pglogical_output: Loaded hooks from function %u. Hooks are: \n"
+				"\tstartup_hook: %p\n"
+				"\tshutdown_hook: %p\n"
+				"\trow_filter_hook: %p\n"
+				"\ttxn_filter_hook: %p\n"
+				"\thooks_private_data: %p\n",
+				hooks_func,
+				data->hooks.startup_hook,
+				data->hooks.shutdown_hook,
+				data->hooks.row_filter_hook,
+				data->hooks.txn_filter_hook,
+				data->hooks.hooks_private_data);
+	}
+
+	if (txn_started)
+		CommitTransactionCommand();
+}
+
+void
+call_startup_hook(PGLogicalOutputData *data, List *plugin_params)
+{
+	struct PGLogicalStartupHookArgs args;
+	MemoryContext old_ctxt;
+
+	if (data->hooks.startup_hook != NULL)
+	{
+		bool tx_started = false;
+
+		args.private_data = data->hooks.hooks_private_data;
+		args.in_params = plugin_params;
+		args.out_params = NIL;
+
+		elog(DEBUG3, "calling pglogical startup hook");
+
+		if (!IsTransactionState())
+		{
+			tx_started = true;
+			StartTransactionCommand();
+		}
+
+		old_ctxt = MemoryContextSwitchTo(data->hooks_mctxt);
+		(void) (*data->hooks.startup_hook)(&args);
+		MemoryContextSwitchTo(old_ctxt);
+
+		if (tx_started)
+			CommitTransactionCommand();
+
+		data->extra_startup_params = args.out_params;
+		/* The startup hook might change the private data seg */
+		data->hooks.hooks_private_data = args.private_data;
+
+		elog(DEBUG3, "called pglogical startup hook");
+	}
+}
+
+void
+call_shutdown_hook(PGLogicalOutputData *data)
+{
+	struct PGLogicalShutdownHookArgs args;
+	MemoryContext old_ctxt;
+
+	if (data->hooks.shutdown_hook != NULL)
+	{
+		args.private_data = data->hooks.hooks_private_data;
+
+		elog(DEBUG3, "calling pglogical shutdown hook");
+
+		old_ctxt = MemoryContextSwitchTo(data->hooks_mctxt);
+		(void) (*data->hooks.shutdown_hook)(&args);
+		MemoryContextSwitchTo(old_ctxt);
+
+		data->hooks.hooks_private_data = args.private_data;
+
+		elog(DEBUG3, "called pglogical shutdown hook");
+	}
+}
+
+/*
+ * Decide if the individual change should be filtered out by
+ * calling a client-provided hook.
+ */
+bool
+call_row_filter_hook(PGLogicalOutputData *data, ReorderBufferTXN *txn,
+		Relation rel, ReorderBufferChange *change)
+{
+	struct  PGLogicalRowFilterArgs hook_args;
+	MemoryContext old_ctxt;
+	bool ret = true;
+
+	if (data->hooks.row_filter_hook != NULL)
+	{
+		hook_args.change_type = change->action;
+		hook_args.private_data = data->hooks.hooks_private_data;
+		hook_args.changed_rel = rel;
+		hook_args.change = change;
+
+		elog(DEBUG3, "calling pglogical row filter hook");
+
+		old_ctxt = MemoryContextSwitchTo(data->hooks_mctxt);
+		ret = (*data->hooks.row_filter_hook)(&hook_args);
+		MemoryContextSwitchTo(old_ctxt);
+
+		/* Filter hooks shouldn't change the private data ptr */
+		Assert(data->hooks.hooks_private_data == hook_args.private_data);
+
+		elog(DEBUG3, "called pglogical row filter hook, returned %d", (int)ret);
+	}
+
+	return ret;
+}
+
+bool
+call_txn_filter_hook(PGLogicalOutputData *data, RepOriginId txn_origin)
+{
+	struct PGLogicalTxnFilterArgs hook_args;
+	bool ret = true;
+	MemoryContext old_ctxt;
+
+	if (data->hooks.txn_filter_hook != NULL)
+	{
+		hook_args.private_data = data->hooks.hooks_private_data;
+		hook_args.origin_id = txn_origin;
+
+		elog(DEBUG3, "calling pglogical txn filter hook");
+
+		old_ctxt = MemoryContextSwitchTo(data->hooks_mctxt);
+		ret = (*data->hooks.txn_filter_hook)(&hook_args);
+		MemoryContextSwitchTo(old_ctxt);
+
+		/* Filter hooks shouldn't change the private data ptr */
+		Assert(data->hooks.hooks_private_data == hook_args.private_data);
+
+		elog(DEBUG3, "called pglogical txn filter hook, returned %d", (int)ret);
+	}
+
+	return ret;
+}
diff --git a/contrib/pglogical_output/pglogical_hooks.h b/contrib/pglogical_output/pglogical_hooks.h
new file mode 100644
index 0000000..5dd9705
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_hooks.h
@@ -0,0 +1,23 @@
+#ifndef PGLOGICAL_HOOKS_H
+#define PGLOGICAL_HOOKS_H
+
+#include "replication/reorderbuffer.h"
+
+/* public interface for hooks */
+#include "pglogical_output/hooks.h"
+#include "pglogical_output.h"
+
+extern void load_hooks(PGLogicalOutputData *data);
+
+extern void call_startup_hook(PGLogicalOutputData *data, List *plugin_params);
+
+extern void call_shutdown_hook(PGLogicalOutputData *data);
+
+extern bool call_row_filter_hook(PGLogicalOutputData *data,
+		ReorderBufferTXN *txn, Relation rel, ReorderBufferChange *change);
+
+extern bool call_txn_filter_hook(PGLogicalOutputData *data,
+		RepOriginId txn_origin);
+
+
+#endif
diff --git a/contrib/pglogical_output/pglogical_infofuncs.c b/contrib/pglogical_output/pglogical_infofuncs.c
new file mode 100644
index 0000000..055cd9b
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_infofuncs.c
@@ -0,0 +1,55 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_infofuncs.c
+ *		  Logical Replication output plugin
+ *
+ * Copyright (c) 2012-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  pglogical_infofuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+#include "fmgr.h"
+
+#include "utils/builtins.h"
+
+#include "pglogical_output.h"
+
+Datum pglogical_output_version(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(pglogical_output_version);
+
+Datum pglogical_output_version_num(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(pglogical_output_version_num);
+
+Datum pglogical_output_proto_version(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(pglogical_output_proto_version);
+
+Datum pglogical_output_min_proto_version(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(pglogical_output_min_proto_version);
+
+Datum
+pglogical_output_version(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_TEXT_P(cstring_to_text(PGLOGICAL_OUTPUT_VERSION));
+}
+
+Datum
+pglogical_output_version_num(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT32(PGLOGICAL_OUTPUT_VERSION_NUM);
+}
+
+Datum
+pglogical_output_proto_version(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT32(PGLOGICAL_PROTO_VERSION_NUM);
+}
+
+Datum
+pglogical_output_min_proto_version(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT32(PGLOGICAL_PROTO_MIN_VERSION_NUM);
+}
diff --git a/contrib/pglogical_output/pglogical_output--1.0.0.sql b/contrib/pglogical_output/pglogical_output--1.0.0.sql
new file mode 100644
index 0000000..d6c330b
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_output--1.0.0.sql
@@ -0,0 +1,11 @@
+CREATE FUNCTION pglogical_output_version() RETURNS text
+LANGUAGE c AS 'MODULE_PATHNAME';
+
+CREATE FUNCTION pglogical_output_version_num() RETURNS integer
+LANGUAGE c AS 'MODULE_PATHNAME';
+
+CREATE FUNCTION pglogical_output_proto_version() RETURNS integer
+LANGUAGE c AS 'MODULE_PATHNAME';
+
+CREATE FUNCTION pglogical_output_min_proto_version() RETURNS integer
+LANGUAGE c AS 'MODULE_PATHNAME';
diff --git a/contrib/pglogical_output/pglogical_output.c b/contrib/pglogical_output/pglogical_output.c
new file mode 100644
index 0000000..ca809e7
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_output.c
@@ -0,0 +1,569 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_output.c
+ *		  Logical Replication output plugin
+ *
+ * Copyright (c) 2012-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  pglogical_output.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "pglogical_output/compat.h"
+#include "pglogical_config.h"
+#include "pglogical_output.h"
+#include "pglogical_proto.h"
+#include "pglogical_hooks.h"
+#include "pglogical_relmetacache.h"
+
+#include "access/hash.h"
+#include "access/sysattr.h"
+#include "access/xact.h"
+
+#include "catalog/pg_class.h"
+#include "catalog/pg_proc.h"
+#include "catalog/pg_type.h"
+
+#include "mb/pg_wchar.h"
+
+#include "nodes/parsenodes.h"
+
+#include "parser/parse_func.h"
+
+#include "replication/output_plugin.h"
+#include "replication/logical.h"
+#ifdef HAVE_REPLICATION_ORIGINS
+#include "replication/origin.h"
+#endif
+
+#include "utils/builtins.h"
+#include "utils/catcache.h"
+#include "utils/guc.h"
+#include "utils/int8.h"
+#include "utils/inval.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/relcache.h"
+#include "utils/syscache.h"
+#include "utils/typcache.h"
+
+PG_MODULE_MAGIC;
+
+extern void		_PG_output_plugin_init(OutputPluginCallbacks *cb);
+
+/* These must be available to pg_dlsym() */
+static void pg_decode_startup(LogicalDecodingContext * ctx,
+							  OutputPluginOptions *opt, bool is_init);
+static void pg_decode_shutdown(LogicalDecodingContext * ctx);
+static void pg_decode_begin_txn(LogicalDecodingContext *ctx,
+					ReorderBufferTXN *txn);
+static void pg_decode_commit_txn(LogicalDecodingContext *ctx,
+					 ReorderBufferTXN *txn, XLogRecPtr commit_lsn);
+static void pg_decode_change(LogicalDecodingContext *ctx,
+				 ReorderBufferTXN *txn, Relation rel,
+				 ReorderBufferChange *change);
+
+#ifdef HAVE_REPLICATION_ORIGINS
+static bool pg_decode_origin_filter(LogicalDecodingContext *ctx,
+						RepOriginId origin_id);
+#endif
+
+static void send_startup_message(LogicalDecodingContext *ctx,
+		PGLogicalOutputData *data, bool last_message);
+
+static bool startup_message_sent = false;
+
+/* specify output plugin callbacks */
+void
+_PG_output_plugin_init(OutputPluginCallbacks *cb)
+{
+	AssertVariableIsOfType(&_PG_output_plugin_init, LogicalOutputPluginInit);
+
+	cb->startup_cb = pg_decode_startup;
+	cb->begin_cb = pg_decode_begin_txn;
+	cb->change_cb = pg_decode_change;
+	cb->commit_cb = pg_decode_commit_txn;
+#ifdef HAVE_REPLICATION_ORIGINS
+	cb->filter_by_origin_cb = pg_decode_origin_filter;
+#endif
+	cb->shutdown_cb = pg_decode_shutdown;
+}
+
+static bool
+check_binary_compatibility(PGLogicalOutputData *data)
+{
+	if (data->client_binary_basetypes_major_version != PG_VERSION_NUM / 100)
+		return false;
+
+	if (data->client_binary_bigendian_set
+		&& data->client_binary_bigendian != server_bigendian())
+	{
+		elog(DEBUG1, "Binary mode rejected: Server and client endian mis-match");
+		return false;
+	}
+
+	if (data->client_binary_sizeofdatum != 0
+		&& data->client_binary_sizeofdatum != sizeof(Datum))
+	{
+		elog(DEBUG1, "Binary mode rejected: Server and client endian sizeof(Datum) mismatch");
+		return false;
+	}
+
+	if (data->client_binary_sizeofint != 0
+		&& data->client_binary_sizeofint != sizeof(int))
+	{
+		elog(DEBUG1, "Binary mode rejected: Server and client endian sizeof(int) mismatch");
+		return false;
+	}
+
+	if (data->client_binary_sizeoflong != 0
+		&& data->client_binary_sizeoflong != sizeof(long))
+	{
+		elog(DEBUG1, "Binary mode rejected: Server and client endian sizeof(long) mismatch");
+		return false;
+	}
+
+	if (data->client_binary_float4byval_set
+		&& data->client_binary_float4byval != server_float4_byval())
+	{
+		elog(DEBUG1, "Binary mode rejected: Server and client endian float4byval mismatch");
+		return false;
+	}
+
+	if (data->client_binary_float8byval_set
+		&& data->client_binary_float8byval != server_float8_byval())
+	{
+		elog(DEBUG1, "Binary mode rejected: Server and client endian float8byval mismatch");
+		return false;
+	}
+
+	if (data->client_binary_intdatetimes_set
+		&& data->client_binary_intdatetimes != server_integer_datetimes())
+	{
+		elog(DEBUG1, "Binary mode rejected: Server and client endian integer datetimes mismatch");
+		return false;
+	}
+
+	return true;
+}
+
+/* initialize this plugin */
+static void
+pg_decode_startup(LogicalDecodingContext * ctx, OutputPluginOptions *opt,
+				  bool is_init)
+{
+	PGLogicalOutputData  *data = palloc0(sizeof(PGLogicalOutputData));
+
+	data->context = AllocSetContextCreate(TopMemoryContext,
+										  "pglogical conversion context",
+										  ALLOCSET_DEFAULT_MINSIZE,
+										  ALLOCSET_DEFAULT_INITSIZE,
+										  ALLOCSET_DEFAULT_MAXSIZE);
+	data->allow_internal_basetypes = false;
+	data->allow_binary_basetypes = false;
+
+
+	ctx->output_plugin_private = data;
+
+	/*
+	 * This is replication start and not slot initialization.
+	 *
+	 * Parse and validate options passed by the client.
+	 */
+	if (!is_init)
+	{
+		int		params_format;
+
+		 /*
+		 * Ideally we'd send the startup message immediately. That way
+		 * it'd arrive before any error we emit if we see incompatible
+		 * options sent by the client here. That way the client could
+		 * possibly adjust its options and reconnect. It'd also make
+		 * sure the client gets the startup message in a timely way if
+		 * the server is idle, since otherwise it could be a while
+		 * before the next callback.
+		 *
+		 * The decoding plugin API doesn't let us write to the stream
+		 * from here, though, so we have to delay the startup message
+		 * until the first change processed on the stream, in a begin
+		 * callback.
+		 *
+		 * If we ERROR there, the startup message is buffered but not
+		 * sent since the callback didn't finish. So we'd have to send
+		 * the startup message, finish the callback and check in the
+		 * next callback if we need to ERROR.
+		 *
+		 * That's a bit much hoop jumping, so for now ERRORs are
+		 * immediate. A way to emit a message from the startup callback
+		 * is really needed to change that.
+		 */
+		startup_message_sent = false;
+
+		/* Now parse the rest of the params and ERROR if we see any we don't recognise */
+		params_format = process_parameters(ctx->output_plugin_options, data);
+
+		if (params_format != 1)
+			ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("client sent startup parameters in format %d but we only support format 1",
+					params_format)));
+
+		if (data->client_min_proto_version > PGLOGICAL_PROTO_VERSION_NUM)
+			ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("client sent min_proto_version=%d but we only support protocol %d or lower",
+					 data->client_min_proto_version, PGLOGICAL_PROTO_VERSION_NUM)));
+
+		if (data->client_max_proto_version < PGLOGICAL_PROTO_MIN_VERSION_NUM)
+			ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("client sent max_proto_version=%d but we only support protocol %d or higher",
+				 	data->client_max_proto_version, PGLOGICAL_PROTO_MIN_VERSION_NUM)));
+
+		/*
+		 * Set correct protocol format.
+		 *
+		 * This is the output plugin protocol format, this is different
+		 * from the individual fields binary vs textual format.
+		 */
+		if (data->client_protocol_format != NULL
+				&& strcmp(data->client_protocol_format, "json") == 0)
+		{
+			data->api = pglogical_init_api(PGLogicalProtoJson);
+			opt->output_type = OUTPUT_PLUGIN_TEXTUAL_OUTPUT;
+		}
+		else if ((data->client_protocol_format != NULL
+			     && strcmp(data->client_protocol_format, "native") == 0)
+				 || data->client_protocol_format == NULL)
+		{
+			data->api = pglogical_init_api(PGLogicalProtoNative);
+			opt->output_type = OUTPUT_PLUGIN_BINARY_OUTPUT;
+
+			if (data->client_no_txinfo)
+			{
+				elog(WARNING, "no_txinfo option ignored for protocols other than json");
+				data->client_no_txinfo = false;
+			}
+		}
+		else
+		{
+			ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("client requested protocol %s but only \"json\" or \"native\" are supported",
+				 	data->client_protocol_format)));
+		}
+
+		/* check for encoding match if specific encoding demanded by client */
+		if (data->client_expected_encoding != NULL
+				&& strlen(data->client_expected_encoding) != 0)
+		{
+			int wanted_encoding = pg_char_to_encoding(data->client_expected_encoding);
+
+			if (wanted_encoding == -1)
+				ereport(ERROR,
+						(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+						 errmsg("unrecognised encoding name %s passed to expected_encoding",
+								data->client_expected_encoding)));
+
+			if (opt->output_type == OUTPUT_PLUGIN_TEXTUAL_OUTPUT)
+			{
+				/*
+				 * datum encoding must match assigned client_encoding in text
+				 * proto, since everything is subject to client_encoding
+				 * conversion.
+				 */
+				if (wanted_encoding != pg_get_client_encoding())
+					ereport(ERROR,
+							(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+							 errmsg("expected_encoding must be unset or match client_encoding in text protocols")));
+			}
+			else
+			{
+				/*
+				 * currently in the binary protocol we can only emit encoded
+				 * datums in the server encoding. There's no support for encoding
+				 * conversion.
+				 */
+				if (wanted_encoding != GetDatabaseEncoding())
+					ereport(ERROR,
+							(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+							 errmsg("encoding conversion for binary datum not supported yet"),
+							 errdetail("expected_encoding %s must be unset or match server_encoding %s",
+								 data->client_expected_encoding, GetDatabaseEncodingName())));
+			}
+
+			data->field_datum_encoding = wanted_encoding;
+		}
+
+		/*
+		 * It's obviously not possible to send binary representatio of data
+		 * unless we use the binary output.
+		 */
+		if (opt->output_type == OUTPUT_PLUGIN_BINARY_OUTPUT &&
+			data->client_want_internal_basetypes)
+		{
+			data->allow_internal_basetypes =
+				check_binary_compatibility(data);
+		}
+
+		if (opt->output_type == OUTPUT_PLUGIN_BINARY_OUTPUT &&
+			data->client_want_binary_basetypes &&
+			data->client_binary_basetypes_major_version == PG_VERSION_NUM / 100)
+		{
+			data->allow_binary_basetypes = true;
+		}
+
+		/*
+		 * 9.4 lacks origins info so don't forward it.
+		 *
+		 * There's currently no knob for clients to use to suppress
+		 * this info and it's sent if it's supported and available.
+		 */
+		if (PG_VERSION_NUM/100 == 904)
+			data->forward_changeset_origins = false;
+		else
+			data->forward_changeset_origins = true;
+
+		if (data->hooks_setup_funcname != NIL)
+		{
+
+			data->hooks_mctxt = AllocSetContextCreate(ctx->context,
+					"pglogical_output hooks context",
+					ALLOCSET_SMALL_MINSIZE,
+					ALLOCSET_SMALL_INITSIZE,
+					ALLOCSET_SMALL_MAXSIZE);
+
+			load_hooks(data);
+			call_startup_hook(data, ctx->output_plugin_options);
+		}
+
+		if (data->client_relmeta_cache_size < -1)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("relmeta_cache_size must be -1, 0, or positive")));
+		}
+
+		/*
+		 * Relation metadata cache configuration.
+		 *
+		 * TODO: support fixed size cache
+		 *
+		 * Need a LRU for eviction, and need to implement a new message type for
+		 * cache purge notifications for clients. In the mean time force it to 0
+		 * (off). The client will be told via a startup param and must respect
+		 * that.
+		 */
+		if (data->client_relmeta_cache_size != 0
+				&& data->client_relmeta_cache_size != -1)
+		{
+			ereport(INFO,
+					(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+					 errmsg("fixed size cache not supported, forced to off"),
+					 errdetail("only relmeta_cache_size=0 (off) or relmeta_cache_size=-1 (unlimited) supported")));
+
+			data->relmeta_cache_size = 0;
+		}
+		else
+		{
+			/* ack client request */
+			data->relmeta_cache_size = data->client_relmeta_cache_size;
+		}
+
+		/* if cache enabled, init it */
+		if (data->relmeta_cache_size != 0)
+			pglogical_init_relmetacache();
+	}
+}
+
+/*
+ * BEGIN callback
+ */
+void
+pg_decode_begin_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn)
+{
+	PGLogicalOutputData* data = (PGLogicalOutputData*)ctx->output_plugin_private;
+	bool send_replication_origin = data->forward_changeset_origins;
+
+	if (!startup_message_sent)
+		send_startup_message(ctx, data, false /* can't be last message */);
+
+#ifdef HAVE_REPLICATION_ORIGINS
+	/* If the record didn't originate locally, send origin info */
+	send_replication_origin &= txn->origin_id != InvalidRepOriginId;
+#endif
+
+	OutputPluginPrepareWrite(ctx, !send_replication_origin);
+	data->api->write_begin(ctx->out, data, txn);
+
+#ifdef HAVE_REPLICATION_ORIGINS
+	if (send_replication_origin)
+	{
+		char *origin;
+
+		/* Message boundary */
+		OutputPluginWrite(ctx, false);
+		OutputPluginPrepareWrite(ctx, true);
+
+		/*
+		 * XXX: which behaviour we want here?
+		 *
+		 * Alternatives:
+		 *  - don't send origin message if origin name not found
+		 *    (that's what we do now)
+		 *  - throw error - that will break replication, not good
+		 *  - send some special "unknown" origin
+		 */
+		if (data->api->write_origin &&
+			replorigin_by_oid(txn->origin_id, true, &origin))
+			data->api->write_origin(ctx->out, origin, txn->origin_lsn);
+	}
+#endif
+
+	OutputPluginWrite(ctx, true);
+}
+
+/*
+ * COMMIT callback
+ */
+void
+pg_decode_commit_txn(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
+					 XLogRecPtr commit_lsn)
+{
+	PGLogicalOutputData* data = (PGLogicalOutputData*)ctx->output_plugin_private;
+
+	OutputPluginPrepareWrite(ctx, true);
+	data->api->write_commit(ctx->out, data, txn, commit_lsn);
+	OutputPluginWrite(ctx, true);
+}
+
+void
+pg_decode_change(LogicalDecodingContext *ctx, ReorderBufferTXN *txn,
+				 Relation relation, ReorderBufferChange *change)
+{
+	PGLogicalOutputData *data = ctx->output_plugin_private;
+	MemoryContext old;
+	struct PGLRelMetaCacheEntry *cached_relmeta = NULL;
+
+
+	/* First check the table filter */
+	if (!call_row_filter_hook(data, txn, relation, change))
+		return;
+
+	/* Avoid leaking memory by using and resetting our own context */
+	old = MemoryContextSwitchTo(data->context);
+
+	/*
+	 * If the protocol wants to write relation information and the client
+	 * isn't known to have metadata cached for this relation already,
+	 * send relation metadata.
+	 *
+	 * TODO: track hit/miss stats
+	 */
+	if (data->api->write_rel != NULL &&
+			!pglogical_cache_relmeta(data, relation, &cached_relmeta))
+	{
+		OutputPluginPrepareWrite(ctx, false);
+		data->api->write_rel(ctx->out, data, relation, cached_relmeta);
+		OutputPluginWrite(ctx, false);
+	}
+
+	/* Send the data */
+	switch (change->action)
+	{
+		case REORDER_BUFFER_CHANGE_INSERT:
+			OutputPluginPrepareWrite(ctx, true);
+			data->api->write_insert(ctx->out, data, relation,
+									&change->data.tp.newtuple->tuple);
+			OutputPluginWrite(ctx, true);
+			break;
+		case REORDER_BUFFER_CHANGE_UPDATE:
+			{
+				HeapTuple oldtuple = change->data.tp.oldtuple ?
+					&change->data.tp.oldtuple->tuple : NULL;
+
+				OutputPluginPrepareWrite(ctx, true);
+				data->api->write_update(ctx->out, data, relation, oldtuple,
+										&change->data.tp.newtuple->tuple);
+				OutputPluginWrite(ctx, true);
+				break;
+			}
+		case REORDER_BUFFER_CHANGE_DELETE:
+			if (change->data.tp.oldtuple)
+			{
+				OutputPluginPrepareWrite(ctx, true);
+				data->api->write_delete(ctx->out, data, relation,
+										&change->data.tp.oldtuple->tuple);
+				OutputPluginWrite(ctx, true);
+			}
+			else
+				elog(DEBUG1, "didn't send DELETE change because of missing oldtuple");
+			break;
+		default:
+			Assert(false);
+	}
+
+	/* Cleanup */
+	MemoryContextSwitchTo(old);
+	MemoryContextReset(data->context);
+}
+
+#ifdef HAVE_REPLICATION_ORIGINS
+/*
+ * Decide if the whole transaction with specific origin should be filtered out.
+ */
+static bool
+pg_decode_origin_filter(LogicalDecodingContext *ctx,
+						RepOriginId origin_id)
+{
+	PGLogicalOutputData *data = ctx->output_plugin_private;
+
+	if (!call_txn_filter_hook(data, origin_id))
+		return true;
+
+	return false;
+}
+#endif
+
+static void
+send_startup_message(LogicalDecodingContext *ctx,
+		PGLogicalOutputData *data, bool last_message)
+{
+	List *msg;
+
+	Assert(!startup_message_sent);
+
+	msg = prepare_startup_message(data);
+
+	/*
+	 * We could free the extra_startup_params DefElem list here, but it's
+	 * pretty harmless to just ignore it, since it's in the decoding memory
+	 * context anyway, and we don't know if it's safe to free the defnames or
+	 * not.
+	 */
+
+	OutputPluginPrepareWrite(ctx, last_message);
+	data->api->write_startup_message(ctx->out, msg);
+	OutputPluginWrite(ctx, last_message);
+
+	pfree(msg);
+
+	startup_message_sent = true;
+}
+
+static void pg_decode_shutdown(LogicalDecodingContext * ctx)
+{
+	PGLogicalOutputData* data = (PGLogicalOutputData*)ctx->output_plugin_private;
+
+	call_shutdown_hook(data);
+
+	if (data->hooks_mctxt != NULL)
+	{
+		MemoryContextDelete(data->hooks_mctxt);
+		data->hooks_mctxt = NULL;
+	}
+}
diff --git a/contrib/pglogical_output/pglogical_output.control.in b/contrib/pglogical_output/pglogical_output.control.in
new file mode 100644
index 0000000..1b86595
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_output.control.in
@@ -0,0 +1,4 @@
+default_version = '__PGLOGICAL_OUTPUT_VERSION__'
+comment = 'general purpose logical decoding plugin'
+module_pathname = 'pglogical_output'
+superuser = false
diff --git a/contrib/pglogical_output/pglogical_output.h b/contrib/pglogical_output/pglogical_output.h
new file mode 100644
index 0000000..a6bf281
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_output.h
@@ -0,0 +1,111 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_output.h
+ *		pglogical output plugin
+ *
+ * Copyright (c) 2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		pglogical_output.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_LOGICAL_OUTPUT_H
+#define PG_LOGICAL_OUTPUT_H
+
+#include "nodes/parsenodes.h"
+
+#include "replication/logical.h"
+#include "replication/output_plugin.h"
+
+#include "storage/lock.h"
+
+#include "pglogical_output/hooks.h"
+
+#include "pglogical_proto.h"
+
+/* XXYYZZ format version number and human readable version */
+#define PGLOGICAL_OUTPUT_VERSION_NUM 10000
+#define PGLOGICAL_OUTPUT_VERSION "1.0.0"
+
+/* Protocol capabilities */
+#define PGLOGICAL_PROTO_VERSION_NUM 1
+#define PGLOGICAL_PROTO_MIN_VERSION_NUM 1
+
+/*
+ * The name of a hook function. This is used instead of the usual List*
+ * because can serve as a hash key.
+ *
+ * Must be zeroed on allocation if used as a hash key since padding is
+ * *not* ignored on compare.
+ */
+typedef struct HookFuncName
+{
+	/* funcname is more likely to be unique, so goes first */
+	char    function[NAMEDATALEN];
+	char    schema[NAMEDATALEN];
+} HookFuncName;
+
+struct PGLogicalProtoAPI;
+
+typedef struct PGLogicalOutputData
+{
+	MemoryContext context;
+
+	struct PGLogicalProtoAPI *api;
+
+	/* protocol */
+	bool	allow_internal_basetypes;
+	bool	allow_binary_basetypes;
+	bool	forward_changeset_origins;
+	int		field_datum_encoding;
+	int		relmeta_cache_size;
+
+	/*
+	 * client info
+	 *
+	 * Lots of this should move to a separate shorter-lived struct used only
+	 * during parameter reading, since it contains what the client asked for.
+	 * Once we've processed this during startup we don't refer to it again.
+	 */
+	uint32	client_pg_version;
+	uint32	client_max_proto_version;
+	uint32	client_min_proto_version;
+	const char *client_expected_encoding;
+	const char *client_protocol_format;
+	uint32  client_binary_basetypes_major_version;
+	bool	client_want_internal_basetypes_set;
+	bool	client_want_internal_basetypes;
+	bool	client_want_binary_basetypes_set;
+	bool	client_want_binary_basetypes;
+	bool	client_binary_bigendian_set;
+	bool	client_binary_bigendian;
+	uint32	client_binary_sizeofdatum;
+	uint32	client_binary_sizeofint;
+	uint32	client_binary_sizeoflong;
+	bool	client_binary_float4byval_set;
+	bool	client_binary_float4byval;
+	bool	client_binary_float8byval_set;
+	bool	client_binary_float8byval;
+	bool	client_binary_intdatetimes_set;
+	bool	client_binary_intdatetimes;
+	bool	client_no_txinfo;
+	int   client_relmeta_cache_size;
+
+	/* hooks */
+	List *hooks_setup_funcname;
+	struct PGLogicalHooks hooks;
+	MemoryContext hooks_mctxt;
+
+	/* DefElem<String> list populated by startup hook */
+	List *extra_startup_params;
+} PGLogicalOutputData;
+
+typedef struct PGLogicalTupleData
+{
+	Datum	values[MaxTupleAttributeNumber];
+	bool	nulls[MaxTupleAttributeNumber];
+	bool	changed[MaxTupleAttributeNumber];
+} PGLogicalTupleData;
+
+#endif /* PG_LOGICAL_OUTPUT_H */
diff --git a/contrib/pglogical_output/pglogical_output/README b/contrib/pglogical_output/pglogical_output/README
new file mode 100644
index 0000000..5480e5c
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_output/README
@@ -0,0 +1,7 @@
+/*
+ * This directory contains the public header files for the pglogical_output
+ * extension. It is installed into the PostgreSQL source tree when the extension
+ * is installed.
+ *
+ * These headers are not part of the PostgreSQL project its self.
+ */
diff --git a/contrib/pglogical_output/pglogical_output/compat.h b/contrib/pglogical_output/pglogical_output/compat.h
new file mode 100644
index 0000000..b0b14fc
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_output/compat.h
@@ -0,0 +1,28 @@
+#ifndef PG_LOGICAL_COMPAT_H
+#define PG_LOGICAL_COMPAT_H
+
+#include "pg_config.h"
+
+/* 9.4 lacks replication origins */
+#if PG_VERSION_NUM >= 90500
+#define HAVE_REPLICATION_ORIGINS
+#else
+/* To allow the same signature on hooks in 9.4 */
+typedef uint16 RepOriginId;
+#define InvalidRepOriginId 0
+#endif
+
+/* 9.4 lacks PG_UINT32_MAX */
+#ifndef PG_UINT32_MAX
+#define PG_UINT32_MAX UINT32_MAX
+#endif
+
+#ifndef PG_INT32_MAX
+#define PG_INT32_MAX INT32_MAX
+#endif
+
+#ifndef PG_INT32_MIN
+#define PG_INT32_MIN INT32_MIN
+#endif
+
+#endif
diff --git a/contrib/pglogical_output/pglogical_output/hooks.h b/contrib/pglogical_output/pglogical_output/hooks.h
new file mode 100644
index 0000000..8766dd7
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_output/hooks.h
@@ -0,0 +1,73 @@
+#ifndef PGLOGICAL_OUTPUT_HOOKS_H
+#define PGLOGICAL_OUTPUT_HOOKS_H
+
+#include "access/xlogdefs.h"
+#include "nodes/pg_list.h"
+#include "utils/rel.h"
+#include "utils/palloc.h"
+#include "replication/reorderbuffer.h"
+
+#include "pglogical_output/compat.h"
+
+/*
+ * This header is to be included by extensions that implement pglogical output
+ * plugin callback hooks for transaction origin and row filtering, etc. It is
+ * installed as "pglogical_output/hooks.h"
+ *
+ * See the README.md and the example in examples/hooks/ for details on hooks.
+ */
+
+
+struct PGLogicalStartupHookArgs
+{
+	void	   *private_data;
+	List	   *in_params;
+	List	   *out_params;
+};
+
+typedef void (*pglogical_startup_hook_fn)(struct PGLogicalStartupHookArgs *args);
+
+
+struct PGLogicalTxnFilterArgs
+{
+	void 	   *private_data;
+	RepOriginId	origin_id;
+};
+
+typedef bool (*pglogical_txn_filter_hook_fn)(struct PGLogicalTxnFilterArgs *args);
+
+
+struct PGLogicalRowFilterArgs
+{
+	void 	   *private_data;
+	Relation	changed_rel;
+	enum ReorderBufferChangeType	change_type;
+	/* detailed row change event from logical decoding */
+	ReorderBufferChange* change;
+};
+
+typedef bool (*pglogical_row_filter_hook_fn)(struct PGLogicalRowFilterArgs *args);
+
+
+struct PGLogicalShutdownHookArgs
+{
+	void	   *private_data;
+};
+
+typedef void (*pglogical_shutdown_hook_fn)(struct PGLogicalShutdownHookArgs *args);
+
+/*
+ * This struct is passed to the pglogical_get_hooks_fn as the first argument,
+ * typed 'internal', and is unwrapped with `DatumGetPointer`.
+ */
+struct PGLogicalHooks
+{
+	pglogical_startup_hook_fn startup_hook;
+	pglogical_shutdown_hook_fn shutdown_hook;
+	pglogical_txn_filter_hook_fn txn_filter_hook;
+	pglogical_row_filter_hook_fn row_filter_hook;
+	void *hooks_private_data;
+};
+
+
+#endif /* PGLOGICAL_OUTPUT_HOOKS_H */
diff --git a/contrib/pglogical_output/pglogical_proto.c b/contrib/pglogical_output/pglogical_proto.c
new file mode 100644
index 0000000..47a883f
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_proto.c
@@ -0,0 +1,49 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_proto.c
+ * 		pglogical protocol functions
+ *
+ * Copyright (c) 2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  pglogical_proto.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "pglogical_output.h"
+#include "pglogical_proto.h"
+#include "pglogical_proto_native.h"
+#include "pglogical_proto_json.h"
+
+PGLogicalProtoAPI *
+pglogical_init_api(PGLogicalProtoType typ)
+{
+	PGLogicalProtoAPI  *res = palloc0(sizeof(PGLogicalProtoAPI));
+
+	if (typ == PGLogicalProtoJson)
+	{
+		res->write_rel = NULL;
+		res->write_begin = pglogical_json_write_begin;
+		res->write_commit = pglogical_json_write_commit;
+		res->write_origin = NULL;
+		res->write_insert = pglogical_json_write_insert;
+		res->write_update = pglogical_json_write_update;
+		res->write_delete = pglogical_json_write_delete;
+		res->write_startup_message = json_write_startup_message;
+	}
+	else
+	{
+		res->write_rel = pglogical_write_rel;
+		res->write_begin = pglogical_write_begin;
+		res->write_commit = pglogical_write_commit;
+		res->write_origin = pglogical_write_origin;
+		res->write_insert = pglogical_write_insert;
+		res->write_update = pglogical_write_update;
+		res->write_delete = pglogical_write_delete;
+		res->write_startup_message = write_startup_message;
+	}
+
+	return res;
+}
diff --git a/contrib/pglogical_output/pglogical_proto.h b/contrib/pglogical_output/pglogical_proto.h
new file mode 100644
index 0000000..27897e2
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_proto.h
@@ -0,0 +1,61 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_proto.h
+ *		pglogical protocol
+ *
+ * Copyright (c) 2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  pglogical_proto.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_LOGICAL_PROTO_H
+#define PG_LOGICAL_PROTO_H
+
+struct PGLogicalOutputData;
+struct PGLRelMetaCacheEntry;
+
+typedef void (*pglogical_write_rel_fn)(StringInfo out, struct PGLogicalOutputData *data,
+							 Relation rel, struct PGLRelMetaCacheEntry *cache_entry);
+
+typedef void (*pglogical_write_begin_fn)(StringInfo out, struct PGLogicalOutputData *data,
+							 ReorderBufferTXN *txn);
+typedef void (*pglogical_write_commit_fn)(StringInfo out, struct PGLogicalOutputData *data,
+							 ReorderBufferTXN *txn, XLogRecPtr commit_lsn);
+
+typedef void (*pglogical_write_origin_fn)(StringInfo out, const char *origin,
+							 XLogRecPtr origin_lsn);
+
+typedef void (*pglogical_write_insert_fn)(StringInfo out, struct PGLogicalOutputData *data,
+							 Relation rel, HeapTuple newtuple);
+typedef void (*pglogical_write_update_fn)(StringInfo out, struct PGLogicalOutputData *data,
+							 Relation rel, HeapTuple oldtuple,
+							 HeapTuple newtuple);
+typedef void (*pglogical_write_delete_fn)(StringInfo out, struct PGLogicalOutputData *data,
+							 Relation rel, HeapTuple oldtuple);
+
+typedef void (*write_startup_message_fn)(StringInfo out, List *msg);
+
+typedef struct PGLogicalProtoAPI
+{
+	pglogical_write_rel_fn		write_rel;
+	pglogical_write_begin_fn	write_begin;
+	pglogical_write_commit_fn	write_commit;
+	pglogical_write_origin_fn	write_origin;
+	pglogical_write_insert_fn	write_insert;
+	pglogical_write_update_fn	write_update;
+	pglogical_write_delete_fn	write_delete;
+	write_startup_message_fn	write_startup_message;
+} PGLogicalProtoAPI;
+
+
+typedef enum PGLogicalProtoType
+{
+	PGLogicalProtoNative,
+	PGLogicalProtoJson
+} PGLogicalProtoType;
+
+extern PGLogicalProtoAPI *pglogical_init_api(PGLogicalProtoType typ);
+
+#endif /* PG_LOGICAL_PROTO_H */
diff --git a/contrib/pglogical_output/pglogical_proto_json.c b/contrib/pglogical_output/pglogical_proto_json.c
new file mode 100644
index 0000000..ae5a591
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_proto_json.c
@@ -0,0 +1,204 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_proto_json.c
+ * 		pglogical protocol functions for json support
+ *
+ * Copyright (c) 2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  pglogical_proto_json.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "miscadmin.h"
+
+#include "pglogical_output.h"
+#include "pglogical_proto_json.h"
+
+#include "access/sysattr.h"
+#include "access/tuptoaster.h"
+#include "access/xact.h"
+
+#include "catalog/catversion.h"
+#include "catalog/index.h"
+
+#include "catalog/namespace.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_namespace.h"
+#include "catalog/pg_type.h"
+
+#include "commands/dbcommands.h"
+
+#include "executor/spi.h"
+
+#include "libpq/pqformat.h"
+
+#include "mb/pg_wchar.h"
+
+#ifdef HAVE_REPLICATION_ORIGINS
+#include "replication/origin.h"
+#endif
+
+#include "utils/builtins.h"
+#include "utils/json.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/typcache.h"
+
+
+/*
+ * Write BEGIN to the output stream.
+ */
+void
+pglogical_json_write_begin(StringInfo out, PGLogicalOutputData *data, ReorderBufferTXN *txn)
+{
+	appendStringInfoChar(out, '{');
+	appendStringInfoString(out, "\"action\":\"B\"");
+	appendStringInfo(out, ", \"has_catalog_changes\":\"%c\"",
+		txn->has_catalog_changes ? 't' : 'f');
+#ifdef HAVE_REPLICATION_ORIGINS
+	if (txn->origin_id != InvalidRepOriginId)
+		appendStringInfo(out, ", \"origin_id\":\"%u\"", txn->origin_id);
+#endif
+	if (!data->client_no_txinfo)
+	{
+		appendStringInfo(out, ", \"xid\":\"%u\"", txn->xid);
+		appendStringInfo(out, ", \"first_lsn\":\"%X/%X\"",
+			(uint32)(txn->first_lsn >> 32), (uint32)(txn->first_lsn));
+#ifdef HAVE_REPLICATION_ORIGINS
+		appendStringInfo(out, ", \"origin_lsn\":\"%X/%X\"",
+			(uint32)(txn->origin_lsn >> 32), (uint32)(txn->origin_lsn));
+#endif
+		if (txn->commit_time != 0)
+		appendStringInfo(out, ", \"commit_time\":\"%s\"",
+			timestamptz_to_str(txn->commit_time));
+	}
+	appendStringInfoChar(out, '}');
+}
+
+/*
+ * Write COMMIT to the output stream.
+ */
+void
+pglogical_json_write_commit(StringInfo out, PGLogicalOutputData *data, ReorderBufferTXN *txn,
+						XLogRecPtr commit_lsn)
+{
+	appendStringInfoChar(out, '{');
+	appendStringInfoString(out, "\"action\":\"C\"");
+	if (!data->client_no_txinfo)
+	{
+		appendStringInfo(out, ", \"final_lsn\":\"%X/%X\"",
+			(uint32)(txn->final_lsn >> 32), (uint32)(txn->final_lsn));
+		appendStringInfo(out, ", \"end_lsn\":\"%X/%X\"",
+			(uint32)(txn->end_lsn >> 32), (uint32)(txn->end_lsn));
+	}
+	appendStringInfoChar(out, '}');
+}
+
+/*
+ * Write a tuple to the outputstream, in the most efficient format possible.
+ */
+static void
+json_write_tuple(StringInfo out, Relation rel, HeapTuple tuple)
+{
+	TupleDesc	desc;
+	Datum		tupdatum,
+				json;
+
+	desc = RelationGetDescr(rel);
+	tupdatum = heap_copy_tuple_as_datum(tuple, desc);
+	json = DirectFunctionCall1(row_to_json, tupdatum);
+
+	appendStringInfoString(out, TextDatumGetCString(json));
+}
+
+/*
+ * Write change.
+ *
+ * Generic function handling DML changes.
+ */
+static void
+pglogical_json_write_change(StringInfo out, const char *change, Relation rel,
+							HeapTuple oldtuple, HeapTuple newtuple)
+{
+	appendStringInfoChar(out, '{');
+	appendStringInfo(out, "\"action\":\"%s\",\"relation\":[\"%s\",\"%s\"]",
+					 change,
+					 get_namespace_name(RelationGetNamespace(rel)),
+					 RelationGetRelationName(rel));
+
+	if (oldtuple)
+	{
+		appendStringInfoString(out, ",\"oldtuple\":");
+		json_write_tuple(out, rel, oldtuple);
+	}
+	if (newtuple)
+	{
+		appendStringInfoString(out, ",\"newtuple\":");
+		json_write_tuple(out, rel, newtuple);
+	}
+	appendStringInfoChar(out, '}');
+}
+
+/*
+ * Write INSERT to the output stream.
+ */
+void
+pglogical_json_write_insert(StringInfo out, PGLogicalOutputData *data,
+							Relation rel, HeapTuple newtuple)
+{
+	pglogical_json_write_change(out, "I", rel, NULL, newtuple);
+}
+
+/*
+ * Write UPDATE to the output stream.
+ */
+void
+pglogical_json_write_update(StringInfo out, PGLogicalOutputData *data,
+							Relation rel, HeapTuple oldtuple,
+							HeapTuple newtuple)
+{
+	pglogical_json_write_change(out, "U", rel, oldtuple, newtuple);
+}
+
+/*
+ * Write DELETE to the output stream.
+ */
+void
+pglogical_json_write_delete(StringInfo out, PGLogicalOutputData *data,
+							Relation rel, HeapTuple oldtuple)
+{
+	pglogical_json_write_change(out, "D", rel, oldtuple, NULL);
+}
+
+/*
+ * The startup message should be constructed as a json object, one
+ * key/value per DefElem list member.
+ */
+void
+json_write_startup_message(StringInfo out, List *msg)
+{
+	ListCell *lc;
+	bool first = true;
+
+	appendStringInfoString(out, "{\"action\":\"S\", \"params\": {");
+	foreach (lc, msg)
+	{
+		DefElem *param = (DefElem*)lfirst(lc);
+		Assert(IsA(param->arg, String) && strVal(param->arg) != NULL);
+		if (first)
+			first = false;
+		else
+			appendStringInfoChar(out, ',');
+		escape_json(out, param->defname);
+		appendStringInfoChar(out, ':');
+		escape_json(out, strVal(param->arg));
+	}
+	appendStringInfoString(out, "}}");
+}
diff --git a/contrib/pglogical_output/pglogical_proto_json.h b/contrib/pglogical_output/pglogical_proto_json.h
new file mode 100644
index 0000000..d853e9e
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_proto_json.h
@@ -0,0 +1,32 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_proto_json.h
+ *		pglogical protocol, json implementation
+ *
+ * Copyright (c) 2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  pglogical_proto_json.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_LOGICAL_PROTO_JSON_H
+#define PG_LOGICAL_PROTO_JSON_H
+
+
+extern void pglogical_json_write_begin(StringInfo out, PGLogicalOutputData *data,
+								 ReorderBufferTXN *txn);
+extern void pglogical_json_write_commit(StringInfo out, PGLogicalOutputData *data,
+								 ReorderBufferTXN *txn, XLogRecPtr commit_lsn);
+
+extern void pglogical_json_write_insert(StringInfo out, PGLogicalOutputData *data,
+								 Relation rel, HeapTuple newtuple);
+extern void pglogical_json_write_update(StringInfo out, PGLogicalOutputData *data,
+								 Relation rel, HeapTuple oldtuple,
+								 HeapTuple newtuple);
+extern void pglogical_json_write_delete(StringInfo out, PGLogicalOutputData *data,
+								 Relation rel, HeapTuple oldtuple);
+
+extern void json_write_startup_message(StringInfo out, List *msg);
+
+#endif /* PG_LOGICAL_PROTO_JSON_H */
diff --git a/contrib/pglogical_output/pglogical_proto_native.c b/contrib/pglogical_output/pglogical_proto_native.c
new file mode 100644
index 0000000..2dfad8b
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_proto_native.c
@@ -0,0 +1,513 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_proto_native.c
+ * 		pglogical binary protocol functions
+ *
+ * Copyright (c) 2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  pglogical_proto_native.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "miscadmin.h"
+
+#include "pglogical_output.h"
+#include "pglogical_relmetacache.h"
+#include "pglogical_proto_native.h"
+
+#include "access/sysattr.h"
+#include "access/tuptoaster.h"
+#include "access/xact.h"
+
+#include "catalog/catversion.h"
+#include "catalog/index.h"
+
+#include "catalog/namespace.h"
+#include "catalog/pg_class.h"
+#include "catalog/pg_database.h"
+#include "catalog/pg_namespace.h"
+#include "catalog/pg_type.h"
+
+#include "commands/dbcommands.h"
+
+#include "executor/spi.h"
+
+#include "libpq/pqformat.h"
+
+#include "mb/pg_wchar.h"
+
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+#include "utils/syscache.h"
+#include "utils/timestamp.h"
+#include "utils/typcache.h"
+
+#define IS_REPLICA_IDENTITY 1
+
+static void pglogical_write_attrs(StringInfo out, Relation rel);
+static void pglogical_write_tuple(StringInfo out, PGLogicalOutputData *data,
+								   Relation rel, HeapTuple tuple);
+static char decide_datum_transfer(Form_pg_attribute att,
+								  Form_pg_type typclass,
+								  bool allow_internal_basetypes,
+								  bool allow_binary_basetypes);
+
+/*
+ * Write relation description to the output stream.
+ */
+void
+pglogical_write_rel(StringInfo out, PGLogicalOutputData *data, Relation rel,
+		struct PGLRelMetaCacheEntry *cache_entry)
+{
+	const char *nspname;
+	uint8		nspnamelen;
+	const char *relname;
+	uint8		relnamelen;
+	uint8		flags = 0;
+
+	/* must not have cache entry if metacache off; must have entry if on */
+	Assert( (data->relmeta_cache_size == 0) == (cache_entry == NULL) );
+	/* if cache enabled must never be called with an already-cached rel */
+	Assert(cache_entry == NULL || !cache_entry->is_cached);
+
+	pq_sendbyte(out, 'R');		/* sending RELATION */
+
+	/* send the flags field */
+	pq_sendbyte(out, flags);
+
+	/* use Oid as relation identifier */
+	pq_sendint(out, RelationGetRelid(rel), 4);
+
+	nspname = get_namespace_name(rel->rd_rel->relnamespace);
+	if (nspname == NULL)
+		elog(ERROR, "cache lookup failed for namespace %u",
+			 rel->rd_rel->relnamespace);
+	nspnamelen = strlen(nspname) + 1;
+
+	relname = NameStr(rel->rd_rel->relname);
+	relnamelen = strlen(relname) + 1;
+
+	pq_sendbyte(out, nspnamelen);		/* schema name length */
+	pq_sendbytes(out, nspname, nspnamelen);
+
+	pq_sendbyte(out, relnamelen);		/* table name length */
+	pq_sendbytes(out, relname, relnamelen);
+
+	/* send the attribute info */
+	pglogical_write_attrs(out, rel);
+
+	/*
+	 * Since we've sent the whole relation metadata not just the columns for
+	 * the coming row(s), we can omit sending it again. The client will cache
+	 * it. If the relation changes the cached flag is cleared by
+	 * pglogical_output and we'll be called again next time it's touched.
+	 *
+	 * We don't care about the cache size here, the size management is done
+	 * in the generic cache code.
+	 */
+	if (cache_entry != NULL)
+		cache_entry->is_cached = true;
+}
+
+/*
+ * Write relation attributes to the outputstream.
+ */
+static void
+pglogical_write_attrs(StringInfo out, Relation rel)
+{
+	TupleDesc	desc;
+	int			i;
+	uint16		nliveatts = 0;
+	Bitmapset  *idattrs;
+
+	desc = RelationGetDescr(rel);
+
+	pq_sendbyte(out, 'A');			/* sending ATTRS */
+
+	/* send number of live attributes */
+	for (i = 0; i < desc->natts; i++)
+	{
+		if (desc->attrs[i]->attisdropped)
+			continue;
+		nliveatts++;
+	}
+	pq_sendint(out, nliveatts, 2);
+
+	/* fetch bitmap of REPLICATION IDENTITY attributes */
+	idattrs = RelationGetIndexAttrBitmap(rel, INDEX_ATTR_BITMAP_IDENTITY_KEY);
+
+	/* send the attributes */
+	for (i = 0; i < desc->natts; i++)
+	{
+		Form_pg_attribute att = desc->attrs[i];
+		uint8			flags = 0;
+		uint16			len;
+		const char	   *attname;
+
+		if (att->attisdropped)
+			continue;
+
+		if (bms_is_member(att->attnum - FirstLowInvalidHeapAttributeNumber,
+						  idattrs))
+			flags |= IS_REPLICA_IDENTITY;
+
+		pq_sendbyte(out, 'C');		/* column definition follows */
+		pq_sendbyte(out, flags);
+
+		pq_sendbyte(out, 'N');		/* column name block follows */
+		attname = NameStr(att->attname);
+		len = strlen(attname) + 1;
+		pq_sendint(out, len, 2);
+		pq_sendbytes(out, attname, len); /* data */
+	}
+}
+
+/*
+ * Write BEGIN to the output stream.
+ */
+void
+pglogical_write_begin(StringInfo out, PGLogicalOutputData *data,
+					  ReorderBufferTXN *txn)
+{
+	uint8	flags = 0;
+
+	pq_sendbyte(out, 'B');		/* BEGIN */
+
+	/* send the flags field its self */
+	pq_sendbyte(out, flags);
+
+	/* fixed fields */
+	pq_sendint64(out, txn->final_lsn);
+	pq_sendint64(out, txn->commit_time);
+	pq_sendint(out, txn->xid, 4);
+}
+
+/*
+ * Write COMMIT to the output stream.
+ */
+void
+pglogical_write_commit(StringInfo out, PGLogicalOutputData *data,
+					   ReorderBufferTXN *txn, XLogRecPtr commit_lsn)
+{
+	uint8 flags = 0;
+
+	pq_sendbyte(out, 'C');		/* sending COMMIT */
+
+	/* send the flags field */
+	pq_sendbyte(out, flags);
+
+	/* send fixed fields */
+	pq_sendint64(out, commit_lsn);
+	pq_sendint64(out, txn->end_lsn);
+	pq_sendint64(out, txn->commit_time);
+}
+
+/*
+ * Write ORIGIN to the output stream.
+ */
+void
+pglogical_write_origin(StringInfo out, const char *origin,
+						XLogRecPtr origin_lsn)
+{
+	uint8	flags = 0;
+	uint8	len;
+
+	Assert(strlen(origin) < 255);
+
+	pq_sendbyte(out, 'O');		/* ORIGIN */
+
+	/* send the flags field its self */
+	pq_sendbyte(out, flags);
+
+	/* fixed fields */
+	pq_sendint64(out, origin_lsn);
+
+	/* origin */
+	len = strlen(origin) + 1;
+	pq_sendbyte(out, len);
+	pq_sendbytes(out, origin, len);
+}
+
+/*
+ * Write INSERT to the output stream.
+ */
+void
+pglogical_write_insert(StringInfo out, PGLogicalOutputData *data,
+						Relation rel, HeapTuple newtuple)
+{
+	uint8 flags = 0;
+
+	pq_sendbyte(out, 'I');		/* action INSERT */
+
+	/* send the flags field */
+	pq_sendbyte(out, flags);
+
+	/* use Oid as relation identifier */
+	pq_sendint(out, RelationGetRelid(rel), 4);
+
+	pq_sendbyte(out, 'N');		/* new tuple follows */
+	pglogical_write_tuple(out, data, rel, newtuple);
+}
+
+/*
+ * Write UPDATE to the output stream.
+ */
+void
+pglogical_write_update(StringInfo out, PGLogicalOutputData *data,
+						Relation rel, HeapTuple oldtuple, HeapTuple newtuple)
+{
+	uint8 flags = 0;
+
+	pq_sendbyte(out, 'U');		/* action UPDATE */
+
+	/* send the flags field */
+	pq_sendbyte(out, flags);
+
+	/* use Oid as relation identifier */
+	pq_sendint(out, RelationGetRelid(rel), 4);
+
+	/* FIXME support whole tuple (O tuple type) */
+	if (oldtuple != NULL)
+	{
+		pq_sendbyte(out, 'K');	/* old key follows */
+		pglogical_write_tuple(out, data, rel, oldtuple);
+	}
+
+	pq_sendbyte(out, 'N');		/* new tuple follows */
+	pglogical_write_tuple(out, data, rel, newtuple);
+}
+
+/*
+ * Write DELETE to the output stream.
+ */
+void
+pglogical_write_delete(StringInfo out, PGLogicalOutputData *data,
+						Relation rel, HeapTuple oldtuple)
+{
+	uint8 flags = 0;
+
+	pq_sendbyte(out, 'D');		/* action DELETE */
+
+	/* send the flags field */
+	pq_sendbyte(out, flags);
+
+	/* use Oid as relation identifier */
+	pq_sendint(out, RelationGetRelid(rel), 4);
+
+	/* FIXME support whole tuple (O tuple type) */
+	pq_sendbyte(out, 'K');	/* old key follows */
+	pglogical_write_tuple(out, data, rel, oldtuple);
+}
+
+/*
+ * Most of the brains for startup message creation lives in
+ * pglogical_config.c, so this presently just sends the set of key/value pairs.
+ */
+void
+write_startup_message(StringInfo out, List *msg)
+{
+	ListCell *lc;
+
+	pq_sendbyte(out, 'S');	/* message type field */
+	pq_sendbyte(out, 1); 	/* startup message version */
+	foreach (lc, msg)
+	{
+		DefElem *param = (DefElem*)lfirst(lc);
+		Assert(IsA(param->arg, String) && strVal(param->arg) != NULL);
+		/* null-terminated key and value pairs, in client_encoding */
+		pq_sendstring(out, param->defname);
+		pq_sendstring(out, strVal(param->arg));
+	}
+}
+
+/*
+ * Write a tuple to the outputstream, in the most efficient format possible.
+ */
+static void
+pglogical_write_tuple(StringInfo out, PGLogicalOutputData *data,
+					   Relation rel, HeapTuple tuple)
+{
+	TupleDesc	desc;
+	Datum		values[MaxTupleAttributeNumber];
+	bool		isnull[MaxTupleAttributeNumber];
+	int			i;
+	uint16		nliveatts = 0;
+
+	desc = RelationGetDescr(rel);
+
+	pq_sendbyte(out, 'T');			/* sending TUPLE */
+
+	for (i = 0; i < desc->natts; i++)
+	{
+		if (desc->attrs[i]->attisdropped)
+			continue;
+		nliveatts++;
+	}
+	pq_sendint(out, nliveatts, 2);
+
+	/* try to allocate enough memory from the get go */
+	enlargeStringInfo(out, tuple->t_len +
+					  nliveatts * (1 + 4));
+
+	/*
+	 * XXX: should this prove to be a relevant bottleneck, it might be
+	 * interesting to inline heap_deform_tuple() here, we don't actually need
+	 * the information in the form we get from it.
+	 */
+	heap_deform_tuple(tuple, desc, values, isnull);
+
+	for (i = 0; i < desc->natts; i++)
+	{
+		HeapTuple	typtup;
+		Form_pg_type typclass;
+		Form_pg_attribute att = desc->attrs[i];
+		char		transfer_type;
+
+		/* skip dropped columns */
+		if (att->attisdropped)
+			continue;
+
+		if (isnull[i])
+		{
+			pq_sendbyte(out, 'n');	/* null column */
+			continue;
+		}
+		else if (att->attlen == -1 && VARATT_IS_EXTERNAL_ONDISK(values[i]))
+		{
+			pq_sendbyte(out, 'u');	/* unchanged toast column */
+			continue;
+		}
+
+		typtup = SearchSysCache1(TYPEOID, ObjectIdGetDatum(att->atttypid));
+		if (!HeapTupleIsValid(typtup))
+			elog(ERROR, "cache lookup failed for type %u", att->atttypid);
+		typclass = (Form_pg_type) GETSTRUCT(typtup);
+
+		transfer_type = decide_datum_transfer(att, typclass,
+											  data->allow_internal_basetypes,
+											  data->allow_binary_basetypes);
+
+		switch (transfer_type)
+		{
+			case 'i':
+				pq_sendbyte(out, 'i');	/* internal-format binary data follows */
+
+				/* pass by value */
+				if (att->attbyval)
+				{
+					pq_sendint(out, att->attlen, 4); /* length */
+
+					enlargeStringInfo(out, att->attlen);
+					store_att_byval(out->data + out->len, values[i],
+									att->attlen);
+					out->len += att->attlen;
+					out->data[out->len] = '\0';
+				}
+				/* fixed length non-varlena pass-by-reference type */
+				else if (att->attlen > 0)
+				{
+					pq_sendint(out, att->attlen, 4); /* length */
+
+					appendBinaryStringInfo(out, DatumGetPointer(values[i]),
+										   att->attlen);
+				}
+				/* varlena type */
+				else if (att->attlen == -1)
+				{
+					char *data = DatumGetPointer(values[i]);
+
+					/* send indirect datums inline */
+					if (VARATT_IS_EXTERNAL_INDIRECT(values[i]))
+					{
+						struct varatt_indirect redirect;
+						VARATT_EXTERNAL_GET_POINTER(redirect, data);
+						data = (char *) redirect.pointer;
+					}
+
+					Assert(!VARATT_IS_EXTERNAL(data));
+
+					pq_sendint(out, VARSIZE_ANY(data), 4); /* length */
+
+					appendBinaryStringInfo(out, data, VARSIZE_ANY(data));
+				}
+				else
+					elog(ERROR, "unsupported tuple type");
+
+				break;
+
+			case 'b':
+				{
+					bytea	   *outputbytes;
+					int			len;
+
+					pq_sendbyte(out, 'b');	/* binary send/recv data follows */
+
+					outputbytes = OidSendFunctionCall(typclass->typsend,
+													  values[i]);
+
+					len = VARSIZE(outputbytes) - VARHDRSZ;
+					pq_sendint(out, len, 4); /* length */
+					pq_sendbytes(out, VARDATA(outputbytes), len); /* data */
+					pfree(outputbytes);
+				}
+				break;
+
+			default:
+				{
+					char   	   *outputstr;
+					int			len;
+
+					pq_sendbyte(out, 't');	/* 'text' data follows */
+
+					outputstr =	OidOutputFunctionCall(typclass->typoutput,
+													  values[i]);
+					len = strlen(outputstr) + 1;
+					pq_sendint(out, len, 4); /* length */
+					appendBinaryStringInfo(out, outputstr, len); /* data */
+					pfree(outputstr);
+				}
+		}
+
+		ReleaseSysCache(typtup);
+	}
+}
+
+/*
+ * Make the executive decision about which protocol to use.
+ */
+static char
+decide_datum_transfer(Form_pg_attribute att, Form_pg_type typclass,
+					  bool allow_internal_basetypes,
+					  bool allow_binary_basetypes)
+{
+	/*
+	 * Use the binary protocol, if allowed, for builtin & plain datatypes.
+	 */
+	if (allow_internal_basetypes &&
+		typclass->typtype == 'b' &&
+		att->atttypid < FirstNormalObjectId &&
+		typclass->typelem == InvalidOid)
+	{
+		return 'i';
+	}
+	/*
+	 * Use send/recv, if allowed, if the type is plain or builtin.
+	 *
+	 * XXX: we can't use send/recv for array or composite types for now due to
+	 * the embedded oids.
+	 */
+	else if (allow_binary_basetypes &&
+			 OidIsValid(typclass->typreceive) &&
+			 (att->atttypid < FirstNormalObjectId || typclass->typtype != 'c') &&
+			 (att->atttypid < FirstNormalObjectId || typclass->typelem == InvalidOid))
+	{
+		return 'b';
+	}
+
+	return 't';
+}
diff --git a/contrib/pglogical_output/pglogical_proto_native.h b/contrib/pglogical_output/pglogical_proto_native.h
new file mode 100644
index 0000000..b8433e9
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_proto_native.h
@@ -0,0 +1,38 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_proto_native.h
+ *		pglogical protocol, native implementation
+ *
+ * Copyright (c) 2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  pglogical_proto_native.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef PG_LOGICAL_PROTO_NATIVE_H
+#define PG_LOGICAL_PROTO_NATIVE_H
+
+
+extern void pglogical_write_rel(StringInfo out, PGLogicalOutputData *data, Relation rel,
+							struct PGLRelMetaCacheEntry *cache_entry);
+
+extern void pglogical_write_begin(StringInfo out, PGLogicalOutputData *data,
+							ReorderBufferTXN *txn);
+extern void pglogical_write_commit(StringInfo out,PGLogicalOutputData *data,
+							ReorderBufferTXN *txn, XLogRecPtr commit_lsn);
+
+extern void pglogical_write_origin(StringInfo out, const char *origin,
+							XLogRecPtr origin_lsn);
+
+extern void pglogical_write_insert(StringInfo out, PGLogicalOutputData *data,
+							Relation rel, HeapTuple newtuple);
+extern void pglogical_write_update(StringInfo out, PGLogicalOutputData *data,
+							Relation rel, HeapTuple oldtuple,
+							HeapTuple newtuple);
+extern void pglogical_write_delete(StringInfo out, PGLogicalOutputData *data,
+							Relation rel, HeapTuple oldtuple);
+
+extern void write_startup_message(StringInfo out, List *msg);
+
+#endif /* PG_LOGICAL_PROTO_NATIVE_H */
diff --git a/contrib/pglogical_output/pglogical_relmetacache.c b/contrib/pglogical_output/pglogical_relmetacache.c
new file mode 100644
index 0000000..5b4426f
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_relmetacache.c
@@ -0,0 +1,194 @@
+/*-------------------------------------------------------------------------
+ *
+ * pglogical_relmetacache.c
+ *		  Logical Replication relmetacache plugin
+ *
+ * Copyright (c) 2012-2015, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *		  pglogical_relmetacache.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "pglogical_output.h"
+#include "pglogical_relmetacache.h"
+
+#include "utils/catcache.h"
+#include "utils/inval.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+
+static void relmeta_cache_callback(Datum arg, Oid relid);
+
+/*
+ * We need a global hash table that invalidation callbacks can
+ * access because they survive past the logical decoding context and
+ * therefore past our local PGLogicalOutputData's lifetime when
+ * using the SQL interface. We cannot just pass them a pointer to a
+ * palloc'd struct.
+ */
+static HTAB *RelMetaCache = NULL;
+
+
+/*
+ * Initialize the relation metadata cache if not already initialized.
+ *
+ * Purge it if it already exists.
+ *
+ * The hash table its self must be in CacheMemoryContext or TopMemoryContext
+ * since it persists outside the decoding session.
+ */
+void
+pglogical_init_relmetacache(void)
+{
+	HASHCTL	ctl;
+
+	if (RelMetaCache == NULL)
+	{
+		/* first time, init the cache */
+		int hash_flags = HASH_ELEM | HASH_CONTEXT;
+
+		/* Make sure we've initialized CacheMemoryContext. */
+		if (CacheMemoryContext == NULL)
+			CreateCacheMemoryContext();
+
+		MemSet(&ctl, 0, sizeof(ctl));
+		ctl.keysize = sizeof(Oid);
+		ctl.entrysize = sizeof(struct PGLRelMetaCacheEntry);
+		/* safe to allocate to CacheMemoryContext since it's never reset */
+		ctl.hcxt = CacheMemoryContext;
+
+#if PG_VERSION_NUM >= 90500
+		hash_flags |= HASH_BLOBS;
+#else
+		ctl.hash = tag_hash;
+		hash_flags |= HASH_FUNCTION;
+#endif
+
+		RelMetaCache = hash_create("pglogical relation metadata cache", 128,
+									&ctl, hash_flags);
+
+		Assert(RelMetaCache != NULL);
+
+		/*
+		 * Watch for invalidation events.
+		 *
+		 * We don't pass PGLogicalOutputData here because it's scoped to the
+		 * individual decoding session, which with the SQL interface has a shorter
+		 * lifetime than the relcache invalidation callback registration. We have
+		 * no way to remove invalidation callbacks at the end of the decoding
+		 * session so we have to cope with them being called later.
+		 */
+		CacheRegisterRelcacheCallback(relmeta_cache_callback, (Datum)0);
+	}
+	else
+	{
+		/*
+		 * On re-init we must flush the cache since there could be
+		 * dangling pointers to api_private data in the freed
+		 * decoding context of a prior session. We could go through
+		 * and clear them and the is_cached flag but it seems best
+		 * to have a clean slate.
+		 */
+		HASH_SEQ_STATUS status;
+		struct PGLRelMetaCacheEntry *hentry;
+		hash_seq_init(&status, RelMetaCache);
+
+		while ((hentry = (struct PGLRelMetaCacheEntry*) hash_seq_search(&status)) != NULL)
+		{
+			if (hash_search(RelMetaCache,
+						(void *) &hentry->relid,
+						HASH_REMOVE, NULL) == NULL)
+				elog(ERROR, "pglogical RelMetaCache hash table corrupted");
+		}
+
+		return;
+	}
+}
+
+/*
+ * Relation metadata invalidation, for when a relcache invalidation
+ * means that we need to resend table metadata to the client.
+ */
+static void
+relmeta_cache_callback(Datum arg, Oid relid)
+ {
+	/*
+	 * Nobody keeps pointers to entries in this hash table around so
+	 * it's safe to directly HASH_REMOVE the entries as soon as they are
+	 * invalidated. Finding them and flagging them invalid then removing
+	 * them lazily might save some memory churn for tables that get
+	 * repeatedly invalidated and re-sent, but it dodesn't seem worth
+	 * doing.
+	 *
+	 * Getting invalidations for relations that aren't in the table is
+	 * entirely normal, since there's no way to unregister for an
+	 * invalidation event. So we don't care if it's found or not.
+	 */
+	(void) hash_search(RelMetaCache, &relid, HASH_REMOVE, NULL);
+ }
+
+/*
+ * Look up an entry, creating it not found.
+ *
+ * Newly created entries are returned as is_cached=false. The API
+ * hook can set is_cached to skip subsequent updates if it sent a
+ * complete response that the client will cache.
+ *
+ * Returns true on a cache hit, false on a miss.
+ */
+bool
+pglogical_cache_relmeta(struct PGLogicalOutputData *data,
+		Relation rel, struct PGLRelMetaCacheEntry **entry)
+{
+	struct PGLRelMetaCacheEntry *hentry;
+	bool found;
+
+	if (data->relmeta_cache_size == 0)
+	{
+		/*
+		 * If cache is disabled must treat every search as a miss
+		 * and return no entry to populate.
+		 */
+		*entry = NULL;
+		return false;
+	}
+
+	/* Find cached function info, creating if not found */
+	hentry = (struct PGLRelMetaCacheEntry*) hash_search(RelMetaCache,
+										 (void *)(&RelationGetRelid(rel)),
+										 HASH_ENTER, &found);
+
+	if (!found)
+	{
+		Assert(hentry->relid = RelationGetRelid(rel));
+		hentry->is_cached = false;
+		hentry->api_private = NULL;
+	}
+
+	Assert(hentry != NULL);
+
+	*entry = hentry;
+	return hentry->is_cached;
+}
+
+
+/*
+ * Tear down the relation metadata cache.
+ *
+ * Do *not* call this at decoding shutdown. The hash table must
+ * continue to exist so that relcache invalidation callbacks can
+ * continue to reference it after a SQL decoding session finishes.
+ * It must be called at backend shutdown only.
+ */
+void
+pglogical_destroy_relmetacache(void)
+{
+	if (RelMetaCache != NULL)
+	{
+		hash_destroy(RelMetaCache);
+		RelMetaCache = NULL;
+	}
+}
diff --git a/contrib/pglogical_output/pglogical_relmetacache.h b/contrib/pglogical_output/pglogical_relmetacache.h
new file mode 100644
index 0000000..b5e8c01
--- /dev/null
+++ b/contrib/pglogical_output/pglogical_relmetacache.h
@@ -0,0 +1,19 @@
+#ifndef PGLOGICAL_RELMETA_CACHE_H
+#define PGLOGICAL_RELMETA_CACHE_H
+
+struct PGLRelMetaCacheEntry
+{
+	Oid relid;
+	/* Does the client have this relation cached? */
+	bool is_cached;
+	/* Field for API plugin use, must be alloc'd in decoding context */
+	void *api_private;
+};
+
+struct PGLogicalOutputData;
+
+extern void pglogical_init_relmetacache(void);
+extern bool pglogical_cache_relmeta(struct PGLogicalOutputData *data, Relation rel, struct PGLRelMetaCacheEntry **entry);
+extern void pglogical_destroy_relmetacache(void);
+
+#endif /* PGLOGICAL_RELMETA_CACHE_H */
diff --git a/contrib/pglogical_output/regression.conf b/contrib/pglogical_output/regression.conf
new file mode 100644
index 0000000..367f706
--- /dev/null
+++ b/contrib/pglogical_output/regression.conf
@@ -0,0 +1,2 @@
+wal_level = logical
+max_replication_slots = 4
diff --git a/contrib/pglogical_output/sql/basic_json.sql b/contrib/pglogical_output/sql/basic_json.sql
new file mode 100644
index 0000000..e8a2352
--- /dev/null
+++ b/contrib/pglogical_output/sql/basic_json.sql
@@ -0,0 +1,24 @@
+\i sql/basic_setup.sql
+
+-- Simple decode with text-format tuples
+TRUNCATE TABLE json_decoding_output;
+
+INSERT INTO json_decoding_output(ch, rn)
+SELECT
+  data::jsonb,
+  row_number() OVER ()
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+
+SELECT * FROM get_startup_params();
+SELECT * FROM get_queued_data();
+
+TRUNCATE TABLE json_decoding_output;
+
+\i sql/basic_teardown.sql
diff --git a/contrib/pglogical_output/sql/basic_native.sql b/contrib/pglogical_output/sql/basic_native.sql
new file mode 100644
index 0000000..6f1862b
--- /dev/null
+++ b/contrib/pglogical_output/sql/basic_native.sql
@@ -0,0 +1,37 @@
+\i sql/basic_setup.sql
+
+-- Simple decode with text-format tuples
+--
+-- It's still the logical decoding binary protocol and as such it has
+-- embedded timestamps, and pglogical its self has embedded LSNs, xids,
+-- etc. So all we can really do is say "yup, we got the expected number
+-- of messages".
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+
+-- ... and send/recv binary format
+-- The main difference visible is that the bytea fields aren't encoded
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'binary.want_binary_basetypes', '1',
+	'binary.basetypes_major_version', (current_setting('server_version_num')::integer / 100)::text);
+
+-- Now enable the relation metadata cache and verify that we get the expected
+-- reduction in number of messages. Not much else we can look for.
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'relmeta_cache_size', '-1');
+
+\i sql/basic_teardown.sql
diff --git a/contrib/pglogical_output/sql/basic_setup.sql b/contrib/pglogical_output/sql/basic_setup.sql
new file mode 100644
index 0000000..19e154c
--- /dev/null
+++ b/contrib/pglogical_output/sql/basic_setup.sql
@@ -0,0 +1,62 @@
+SET synchronous_commit = on;
+
+-- Schema setup
+
+CREATE TABLE demo (
+	seq serial primary key,
+	tx text,
+	ts timestamp,
+	jsb jsonb,
+	js json,
+	ba bytea
+);
+
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+
+-- Queue up some work to decode with a variety of types
+
+INSERT INTO demo(tx) VALUES ('textval');
+INSERT INTO demo(ba) VALUES (BYTEA '\xDEADBEEF0001');
+INSERT INTO demo(ts, tx) VALUES (TIMESTAMP '2045-09-12 12:34:56.00', 'blah');
+INSERT INTO demo(js, jsb) VALUES ('{"key":"value"}', '{"key":"value"}');
+
+-- Rolled back txn
+BEGIN;
+DELETE FROM demo;
+INSERT INTO demo(tx) VALUES ('blahblah');
+ROLLBACK;
+
+-- Multi-statement transaction with subxacts
+BEGIN;
+SAVEPOINT sp1;
+INSERT INTO demo(tx) VALUES ('row1');
+RELEASE SAVEPOINT sp1;
+SAVEPOINT sp2;
+UPDATE demo SET tx = 'update-rollback' WHERE tx = 'row1';
+ROLLBACK TO SAVEPOINT sp2;
+SAVEPOINT sp3;
+INSERT INTO demo(tx) VALUES ('row2');
+INSERT INTO demo(tx) VALUES ('row3');
+RELEASE SAVEPOINT sp3;
+SAVEPOINT sp4;
+DELETE FROM demo WHERE tx = 'row2';
+RELEASE SAVEPOINT sp4;
+SAVEPOINT sp5;
+UPDATE demo SET tx = 'updated' WHERE tx = 'row1';
+COMMIT;
+
+
+-- txn with catalog changes
+BEGIN;
+CREATE TABLE cat_test(id integer);
+INSERT INTO cat_test(id) VALUES (42);
+COMMIT;
+
+-- Aborted subxact with catalog changes
+BEGIN;
+INSERT INTO demo(tx) VALUES ('1');
+SAVEPOINT sp1;
+ALTER TABLE demo DROP COLUMN tx;
+ROLLBACK TO SAVEPOINT sp1;
+INSERT INTO demo(tx) VALUES ('2');
+COMMIT;
diff --git a/contrib/pglogical_output/sql/basic_teardown.sql b/contrib/pglogical_output/sql/basic_teardown.sql
new file mode 100644
index 0000000..d7a752f
--- /dev/null
+++ b/contrib/pglogical_output/sql/basic_teardown.sql
@@ -0,0 +1,4 @@
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+
+DROP TABLE demo;
+DROP TABLE cat_test;
diff --git a/contrib/pglogical_output/sql/cleanup.sql b/contrib/pglogical_output/sql/cleanup.sql
new file mode 100644
index 0000000..e7a02c8
--- /dev/null
+++ b/contrib/pglogical_output/sql/cleanup.sql
@@ -0,0 +1,4 @@
+DROP TABLE excluded_startup_keys;
+DROP TABLE json_decoding_output;
+DROP FUNCTION get_queued_data();
+DROP FUNCTION get_startup_params();
diff --git a/contrib/pglogical_output/sql/encoding_json.sql b/contrib/pglogical_output/sql/encoding_json.sql
new file mode 100644
index 0000000..543c306
--- /dev/null
+++ b/contrib/pglogical_output/sql/encoding_json.sql
@@ -0,0 +1,58 @@
+SET synchronous_commit = on;
+
+-- This file doesn't share common setup with the native tests,
+-- since it's specific to how the text protocol handles encodings.
+
+CREATE TABLE enctest(blah text);
+
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+
+
+SET client_encoding = 'UTF-8';
+INSERT INTO enctest(blah)
+VALUES
+('áàä'),('fl'), ('½⅓'), ('カンジ');
+RESET client_encoding;
+
+
+SET client_encoding = 'LATIN-1';
+
+-- Will ERROR, explicit encoding request doesn't match client_encoding
+SELECT data
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+
+-- Will succeed since we don't request any encoding
+-- then ERROR because it can't turn the kanjii into latin-1
+SELECT data
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+
+-- Will succeed since it matches the current encoding
+-- then ERROR because it can't turn the kanjii into latin-1
+SELECT data
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'LATIN-1',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+
+RESET client_encoding;
+
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+
+DROP TABLE enctest;
diff --git a/contrib/pglogical_output/sql/extension.sql b/contrib/pglogical_output/sql/extension.sql
new file mode 100644
index 0000000..00937bf
--- /dev/null
+++ b/contrib/pglogical_output/sql/extension.sql
@@ -0,0 +1,7 @@
+CREATE EXTENSION pglogical_output;
+
+SELECT pglogical_output_proto_version();
+
+SELECT pglogical_output_min_proto_version();
+
+DROP EXTENSION pglogical_output;
diff --git a/contrib/pglogical_output/sql/hooks_json.sql b/contrib/pglogical_output/sql/hooks_json.sql
new file mode 100644
index 0000000..cd58960
--- /dev/null
+++ b/contrib/pglogical_output/sql/hooks_json.sql
@@ -0,0 +1,49 @@
+\i sql/hooks_setup.sql
+
+
+-- Test table filter
+TRUNCATE TABLE json_decoding_output;
+
+INSERT INTO json_decoding_output(ch, rn)
+SELECT
+  data::jsonb,
+  row_number() OVER ()
+FROM pg_logical_slot_peek_changes('regression_slot',
+ 	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.test_filter',
+	'pglo_plhooks.client_hook_arg', 'foo',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+
+SELECT * FROM get_startup_params();
+SELECT * FROM get_queued_data();
+
+-- test action filter
+TRUNCATE TABLE json_decoding_output;
+
+INSERT INTO json_decoding_output (ch, rn)
+SELECT
+  data::jsonb,
+  row_number() OVER ()
+FROM pg_logical_slot_peek_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.test_action_filter',
+	'proto_format', 'json',
+	'no_txinfo', 't');
+
+SELECT * FROM get_startup_params();
+SELECT * FROM get_queued_data();
+
+TRUNCATE TABLE json_decoding_output;
+
+\i sql/hooks_teardown.sql
diff --git a/contrib/pglogical_output/sql/hooks_native.sql b/contrib/pglogical_output/sql/hooks_native.sql
new file mode 100644
index 0000000..e2bfc54
--- /dev/null
+++ b/contrib/pglogical_output/sql/hooks_native.sql
@@ -0,0 +1,48 @@
+\i sql/hooks_setup.sql
+
+-- Regular hook setup
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.test_filter',
+	'pglo_plhooks.client_hook_arg', 'foo'
+	);
+
+-- Test action filter
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.test_action_filter'
+	);
+
+-- Invalid row fiter hook function
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.nosuchfunction'
+	);
+
+-- Hook filter functoin with wrong signature
+SELECT count(data) FROM pg_logical_slot_peek_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'hooks.setup_function', 'public.pglo_plhooks_setup_fn',
+	'pglo_plhooks.row_filter_hook', 'public.wrong_signature_fn'
+	);
+
+\i sql/hooks_teardown.sql
diff --git a/contrib/pglogical_output/sql/hooks_setup.sql b/contrib/pglogical_output/sql/hooks_setup.sql
new file mode 100644
index 0000000..4de15b7
--- /dev/null
+++ b/contrib/pglogical_output/sql/hooks_setup.sql
@@ -0,0 +1,37 @@
+CREATE EXTENSION pglogical_output_plhooks;
+
+CREATE FUNCTION test_filter(relid regclass, action "char", nodeid text)
+returns bool stable language plpgsql AS $$
+BEGIN
+	IF nodeid <> 'foo' THEN
+	    RAISE EXCEPTION 'Expected nodeid <foo>, got <%>',nodeid;
+	END IF;
+	RETURN relid::regclass::text NOT LIKE '%_filter%';
+END
+$$;
+
+CREATE FUNCTION test_action_filter(relid regclass, action "char", nodeid text)
+returns bool stable language plpgsql AS $$
+BEGIN
+    RETURN action NOT IN ('U', 'D');
+END
+$$;
+
+CREATE FUNCTION wrong_signature_fn(relid regclass)
+returns bool stable language plpgsql as $$
+BEGIN
+END;
+$$;
+
+CREATE TABLE test_filter(id integer);
+CREATE TABLE test_nofilt(id integer);
+
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+
+INSERT INTO test_filter(id) SELECT generate_series(1,10);
+INSERT INTO test_nofilt(id) SELECT generate_series(1,10);
+
+DELETE FROM test_filter WHERE id % 2 = 0;
+DELETE FROM test_nofilt WHERE id % 2 = 0;
+UPDATE test_filter SET id = id*100 WHERE id = 5;
+UPDATE test_nofilt SET id = id*100 WHERE id = 5;
diff --git a/contrib/pglogical_output/sql/hooks_teardown.sql b/contrib/pglogical_output/sql/hooks_teardown.sql
new file mode 100644
index 0000000..837e2d0
--- /dev/null
+++ b/contrib/pglogical_output/sql/hooks_teardown.sql
@@ -0,0 +1,10 @@
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
+
+DROP TABLE test_filter;
+DROP TABLE test_nofilt;
+
+DROP FUNCTION test_filter(relid regclass, action "char", nodeid text);
+DROP FUNCTION test_action_filter(relid regclass, action "char", nodeid text);
+DROP FUNCTION wrong_signature_fn(relid regclass);
+
+DROP EXTENSION pglogical_output_plhooks;
diff --git a/contrib/pglogical_output/sql/params_native.sql b/contrib/pglogical_output/sql/params_native.sql
new file mode 100644
index 0000000..9203bd7
--- /dev/null
+++ b/contrib/pglogical_output/sql/params_native.sql
@@ -0,0 +1,104 @@
+SET synchronous_commit = on;
+
+-- no need to CREATE EXTENSION as we intentionally don't have any catalog presence
+-- Instead, just create a slot.
+
+SELECT 'init' FROM pg_create_logical_replication_slot('regression_slot', 'pglogical_output');
+
+-- Minimal invocation with no data
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+
+--
+-- Various invalid parameter combos:
+--
+
+-- Text mode is not supported for native protocol
+SELECT data FROM pg_logical_slot_get_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+
+-- error, only supports proto v1
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '2',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+
+-- error, only supports proto v1
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '2',
+	'max_proto_version', '2',
+	'startup_params_format', '1');
+
+-- error, unrecognised startup params format
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '2');
+
+-- Should be OK and result in proto version 1 selection, though we won't
+-- see that here.
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '2',
+	'startup_params_format', '1');
+
+-- no such encoding / encoding mismatch
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'bork',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+
+-- Different spellings of encodings are OK too
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF-8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1');
+
+-- bogus param format
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'invalid');
+
+-- native params format explicitly
+SELECT data FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'proto_format', 'native');
+
+-- relmeta cache with fixed size (not supported yet, so error)
+SELECT count(data) FROM pg_logical_slot_get_binary_changes('regression_slot',
+	NULL, NULL,
+	'expected_encoding', 'UTF8',
+	'min_proto_version', '1',
+	'max_proto_version', '1',
+	'startup_params_format', '1',
+	'relmeta_cache_size', '200');
+
+SELECT 'drop' FROM pg_drop_replication_slot('regression_slot');
diff --git a/contrib/pglogical_output/sql/prep.sql b/contrib/pglogical_output/sql/prep.sql
new file mode 100644
index 0000000..26e79c8
--- /dev/null
+++ b/contrib/pglogical_output/sql/prep.sql
@@ -0,0 +1,30 @@
+CREATE TABLE excluded_startup_keys (key_name text primary key);
+
+INSERT INTO excluded_startup_keys
+VALUES
+('pg_version_num'),('pg_version'),('pg_catversion'),('binary.basetypes_major_version'),('binary.integer_datetimes'),('binary.bigendian'),('binary.maxalign'),('binary.binary_pg_version'),('sizeof_int'),('sizeof_long'),('sizeof_datum');
+
+CREATE UNLOGGED TABLE json_decoding_output(ch jsonb, rn integer);
+
+CREATE OR REPLACE FUNCTION get_startup_params()
+RETURNS TABLE ("key" text, "value" jsonb)
+LANGUAGE sql
+AS $$
+SELECT key, value
+FROM json_decoding_output
+CROSS JOIN LATERAL jsonb_each(ch -> 'params')
+WHERE rn = 1
+  AND key NOT IN (SELECT * FROM excluded_startup_keys)
+  AND ch ->> 'action' = 'S'
+ORDER BY key;
+$$;
+
+CREATE OR REPLACE FUNCTION get_queued_data()
+RETURNS TABLE (data jsonb)
+LANGUAGE sql
+AS $$
+SELECT ch
+FROM json_decoding_output
+WHERE rn > 1
+ORDER BY rn ASC;
+$$;
diff --git a/contrib/pglogical_output_plhooks/.gitignore b/contrib/pglogical_output_plhooks/.gitignore
new file mode 100644
index 0000000..140f8cf
--- /dev/null
+++ b/contrib/pglogical_output_plhooks/.gitignore
@@ -0,0 +1 @@
+*.so
diff --git a/contrib/pglogical_output_plhooks/Makefile b/contrib/pglogical_output_plhooks/Makefile
new file mode 100644
index 0000000..ecd3f89
--- /dev/null
+++ b/contrib/pglogical_output_plhooks/Makefile
@@ -0,0 +1,13 @@
+MODULES = pglogical_output_plhooks
+EXTENSION = pglogical_output_plhooks
+DATA = pglogical_output_plhooks--1.0.sql
+DOCS = README.pglogical_output_plhooks
+
+subdir = contrib/pglogical_output_plhooks
+top_builddir = ../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+
+# Allow the hook plugin to see the pglogical_output headers
+# Necessary because !PGXS builds don't respect PG_CPPFLAGS
+override CPPFLAGS := $(CPPFLAGS) -I$(top_srcdir)/contrib/pglogical_output
diff --git a/contrib/pglogical_output_plhooks/README.pglogical_output_plhooks b/contrib/pglogical_output_plhooks/README.pglogical_output_plhooks
new file mode 100644
index 0000000..f2ad9d4
--- /dev/null
+++ b/contrib/pglogical_output_plhooks/README.pglogical_output_plhooks
@@ -0,0 +1,158 @@
+pglogical_output_plhooks is an example module for pglogical_output, showing how
+hooks can be implemented.
+
+It provides C wrappers to allow hooks to be written in any supported PL,
+such as PL/PgSQL.
+
+No effort is made to be efficient. To avoid the need to set up cache
+invalidation handling function calls are done via oid each time, with no
+FmgrInfo caching.  Also, memory contexts are reset rather freely. If you
+want efficiency, write your hook in C.
+
+(Catalog timetravel is another reason not to write hooks in PLs; see below).
+
+Simple pointless example
+===
+
+To compile and install, just "make USE_PGXS=1 install". Note that pglogical
+must already be installed so that its headers can be found. You might have
+to set the `PATH` so that `pg_config` can be found.
+
+To use it:
+
+    CREATE EXTENSION pglogical_output_plhooks IN SCHEMA public;
+
+in the target database.
+
+Then create at least one hook procedure, of the supported hooks listed below.
+For the sake of this example we'll use some of the toy examples provided in the
+extension:
+
+* startup function: pglo_plhooks_demo_startup
+* row filter: pglo_plhooks_demo_row_filter
+* txn filter: pglo_plhooks_demo_txn_filter
+* shutdown function: pglo_plhooks_demo_shutdown
+
+Now add some arguments to your pglogical_output client's logical decoding setup
+parameters to specify the hook setup function and to tell
+pglogical_output_plhooks about one or more of the hooks you wish it to run. For
+example you might add the following parameters:
+
+	hooks.setup_function, public.pglo_plhooks_setup_fn,
+	pglo_plhooks.startup_hook, pglo_plhooks_demo_startup,
+	pglo_plhooks.row_filter_hook, pglo_plhooks_demo_row_filter,
+	pglo_plhooks.txn_filter_hook, pglo_plhooks_demo_txn_filter,
+	pglo_plhooks.shutdown_hook, pglo_plhooks_demo_shutdown,
+	pglo_plhooks.client_hook_arg, 'whatever-you-want'
+
+to configure the extension to load its hooks, then configure all the demo hooks.
+
+Why the preference for C hooks?
+===
+
+Speed. The row filter hook is called for *every single row* replicated.
+
+If a hook raises an ERROR then replication will probably stop. You won't be
+able to fix it either, because when you change the hook definition the new
+definition won't be visible in the catalogs at the current replay position due
+to catalog time travel. The old definition that raises an error will keep being
+used. You'll need to remove the problem hook from your logical decoding startup
+parameters, which will disable use the hook entirely, until replay proceeds
+past the point you fixed the problem with the hook function.
+
+Similarly, if you try to add use of a newly defined hook on an existing
+replication slot that hasn't replayed past the point you defined the hook yet,
+you'll get an error complaining that the hook function doesn't exist. Even
+though it clearly does when you look at it in psql. The reason is the same: in
+the time traveled catalogs it really doesn't exist. You have to replay past the
+point the hook was created then enable it. In this case the
+pglogical_output_plhooks startup hook will actually see your functions, but
+fail when it tries to call them during decoding since they'll appear to have
+vanished.
+
+If you write your hooks in C you can redefine them rather more easily, since
+the function definition is not subject to catalog timetravel. More importantly,
+it'll probably be a lot faster. The plhooks code has to do a lot of translation
+to pass information to the PL functions and more to get results back; it also
+has to do a lot of memory allocations and a memory context reset after each
+call. That all adds up.
+
+(You could actually write C functions to be called by this extension, but
+that'd be crazy.)
+
+Available hooks
+===
+
+The four hooks provided by pglogical_output are exposed by the module. See the
+pglogical_output documentation for details on what each hook does and when it
+runs.
+
+A function for each hook must have *exactly* the specified parameters and
+return value, or you'll get an error.
+
+None of the functions may return NULL. If they do you'll get an error.
+
+If you specified `pglo_plhooks.client_hook_arg` in the startup parameters it is
+passed as `client_hook_arg` to all hooks. If not specified the empty string is
+passed.
+
+You can find some toy examples in `pglogical_output_plhooks--1.0.sql`.
+
+
+
+Startup hook
+---
+
+Configured with `pglo_plhooks.startup_hook` startup parameter. Runs when
+logical decoding starts.
+
+Signature *must* be:
+
+    CREATE FUNCTION whatever_funcname(startup_params text[], client_hook_arg text)
+    RETURNS text[]
+
+startup_params is an array of the startup params passed to the pglogical output
+plugin, as alternating key/value elements in text representation.
+
+client_hook_arg is also passed.
+
+The return value is an array of alternating key/value elements forming a set
+of parameters you wish to add to the startup reply message sent by pglogical
+on decoding start. It must not be null; return `ARRAY[]::text[]` if you don't
+want to add any params.
+
+Transaction filter
+---
+
+The arguments are the replication origin identifier and the client hook param.
+
+The return value is true to keep the transaction, false to discard it.
+
+Signature:
+
+	CREATE FUNCTION whatevername(origin_id int, client_hook_arg text)
+	RETURNS boolean
+
+Row filter
+--
+
+Called for each row. Return true to replicate the row, false to discard it.
+
+Arguments are the oid of the affected relation, and the change type: 'I'nsert,
+'U'pdate or 'D'elete. There is no way to access the change data - columns changed,
+new values, etc.
+
+Signature:
+
+	CREATE FUNCTION whatevername(affected_rel regclass, change_type "char", client_hook_arg text)
+	RETURNS boolean
+
+Shutdown hook
+--
+
+Pretty uninteresting, but included for completeness.
+
+Signature:
+
+	CREATE FUNCTION whatevername(client_hook_arg text)
+	RETURNS void
diff --git a/contrib/pglogical_output_plhooks/pglogical_output_plhooks--1.0.sql b/contrib/pglogical_output_plhooks/pglogical_output_plhooks--1.0.sql
new file mode 100644
index 0000000..cdd2af3
--- /dev/null
+++ b/contrib/pglogical_output_plhooks/pglogical_output_plhooks--1.0.sql
@@ -0,0 +1,89 @@
+\echo Use "CREATE EXTENSION pglogical_output_plhooks" to load this file. \quit
+
+-- Use @extschema@ or leave search_path unchanged, don't use explicit schema
+
+CREATE FUNCTION pglo_plhooks_setup_fn(internal)
+RETURNS void
+STABLE
+LANGUAGE c AS 'MODULE_PATHNAME';
+
+COMMENT ON FUNCTION pglo_plhooks_setup_fn(internal)
+IS 'Register pglogical output pl hooks. See docs for how to specify functions';
+
+--
+-- Called as the startup hook.
+--
+-- There's no useful way to expose the private data segment, so you
+-- just don't get to use that from pl hooks at this point. The C
+-- wrapper will extract a startup param named pglo_plhooks.client_hook_arg
+-- for you and pass it as client_hook_arg to all callbacks, though.
+--
+-- For implementation convenience, a null client_hook_arg is passed
+-- as the empty string.
+--
+-- Must return the empty array, not NULL, if it has nothing to add.
+--
+CREATE FUNCTION pglo_plhooks_demo_startup(startup_params text[], client_hook_arg text)
+RETURNS text[]
+LANGUAGE plpgsql AS $$
+DECLARE
+    elem text;
+	paramname text;
+	paramvalue text;
+BEGIN
+	FOREACH elem IN ARRAY startup_params
+	LOOP
+		IF elem IS NULL THEN
+				RAISE EXCEPTION 'Startup params may not be null';
+		END IF;
+
+		IF paramname IS NULL THEN
+				paramname := elem;
+		ELSIF paramvalue IS NULL THEN
+				paramvalue := elem;
+		ELSE
+				RAISE NOTICE 'got param: % = %', paramname, paramvalue;
+				paramname := NULL;
+				paramvalue := NULL;
+		END IF;
+	END LOOP;
+
+	RETURN ARRAY['pglo_plhooks_demo_startup_ran', 'true', 'otherparam', '42'];
+END;
+$$;
+
+CREATE FUNCTION pglo_plhooks_demo_txn_filter(origin_id int, client_hook_arg text)
+RETURNS boolean
+LANGUAGE plpgsql AS $$
+BEGIN
+		-- Not much to filter on really...
+		RAISE NOTICE 'Got tx with origin %',origin_id;
+		RETURN true;
+END;
+$$;
+
+CREATE FUNCTION pglo_plhooks_demo_row_filter(affected_rel regclass, change_type "char", client_hook_arg text)
+RETURNS boolean
+LANGUAGE plpgsql AS $$
+BEGIN
+		-- This is a totally absurd test, since it checks if the upstream user
+		-- doing replication has rights to make modifications that have already
+		-- been committed and are being decoded for replication. Still, it shows
+		-- how the hook works...
+		IF pg_catalog.has_table_privilege(current_user, affected_rel,
+				CASE change_type WHEN 'I' THEN 'INSERT' WHEN 'U' THEN 'UPDATE' WHEN 'D' THEN 'DELETE' END)
+		THEN
+				RETURN true;
+		ELSE
+				RETURN false;
+		END IF;
+END;
+$$;
+
+CREATE FUNCTION pglo_plhooks_demo_shutdown(client_hook_arg text)
+RETURNS void
+LANGUAGE plpgsql AS $$
+BEGIN
+		RAISE NOTICE 'Decoding shutdown';
+END;
+$$
diff --git a/contrib/pglogical_output_plhooks/pglogical_output_plhooks.c b/contrib/pglogical_output_plhooks/pglogical_output_plhooks.c
new file mode 100644
index 0000000..a5144f0
--- /dev/null
+++ b/contrib/pglogical_output_plhooks/pglogical_output_plhooks.c
@@ -0,0 +1,414 @@
+#include "postgres.h"
+
+#include "pglogical_output/hooks.h"
+
+#include "access/xact.h"
+
+#include "catalog/pg_type.h"
+
+#include "nodes/makefuncs.h"
+
+#include "parser/parse_func.h"
+
+#include "replication/reorderbuffer.h"
+
+#include "utils/acl.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/lsyscache.h"
+#include "utils/memutils.h"
+#include "utils/rel.h"
+
+#include "fmgr.h"
+#include "miscadmin.h"
+
+PG_MODULE_MAGIC;
+
+PGDLLEXPORT extern Datum pglo_plhooks_setup_fn(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(pglo_plhooks_setup_fn);
+
+void pglo_plhooks_startup(struct PGLogicalStartupHookArgs *startup_args);
+void pglo_plhooks_shutdown(struct PGLogicalShutdownHookArgs *shutdown_args);
+bool pglo_plhooks_row_filter(struct PGLogicalRowFilterArgs *rowfilter_args);
+bool pglo_plhooks_txn_filter(struct PGLogicalTxnFilterArgs *txnfilter_args);
+
+typedef struct PLHPrivate
+{
+	const char *client_arg;
+	Oid startup_hook;
+	Oid shutdown_hook;
+	Oid row_filter_hook;
+	Oid txn_filter_hook;
+	MemoryContext hook_call_context;
+} PLHPrivate;
+
+static void read_parameters(PLHPrivate *private, List *in_params);
+static Oid find_startup_hook(const char *proname);
+static Oid find_shutdown_hook(const char *proname);
+static Oid find_row_filter_hook(const char *proname);
+static Oid find_txn_filter_hook(const char *proname);
+static void exec_user_startup_hook(PLHPrivate *private, List *in_params, List **out_params);
+
+void
+pglo_plhooks_startup(struct PGLogicalStartupHookArgs *startup_args)
+{
+	PLHPrivate *private;
+
+	/* pglogical_output promises to call us in a tx */
+	Assert(IsTransactionState());
+
+	/* Allocated in hook memory context, scoped to the logical decoding session: */
+	startup_args->private_data = private = (PLHPrivate*)palloc(sizeof(PLHPrivate));
+
+	private->startup_hook = InvalidOid;
+	private->shutdown_hook = InvalidOid;
+	private->row_filter_hook = InvalidOid;
+	private->txn_filter_hook = InvalidOid;
+	/* client_arg is the empty string when not specified to simplify function calls */
+	private->client_arg = "";
+
+	read_parameters(private, startup_args->in_params);
+
+	private->hook_call_context = AllocSetContextCreate(CurrentMemoryContext,
+			                    "pglogical_output plhooks hook call context",
+			                    ALLOCSET_SMALL_MINSIZE,
+			                    ALLOCSET_SMALL_INITSIZE,
+			                    ALLOCSET_SMALL_MAXSIZE);
+
+
+	if (private->startup_hook != InvalidOid)
+		exec_user_startup_hook(private, startup_args->in_params, &startup_args->out_params);
+}
+
+void
+pglo_plhooks_shutdown(struct PGLogicalShutdownHookArgs *shutdown_args)
+{
+	PLHPrivate *private = (PLHPrivate*)shutdown_args->private_data;
+	MemoryContext old_ctx;
+
+	Assert(private != NULL);
+
+	if (OidIsValid(private->shutdown_hook))
+	{
+		old_ctx = MemoryContextSwitchTo(private->hook_call_context);
+		elog(DEBUG3, "calling pglo shutdown hook with %s", private->client_arg);
+		(void) OidFunctionCall1(
+				private->shutdown_hook,
+				CStringGetTextDatum(private->client_arg));
+		elog(DEBUG3, "called pglo shutdown hook");
+		MemoryContextSwitchTo(old_ctx);
+		MemoryContextReset(private->hook_call_context);
+	}
+}
+
+bool
+pglo_plhooks_row_filter(struct PGLogicalRowFilterArgs *rowfilter_args)
+{
+	PLHPrivate *private = (PLHPrivate*)rowfilter_args->private_data;
+	bool ret = true;
+	MemoryContext old_ctx;
+
+	Assert(private != NULL);
+
+	if (OidIsValid(private->row_filter_hook))
+	{
+		char change_type;
+		switch (rowfilter_args->change_type)
+		{
+			case REORDER_BUFFER_CHANGE_INSERT:
+				change_type = 'I';
+				break;
+			case REORDER_BUFFER_CHANGE_UPDATE:
+				change_type = 'U';
+				break;
+			case REORDER_BUFFER_CHANGE_DELETE:
+				change_type = 'D';
+				break;
+			default:
+				elog(ERROR, "unknown change type %d", rowfilter_args->change_type);
+				change_type = '0';	/* silence compiler */
+		}
+
+		old_ctx = MemoryContextSwitchTo(private->hook_call_context);
+		elog(DEBUG3, "calling pglo row filter hook with (%u,%c,%s)",
+				rowfilter_args->changed_rel->rd_id, change_type,
+				private->client_arg);
+		ret = DatumGetBool(OidFunctionCall3(
+				private->row_filter_hook,
+				ObjectIdGetDatum(rowfilter_args->changed_rel->rd_id),
+				CharGetDatum(change_type),
+				CStringGetTextDatum(private->client_arg)));
+		elog(DEBUG3, "called pglo row filter hook, returns %d", (int)ret);
+		MemoryContextSwitchTo(old_ctx);
+		MemoryContextReset(private->hook_call_context);
+	}
+
+	return ret;
+}
+
+bool
+pglo_plhooks_txn_filter(struct PGLogicalTxnFilterArgs *txnfilter_args)
+{
+	PLHPrivate *private = (PLHPrivate*)txnfilter_args->private_data;
+	bool ret = true;
+	MemoryContext old_ctx;
+
+	Assert(private != NULL);
+
+
+	if (OidIsValid(private->txn_filter_hook))
+	{
+		old_ctx = MemoryContextSwitchTo(private->hook_call_context);
+
+		elog(DEBUG3, "calling pglo txn filter hook with (%hu,%s)",
+				txnfilter_args->origin_id, private->client_arg);
+		ret = DatumGetBool(OidFunctionCall2(
+					private->txn_filter_hook,
+					UInt16GetDatum(txnfilter_args->origin_id),
+					CStringGetTextDatum(private->client_arg)));
+		elog(DEBUG3, "calling pglo txn filter hook, returns %d", (int)ret);
+
+		MemoryContextSwitchTo(old_ctx);
+		MemoryContextReset(private->hook_call_context);
+	}
+
+	return ret;
+}
+
+Datum
+pglo_plhooks_setup_fn(PG_FUNCTION_ARGS)
+{
+	struct PGLogicalHooks *hooks = (struct PGLogicalHooks*) PG_GETARG_POINTER(0);
+
+	/* Your code doesn't need this, it's just for the tests: */
+	Assert(hooks != NULL);
+	Assert(hooks->hooks_private_data == NULL);
+	Assert(hooks->startup_hook == NULL);
+	Assert(hooks->shutdown_hook == NULL);
+	Assert(hooks->row_filter_hook == NULL);
+	Assert(hooks->txn_filter_hook == NULL);
+
+	/*
+	 * Just assign the hook pointers. We're not meant to do much
+	 * work here.
+	 *
+	 * Note that private_data is left untouched, to be set up by the
+	 * startup hook.
+	 */
+	hooks->startup_hook = pglo_plhooks_startup;
+	hooks->shutdown_hook = pglo_plhooks_shutdown;
+	hooks->row_filter_hook = pglo_plhooks_row_filter;
+	hooks->txn_filter_hook = pglo_plhooks_txn_filter;
+	elog(DEBUG3, "configured pglo hooks");
+
+	PG_RETURN_VOID();
+}
+
+static void
+exec_user_startup_hook(PLHPrivate *private, List *in_params, List **out_params)
+{
+		ArrayType *startup_params;
+		Datum ret;
+		ListCell *lc;
+		Datum *startup_params_elems;
+		bool  *startup_params_isnulls;
+		int   n_startup_params;
+		int   i;
+		MemoryContext old_ctx;
+
+
+		old_ctx = MemoryContextSwitchTo(private->hook_call_context);
+
+		/*
+		 * Build the input parameter array. NULL parameters are passed as the
+		 * empty string for the sake of convenience. Each param is two
+		 * elements, a key then a value element.
+		 */
+		n_startup_params = list_length(in_params) * 2;
+		startup_params_elems = (Datum*)palloc0(sizeof(Datum)*n_startup_params);
+
+		i = 0;
+		foreach (lc, in_params)
+		{
+			DefElem * elem = (DefElem*)lfirst(lc);
+			const char *val;
+
+			if (elem->arg == NULL || strVal(elem->arg) == NULL)
+				val = "";
+			else
+				val = strVal(elem->arg);
+
+			startup_params_elems[i++] = CStringGetTextDatum(elem->defname);
+			startup_params_elems[i++] = CStringGetTextDatum(val);
+		}
+		Assert(i == n_startup_params);
+
+		startup_params = construct_array(startup_params_elems, n_startup_params,
+				TEXTOID, -1, false, 'i');
+
+		ret = OidFunctionCall2(
+				private->startup_hook,
+				PointerGetDatum(startup_params),
+				CStringGetTextDatum(private->client_arg));
+
+		/*
+		 * deconstruct return array and add pairs of results to a DefElem list.
+		 */
+		deconstruct_array(DatumGetArrayTypeP(ret), TEXTARRAYOID,
+				-1, false, 'i', &startup_params_elems, &startup_params_isnulls,
+				&n_startup_params);
+
+
+		*out_params = NIL;
+		for (i = 0; i < n_startup_params; i = i + 2)
+		{
+			char *value;
+			DefElem *elem;
+
+			if (startup_params_isnulls[i])
+				elog(ERROR, "Array entry corresponding to a key was null at idx=%d", i);
+
+			if (startup_params_isnulls[i+1])
+				value = "";
+			else
+				value = TextDatumGetCString(startup_params_elems[i+1]);
+
+			elem = makeDefElem(
+					TextDatumGetCString(startup_params_elems[i]),
+					(Node*)makeString(value));
+
+			*out_params = lcons(elem, *out_params);
+		}
+
+		MemoryContextSwitchTo(old_ctx);
+		MemoryContextReset(private->hook_call_context);
+}
+
+static void
+read_parameters(PLHPrivate *private, List *in_params)
+{
+	ListCell *option;
+
+	foreach(option, in_params)
+	{
+		DefElem    *elem = lfirst(option);
+
+		if (pg_strcasecmp("pglo_plhooks.client_hook_arg", elem->defname) == 0)
+		{
+			if (elem->arg == NULL || strVal(elem->arg) == NULL)
+				elog(ERROR, "pglo_plhooks.client_hook_arg may not be NULL");
+			private->client_arg = pstrdup(strVal(elem->arg));
+		}
+
+		if (pg_strcasecmp("pglo_plhooks.startup_hook", elem->defname) == 0)
+		{
+			if (elem->arg == NULL || strVal(elem->arg) == NULL)
+				elog(ERROR, "pglo_plhooks.startup_hook may not be NULL");
+			private->startup_hook = find_startup_hook(strVal(elem->arg));
+		}
+
+		if (pg_strcasecmp("pglo_plhooks.shutdown_hook", elem->defname) == 0)
+		{
+			if (elem->arg == NULL || strVal(elem->arg) == NULL)
+				elog(ERROR, "pglo_plhooks.shutdown_hook may not be NULL");
+			private->shutdown_hook = find_shutdown_hook(strVal(elem->arg));
+		}
+
+		if (pg_strcasecmp("pglo_plhooks.txn_filter_hook", elem->defname) == 0)
+		{
+			if (elem->arg == NULL || strVal(elem->arg) == NULL)
+				elog(ERROR, "pglo_plhooks.txn_filter_hook may not be NULL");
+			private->txn_filter_hook = find_txn_filter_hook(strVal(elem->arg));
+		}
+
+		if (pg_strcasecmp("pglo_plhooks.row_filter_hook", elem->defname) == 0)
+		{
+			if (elem->arg == NULL || strVal(elem->arg) == NULL)
+				elog(ERROR, "pglo_plhooks.row_filter_hook may not be NULL");
+			private->row_filter_hook = find_row_filter_hook(strVal(elem->arg));
+		}
+	}
+}
+
+static Oid
+find_hook_fn(const char *funcname, Oid funcargtypes[], int nfuncargtypes, Oid returntype)
+{
+	Oid			funcid;
+	List	   *qname;
+
+	qname = stringToQualifiedNameList(funcname);
+
+	/* find the the function */
+	funcid = LookupFuncName(qname, nfuncargtypes, funcargtypes, false);
+
+	/* Check expected return type */
+	if (get_func_rettype(funcid) != returntype)
+	{
+		ereport(ERROR,
+				(errcode(ERRCODE_WRONG_OBJECT_TYPE),
+				 errmsg("function %s doesn't return expected type %d",
+						NameListToString(qname), returntype)));
+	}
+
+	if (pg_proc_aclcheck(funcid, GetUserId(), ACL_EXECUTE) != ACLCHECK_OK)
+	{
+		const char * username;
+#if PG_VERSION_NUM >= 90500
+		username = GetUserNameFromId(GetUserId(), false);
+#else
+		username = GetUserNameFromId(GetUserId());
+#endif
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("current user %s does not have permission to call function %s",
+					 username, NameListToString(qname))));
+	}
+
+	list_free_deep(qname);
+
+	return funcid;
+}
+
+static Oid
+find_startup_hook(const char *proname)
+{
+	Oid argtypes[2];
+
+	argtypes[0] = TEXTARRAYOID;
+	argtypes[1] = TEXTOID;
+
+	return find_hook_fn(proname, argtypes, 2, VOIDOID);
+}
+
+static Oid
+find_shutdown_hook(const char *proname)
+{
+	Oid argtypes[1];
+
+	argtypes[0] = TEXTOID;
+
+	return find_hook_fn(proname, argtypes, 1, VOIDOID);
+}
+
+static Oid
+find_row_filter_hook(const char *proname)
+{
+	Oid argtypes[3];
+
+	argtypes[0] = REGCLASSOID;
+	argtypes[1] = CHAROID;
+	argtypes[2] = TEXTOID;
+
+	return find_hook_fn(proname, argtypes, 3, BOOLOID);
+}
+
+static Oid
+find_txn_filter_hook(const char *proname)
+{
+	Oid argtypes[2];
+
+	argtypes[0] = INT4OID;
+	argtypes[1] = TEXTOID;
+
+	return find_hook_fn(proname, argtypes, 2, BOOLOID);
+}
diff --git a/contrib/pglogical_output_plhooks/pglogical_output_plhooks.control b/contrib/pglogical_output_plhooks/pglogical_output_plhooks.control
new file mode 100644
index 0000000..647b9ef
--- /dev/null
+++ b/contrib/pglogical_output_plhooks/pglogical_output_plhooks.control
@@ -0,0 +1,4 @@
+comment = 'pglogical_output pl hooks'
+default_version = '1.0'
+module_pathname = '$libdir/pglogical_output_plhooks'
+relocatable = false
-- 
2.1.0

#4Craig Ringer
craig@2ndquadrant.com
In reply to: Craig Ringer (#3)
Re: pglogical - logical replication contrib module

... and this is why we don't post while jetlagged and tired.

The patch on the prior mail is the output plugin. Wrong thread, wrong
filename. It's the output plugin update needed for the pglogical downstream
in this thread.

Corrected post of v5 output plugin here:

/messages/by-id/CAMsr+YEGtE8gYnpAo7=n=iMS9OLcc8oeMvMrh+9ki9WB5CykGA@mail.gmail.com

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#5Steve Singer
steve@ssinger.info
In reply to: Petr Jelinek (#1)
1 attachment(s)
Re: pglogical - logical replication contrib module

On 12/31/2015 06:34 PM, Petr Jelinek wrote:

Hi,

I'd like to submit the replication solution which is based on the
pglogical_output [1] module (which is obviously needed for this to
compile).

Hi,

make check gives me

for extra in ../../contrib/pglogical_output contrib/pglogical; do make
-C '../..'/$extra DESTDIR='/usr/local/src/postgresql'/tmp_install
install >>'/usr/local/src/postgresql'/tmp_install/log/install.log ||
exit; done
make[1]: *** ../../../../contrib/pglogical_output: No such file or
directory. Stop.
../../src/Makefile.global:325: recipe for target 'temp-install' failed
make: *** [temp-install] Error 2
ssinger@ssinger-laptop:/usr/local/src/postgresql/contrib/pglogical$

The attached patch fixes that but it then is creating the test database
'contrib_regression' not 'regression'
changing pglogical.provider_dsn = 'contrib_regression' still leaves me
with a lot of failures.

Attachments:

makefile.difftext/x-diff; name=makefile.diffDownload
diff --git a/contrib/pglogical/Makefile b/contrib/pglogical/Makefile
new file mode 100644
index 1640f63..a4dab88
*** a/contrib/pglogical/Makefile
--- b/contrib/pglogical/Makefile
*************** include $(top_srcdir)/contrib/contrib-gl
*** 27,34 ****
  # typical installcheck users do not have (e.g. buildfarm clients).
  @installcheck: ;
  
! EXTRA_INSTALL += $(top_srcdir)/contrib/pglogical_output
! EXTRA_REGRESS_OPTS += $(top_srcdir)/contrib/pglogical/regress-postgresql.conf
  
  override CPPFLAGS := $(CPPFLAGS) -I$(top_srcdir)/contrib/pglogical_output
  
--- 27,34 ----
  # typical installcheck users do not have (e.g. buildfarm clients).
  @installcheck: ;
  
! EXTRA_INSTALL += contrib/pglogical_output
! EXTRA_REGRESS_OPTS += --temp-config=regress-postgresql.conf
  
  override CPPFLAGS := $(CPPFLAGS) -I$(top_srcdir)/contrib/pglogical_output
  
#6Steve Singer
steve@ssinger.info
In reply to: Petr Jelinek (#1)
Re: pglogical - logical replication contrib module

On 12/31/2015 06:34 PM, Petr Jelinek wrote:

Hi,

I'd like to submit the replication solution which is based on the
pglogical_output [1] module (which is obviously needed for this to
compile).

The pglogical contrib module provides extension which does the
master-slave logical replication based on the logical decoding.

The basic documentation is in README.md, I didn't bother making sgml
docs yet since I expect that there will be ongoing changes happening
and it's easier for me to update the markdown docs than sgml. I will
do the conversion once we start approaching committable state.

I am going to send my comments/issues out in batches as I find them
instead of waiting till I look over everything.

I find this part of the documentation a bit unclear

+Once the provider node is setup, subscribers can be subscribed to it. 
First the
+subscriber node must be created:
+
+    SELECT pglogical.create_node(
+        node_name := 'subscriber1',
+        dsn := 'host=thishost port=5432 dbname=db'
+    );
+

My initial reading was that I should execute this on the provider node.
Perhaps instead
-----------------
Once the provider node is setup you can then create subscriber nodes.
Create the subscriber nodes and
then execute the following commands on each subscriber node

create extension pglogical

select pglogical.create_node(node_name:='subsriberX',dsn:='host=thishost
dbname=db port=5432');

-------------------

Also the documentation for create_subscription talks about

+  - `synchronize_structure` - specifies if to synchronize structure from
+    provider to the subscriber, default true

I did the following

test2=# select pglogical.create_subscription(subscription_name:='default
sub',provider_dsn:='host=localhost dbname=test1 port=5436');
create_subscription
---------------------
247109879

Which then resulted in the following showing up in my PG log

LOG: worker process: pglogical apply 16542:247109879 (PID 4079) exited
with exit code 1
ERROR: replication slot name "pgl_test2_test1_default sub" contains
invalid character
HINT: Replication slot names may only contain lower case letters,
numbers, and the underscore character.
FATAL: could not send replication command "CREATE_REPLICATION_SLOT
"pgl_test2_test1_default sub" LOGICAL pglogical_output": status
PGRES_FATAL_ERROR: ERROR: replication slot name
"pgl_test2_test1_default sub" contains invalid character
HINT: Replication slot names may only contain lower case letters,
numbers, and the underscore character.

The create_subscription command should check if the subscription name is
valid (meets the rules that will be applied against the slot command).

I wondered how I could fix my mistake.

The docs say

+- `pglogical.pglogical_drop_subscription(subscription_name name, 
ifexists bool)`
+  Disconnects the subscription and removes it from the catalog.
+

test2=# select pglogical.pglogical_drop_subscription('default sub', true);
ERROR: function pglogical.pglogical_drop_subscription(unknown, boolean)
does not exist

The command is actually called pglogical.drop_subscription the docs
should be fixed to show the actual command name

I then wanted to add a second table to my database. ('b').

select pglogical.replication_set_add_table('default','public.b',true);
replication_set_add_table
---------------------------
t
(1 row)

In my pglog I then got

LOG: starting sync of table public.b for subscriber defaultsub
ERROR: replication slot name "pgl_test2_test1_defaultsub_public.b"
contains invalid character
HINT: Replication slot names may only contain lower case letters,
numbers, and the underscore character.
FATAL: could not send replication command "CREATE_REPLICATION_SLOT
"pgl_test2_test1_defaultsub_public.b" LOGICAL pglogical_output": status
PGRES_FATAL_ERROR: ERROR: replication slot name
"pgl_test2_test1_defaultsub_public.b" contains invalid character
HINT: Replication slot names may only contain lower case letters,
numbers, and the underscore character.

I then did

test1=# select pglogical.replication_set_remove_table('default','public.b');
replication_set_remove_table
------------------------------
t
(1 row)

but my log still keep repeating the error, so I tried connecting to the
replica and did the same

test2=# select pglogical.replication_set_remove_table('default','public.b');
ERROR: replication set mapping -303842815:16726 not found

Is there any way to recover from this situation?

The documenation says I can drop a replication set, maybe that will let
replication continue.

+- `pglogical.delete_replication_set(set_name text)`
+  Removes the replication set.
+

select pglogical.delete_replication_set('default');
ERROR: function pglogical.delete_replication_set(unknown) does not exist
LINE 1: select pglogical.delete_replication_set('default');
^
HINT: No function matches the given name and argument types. You might
need to add explicit type casts.

The function is actually pglogical.drop_replication_set , the docs
should be updated.
(note that didn't fix my problem either but then dropping the
subscription did seem to work).

I then re-added the default set to the origin and resubscribed my replica

test2=# select
pglogical.create_subscription(subscription_name:='defaultsub',provider_dsn:='host=localhost
dbname=test1 port=5436');
create_subscription
---------------------
2974019075

I then saw a bunch of
LOG: worker process: pglogical apply 16542:2974019075 (PID 26778)
exited with exit code 1
ERROR: subscriber defaultsub initialization failed during
nonrecoverable step (s), please try the setup again
LOG: worker process: pglogical apply 16542:2974019075 (PID 26779)
exited with exit code 1

in the log but then those stopped and I see

test2=# select pglogical.show_subscription_status();
show_subscription_status

--------------------------------------------------------------------------------
--------------------------------------------------
(defaultsub,down,test1,"host=localhost dbname=test1
port=5436",pgl_test2_test1_
defaultsub,"{default,default_insert_only}",{all})
(1 row)

I'm not really sure what to do to 'recover' my cluster at this point so
I'll send this off and rebuild my cluster and start over.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#7Steve Singer
steve@ssinger.info
In reply to: Steve Singer (#6)
Re: pglogical - logical replication contrib module

On 01/09/2016 01:30 PM, Steve Singer wrote:

On 12/31/2015 06:34 PM, Petr Jelinek wrote:

I'm not really sure what to do to 'recover' my cluster at this point
so I'll send this off and rebuild my cluster and start over.

I had a setup test1--->test2 (with 2 tables in the default set)

I then created a third database (all three hosted on the same PG cluster)

In the third database (test3)
test3=# create extension pglogical;
CREATE EXTENSION
test3=# select pglogical.create_node(node_name:='test3',
dsn:='host=localhost dbname=test3 port=5436');
create_node
-------------
2001662995
(1 row)

test3=# select
pglogical.create_subscription(subscription_name:='defaultsub',provider_dsn:='host=localhost
dbname=test2 port=5436');
create_subscription
---------------------
2974019075

It copied the schema over but not the data (if I use test2 as the
provider_dsn then it does copy the data).

I then tried inserting a row into a table on test1. Things crashed and
after crash recovery I keep getting

2016-01-10 13:03:15 EST LOG: database system is ready to accept connections
2016-01-10 13:03:15 EST LOG: autovacuum launcher started
2016-01-10 13:03:15 EST LOG: starting apply for subscription defaultsub
2016-01-10 13:03:15 EST LOG: starting apply for subscription defaultsub
2016-01-10 13:03:15 EST test2LOG: starting logical decoding for slot
"pgl_test3
_test2_defaultsub"
2016-01-10 13:03:15 EST test2DETAIL: streaming transactions committing
after 0/
18292D8, reading WAL from 0/18292D8
2016-01-10 13:03:15 EST test2LOG: logical decoding found consistent
point at 0/
18292D8
2016-01-10 13:03:15 EST test2DETAIL: Logical decoding will begin using
saved sn
apshot.
TRAP: FailedAssertion("!(IsTransactionState())", File: "catcache.c",
Line: 1127)
2016-01-10 13:03:15 EST test2LOG: unexpected EOF on standby connection
2016-01-10 13:03:15 EST LOG: worker process: pglogical apply
17016:2974019075 (
PID 24746) was terminated by signal 6: Aborted

The stack trace is

#3 0x00000000007b83af in SearchCatCache (cache=0xe27d18, v1=15015784,
v2=v2@entry=0, v3=v3@entry=0, v4=v4@entry=0) at catcache.c:1127
#4 0x00000000007c503e in SearchSysCache (cacheId=cacheId@entry=47,
key1=<optimized out>, key2=key2@entry=0, key3=key3@entry=0,
key4=key4@entry=0) at syscache.c:981
#5 0x00000000006996d4 in replorigin_by_name (
roname=0xe51f30 "pgl_test2_test1_defaultsub",
missing_ok=missing_ok@entry=0 '\000') at origin.c:216
#6 0x00007fdb54a908d3 in handle_origin (s=0x7ffd873f6da0)
at pglogical_apply.c:235
#7 replication_handler (s=0x7ffd873f6da0) at pglogical_apply.c:1031
#8 apply_work (streamConn=streamConn@entry=0xe84fb0) at
pglogical_apply.c:1309
#9 0x00007fdb54a911cc in pglogical_apply_main (main_arg=<optimized out>)
at pglogical_apply.c:1691
#10 0x0000000000674912 in StartBackgroundWorker () at bgworker.c:726
---Type <return> to continue, or q <return> to quit---
#11 0x000000000067f7e2 in do_start_bgworker (rw=0xe03890) at
postmaster.c:5501
#12 maybe_start_bgworker () at postmaster.c:5676
#13 0x0000000000680206 in sigusr1_handler
(postgres_signal_arg=<optimized out>)
at postmaster.c:4937
#14 <signal handler called>
#15 0x00007fdb54fa2293 in __select_nocancel ()
at ../sysdeps/unix/syscall-template.S:81
#16 0x0000000000468285 in ServerLoop () at postmaster.c:1648
#17 0x000000000068161e in PostmasterMain (argc=argc@entry=3,
argv=argv@entry=0xddede0) at postmaster.c:1292
#18 0x000000000046979d in main (argc=3, argv=0xddede0) at main.c:223

I tried dropping the subscription and re-adding it. I keep getting

2016-01-10 13:21:48 EST test1LOG: logical decoding found consistent
point at 0/1830080
2016-01-10 13:21:48 EST test1DETAIL: There are no running transactions.
2016-01-10 13:21:48 EST test1LOG: exported logical decoding snapshot:
"000004DE-1" with 0 transaction IDs
2016-01-10 13:21:48 EST test3ERROR: relation "a" already exists
2016-01-10 13:21:48 EST test3STATEMENT: CREATE TABLE a (
a integer NOT NULL,
b integer
);

pg_restore: [archiver (db)] Error while PROCESSING TOC:
pg_restore: [archiver (db)] Error from TOC entry 182; 1259 16700 TABLE a
ssinger
pg_restore: [archiver (db)] could not execute query: ERROR: relation "a"
already exists
Command was: CREATE TABLE a (
a integer NOT NULL,
b integer
);

2016-01-10 13:21:48 EST ERROR: could not execute command
"/usr/local/pgsql96gitlogical/bin/pg_restore --section="pre-data"
--exit-on-error -1 -d "host=localhost dbname=test3 port=5436"
"/tmp/pglogical-28079.dump""
2016-01-10 13:21:48 EST test1LOG: unexpected EOF on client connection
with an open transaction
2016-01-10 13:21:48 EST LOG: worker process: pglogical apply
17016:844915593 (PID 28079) exited with exit code 1
2016-01-10 13:21:48 EST ERROR: subscriber defaultsub4 initialization
failed during nonrecoverable step (s), please try the setup again

Which is probably also the cause of the error I reported yesterday (that
I tried creating a subscription without dropping the tables).
From a usability point of view I think we need a way of making this
errors available in the output of pglogical.show_subscription_status().

I asked to subscribe something through psql, even thought it is
asynchronous, if the async operation fails I should be able to learn
about the problem through psql. If I am writing a script to subscribe a
node it needs a way in my script of checking if the subscription has
failed and reporting the error.
My subscription script might not have easy access to the server log.

+- `pglogical.show_subscription_table(subscription_name name,
+  relation regclass)`
+  Shows synchronization status of a table.
+
+  Parameters:
+  - `subscription_name` - name of the existing subscription
+  - `relation` - name of existing table, optionally qualified
+

It isn't clear from the documentation what the output of this function
means, nor could I tell looking at it. Is this function just supposed
to tell us if a table is part of the replication set or if it is
'up-to-date'. It still reports 'synchornized' when a table is behind.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#8Petr Jelinek
petr@2ndquadrant.com
In reply to: Steve Singer (#6)
Re: pglogical - logical replication contrib module

On 2016-01-09 19:30, Steve Singer wrote:\

I am going to send my comments/issues out in batches as I find them
instead of waiting till I look over everything.

Thanks for looking at this! Yes going in batches/steps makes sense, this
is huge patch.

I find this part of the documentation a bit unclear

+Once the provider node is setup, subscribers can be subscribed to it.
First the
+subscriber node must be created:
+
+    SELECT pglogical.create_node(
+        node_name := 'subscriber1',
+        dsn := 'host=thishost port=5432 dbname=db'
+    );
+

My initial reading was that I should execute this on the provider node.
Perhaps instead
-----------------
Once the provider node is setup you can then create subscriber nodes.
Create the subscriber nodes and
then execute the following commands on each subscriber node

create extension pglogical

select pglogical.create_node(node_name:='subsriberX',dsn:='host=thishost
dbname=db port=5432');

-------------------

Makes sense I guess, this is probably relic of how this internally
evolved (we used to have providers and subscribers before we merged them
into nodes).

Also the documentation for create_subscription talks about

+  - `synchronize_structure` - specifies if to synchronize structure from
+    provider to the subscriber, default true

Not sure what's your comment on this.

I did the following

test2=# select pglogical.create_subscription(subscription_name:='default
sub',provider_dsn:='host=localhost dbname=test1 port=5436');
create_subscription
---------------------
247109879

Which then resulted in the following showing up in my PG log

LOG: worker process: pglogical apply 16542:247109879 (PID 4079) exited
with exit code 1
ERROR: replication slot name "pgl_test2_test1_default sub" contains
invalid character
HINT: Replication slot names may only contain lower case letters,
numbers, and the underscore character.
FATAL: could not send replication command "CREATE_REPLICATION_SLOT
"pgl_test2_test1_default sub" LOGICAL pglogical_output": status
PGRES_FATAL_ERROR: ERROR: replication slot name
"pgl_test2_test1_default sub" contains invalid character
HINT: Replication slot names may only contain lower case letters,
numbers, and the underscore character.

The create_subscription command should check if the subscription name is
valid (meets the rules that will be applied against the slot command).

Yes, fixed. Also added some other sensitization code since we also use
dbname in slot name and that can contain whatever.

I wondered how I could fix my mistake.

The docs say

+- `pglogical.pglogical_drop_subscription(subscription_name name,
ifexists bool)`
+  Disconnects the subscription and removes it from the catalog.
+

test2=# select pglogical.pglogical_drop_subscription('default sub', true);
ERROR: function pglogical.pglogical_drop_subscription(unknown, boolean)
does not exist

The command is actually called pglogical.drop_subscription the docs
should be fixed to show the actual command name

Yep, got this from other people as well, fixed.

I then wanted to add a second table to my database. ('b').

select pglogical.replication_set_add_table('default','public.b',true);
replication_set_add_table
---------------------------
t
(1 row)

In my pglog I then got

LOG: starting sync of table public.b for subscriber defaultsub
ERROR: replication slot name "pgl_test2_test1_defaultsub_public.b"
contains invalid character
HINT: Replication slot names may only contain lower case letters,
numbers, and the underscore character.
FATAL: could not send replication command "CREATE_REPLICATION_SLOT
"pgl_test2_test1_defaultsub_public.b" LOGICAL pglogical_output": status
PGRES_FATAL_ERROR: ERROR: replication slot name
"pgl_test2_test1_defaultsub_public.b" contains invalid character
HINT: Replication slot names may only contain lower case letters,
numbers, and the underscore character.

Right, needed the sensitization as well (I am actually using the hash
now as there is only 8 chars left anyway).

I then did

test1=# select
pglogical.replication_set_remove_table('default','public.b');
replication_set_remove_table
------------------------------
t
(1 row)

but my log still keep repeating the error, so I tried connecting to the
replica and did the same

test2=# select
pglogical.replication_set_remove_table('default','public.b');
ERROR: replication set mapping -303842815:16726 not found

Is there any way to recover from this situation?

Not really, there is no api yet to remove table from synchronization
process so you'd have to manually delete row from
pglogical.local_sync_status on subscriber, kill the sync process and
remove the slot. I will think about what would be good api to solve this.

The documenation says I can drop a replication set, maybe that will let
replication continue.

+- `pglogical.delete_replication_set(set_name text)`
+  Removes the replication set.
+

select pglogical.delete_replication_set('default');
ERROR: function pglogical.delete_replication_set(unknown) does not exist
LINE 1: select pglogical.delete_replication_set('default');
^
HINT: No function matches the given name and argument types. You might
need to add explicit type casts.

The function is actually pglogical.drop_replication_set , the docs
should be updated.
(note that didn't fix my problem either but then dropping the
subscription did seem to work).

Yeah it doesn't as the problem is on subscriber, replication sets only
affect provider. And fixed the docs.

I then re-added the default set to the origin and resubscribed my replica

test2=# select
pglogical.create_subscription(subscription_name:='defaultsub',provider_dsn:='host=localhost
dbname=test1 port=5436');
create_subscription
---------------------
2974019075

I then saw a bunch of
LOG: worker process: pglogical apply 16542:2974019075 (PID 26778)
exited with exit code 1
ERROR: subscriber defaultsub initialization failed during
nonrecoverable step (s), please try the setup again
LOG: worker process: pglogical apply 16542:2974019075 (PID 26779)
exited with exit code 1

in the log but then those stopped and I see

test2=# select pglogical.show_subscription_status();
show_subscription_status

--------------------------------------------------------------------------------

--------------------------------------------------
(defaultsub,down,test1,"host=localhost dbname=test1
port=5436",pgl_test2_test1_
defaultsub,"{default,default_insert_only}",{all})
(1 row)

I'm not really sure what to do to 'recover' my cluster at this point so
I'll send this off and rebuild my cluster and start over.

I think the problem here is that you resubscribed with
syncrhonize_structure := true while the conflicting structure already
existed, that option only works correctly when there is no conflicting
structure (we don't try to make diffs or anything, just dump/restore).
Recovering should be drop the uninitialized subscription and create new
one where you don't synchronize structure.

--
Petr Jelinek http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#9Petr Jelinek
petr@2ndquadrant.com
In reply to: Steve Singer (#7)
Re: pglogical - logical replication contrib module

On 2016-01-10 20:57, Steve Singer wrote:

On 01/09/2016 01:30 PM, Steve Singer wrote:

On 12/31/2015 06:34 PM, Petr Jelinek wrote:

I'm not really sure what to do to 'recover' my cluster at this point
so I'll send this off and rebuild my cluster and start over.

I had a setup test1--->test2 (with 2 tables in the default set)

I then created a third database (all three hosted on the same PG cluster)

In the third database (test3)
test3=# create extension pglogical;
CREATE EXTENSION
test3=# select pglogical.create_node(node_name:='test3',
dsn:='host=localhost dbname=test3 port=5436');
create_node
-------------
2001662995
(1 row)

test3=# select
pglogical.create_subscription(subscription_name:='defaultsub',provider_dsn:='host=localhost
dbname=test2 port=5436');
create_subscription
---------------------
2974019075

It copied the schema over but not the data (if I use test2 as the
provider_dsn then it does copy the data).

Yes, because you probably don't have any replication sets defined there.
That's by design, replication sets are defined per provider and their
definition is not replicated. This seems to be the only sane way to
actually support merging data from multiple provider nodes. I guess this
could be documented better, but cascading is something that's still WIP.

I then tried inserting a row into a table on test1. Things crashed and
after crash recovery I keep getting

2016-01-10 13:03:15 EST LOG: database system is ready to accept
connections
2016-01-10 13:03:15 EST LOG: autovacuum launcher started
2016-01-10 13:03:15 EST LOG: starting apply for subscription defaultsub
2016-01-10 13:03:15 EST LOG: starting apply for subscription defaultsub
2016-01-10 13:03:15 EST test2LOG: starting logical decoding for slot
"pgl_test3
_test2_defaultsub"
2016-01-10 13:03:15 EST test2DETAIL: streaming transactions committing
after 0/
18292D8, reading WAL from 0/18292D8
2016-01-10 13:03:15 EST test2LO
I asked to subscribe something through psql, even thought it is
asynchronous, if the async operation fails I should be able to learn
about the problem through psql. If I am writing a script to subscribe a
node it needs a way in my script of checking if the subscription has
failed and reporting the error.
My subscription script might not have easy access to the server log.
G: logical decoding found consistent
point at 0/
18292D8
2016-01-10 13:03:15 EST test2DETAIL: Logical decoding will begin using
saved sn
apshot.
TRAP: FailedAssertion("!(IsTransactionState())", File: "catcache.c",
Line: 1127)
2016-01-10 13:03:15 EST test2LOG: unexpected EOF on standby connection
2016-01-10 13:03:15 EST LOG: worker process: pglogical apply
17016:2974019075 (
PID 24746) was terminated by signal 6: Aborted

The stack trace is

#3 0x00000000007b83af in SearchCatCache (cache=0xe27d18, v1=15015784,
v2=v2@entry=0, v3=v3@entry=0, v4=v4@entry=0) at catcache.c:1127
#4 0x00000000007c503e in SearchSysCache (cacheId=cacheId@entry=47,
key1=<optimized out>, key2=key2@entry=0, key3=key3@entry=0,
key4=key4@entry=0) at syscache.c:981
#5 0x00000000006996d4 in replorigin_by_name (
roname=0xe51f30 "pgl_test2_test1_defaultsub",
missing_ok=missing_ok@entry=0 '\000') at origin.c:216
#6 0x00007fdb54a908d3 in handle_origin (s=0x7ffd873f6da0)
at pglogical_apply.c:235
#7 replication_handler (s=0x7ffd873f6da0) at pglogical_apply.c:1031
#8 apply_work (streamConn=streamConn@entry=0xe84fb0) at
pglogical_apply.c:1309
#9 0x00007fdb54a911cc in pglogical_apply_main (main_arg=<optimized out>)
at pglogical_apply.c:1691
#10 0x0000000000674912 in StartBackgroundWorker () at bgworker.c:726
---Type <return> to continue, or q <return> to quit---
#11 0x000000000067f7e2 in do_start_bgworker (rw=0xe03890) at
postmaster.c:5501
#12 maybe_start_bgworker () at postmaster.c:5676
#13 0x0000000000680206 in sigusr1_handler
(postgres_signal_arg=<optimized out>)
at postmaster.c:4937
#14 <signal handler called>
#15 0x00007fdb54fa2293 in __select_nocancel ()
at ../sysdeps/unix/syscall-template.S:81
#16 0x0000000000468285 in ServerLoop () at postmaster.c:1648
#17 0x000000000068161e in PostmasterMain (argc=argc@entry=3,
argv=argv@entry=0xddede0) at postmaster.c:1292
#18 0x000000000046979d in main (argc=3, argv=0xddede0) at main.c:223

That's bug, fixed.

I tried dropping the subscription and re-adding it. I keep getting

2016-01-10 13:21:48 EST test1LOG: logical decoding found consistent
point at 0/1830080
2016-01-10 13:21:48 EST test1DETAIL: There are no running transactions.
2016-01-10 13:21:48 EST test1LOG: exported logical decoding snapshot:
"000004DE-1" with 0 transaction IDs
2016-01-10 13:21:48 EST test3ERROR: relation "a" already exists
2016-01-10 13:21:48 EST test3STATEMENT: CREATE TABLE a (
a integer NOT NULL,
b integer
);

pg_restore: [archiver (db)] Error while PROCESSING TOC:
pg_restore: [archiver (db)] Error from TOC entry 182; 1259 16700 TABLE a
ssinger
pg_restore: [archiver (db)] could not execute query: ERROR: relation "a"
already exists
Command was: CREATE TABLE a (
a integer NOT NULL,
b integer
);

2016-01-10 13:21:48 EST ERROR: could not execute command
"/usr/local/pgsql96gitlogical/bin/pg_restore --section="pre-data"
--exit-on-error -1 -d "host=localhost dbname=test3 port=5436"
"/tmp/pglogical-28079.dump""
2016-01-10 13:21:48 EST test1LOG: unexpected EOF on client connection
with an open transaction
2016-01-10 13:21:48 EST LOG: worker process: pglogical apply
17016:844915593 (PID 28079) exited with exit code 1
2016-01-10 13:21:48 EST ERROR: subscriber defaultsub4 initialization
failed during nonrecoverable step (s), please try the setup again

Which is probably also the cause of the error I reported yesterday (that
I tried creating a subscription without dropping the tables).
From a usability point of view I think we need a way of making this
errors available in the output of pglogical.show_subscription_status().

Yes it is same reason I explained in previous email, can be solved with
synchronize_structure := false in the create_subscription.

And yes the show_subscription_status should show the last error. I
didn't find good way to do that yet as some errors result in being
unable to write to database anymore so this needs to be done either via
libpq connection which is ugly or via shmem communication with another
process (the manager process seems like good candidate for this), but we
don't yet have infrastructure in pglogical to do this. This is
definitely on my TODO.

+- `pglogical.show_subscription_table(subscription_name name,
+  relation regclass)`
+  Shows synchronization status of a table.
+
+  Parameters:
+  - `subscription_name` - name of the existing subscription
+  - `relation` - name of existing table, optionally qualified
+

It isn't clear from the documentation what the output of this function
means, nor could I tell looking at it. Is this function just supposed
to tell us if a table is part of the replication set or if it is
'up-to-date'. It still reports 'synchornized' when a table is behind.

There are several statuses the table goes through, during the COPY it's
in synchronizing status, so next logical step seemed to be synchronized.
Maybe it should be renamed to 'replicating' instead as that's what it
actually means (table has finished synchronization and is now
replicating normally).

--
Petr Jelinek http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#10Steve Singer
steve@ssinger.info
In reply to: Petr Jelinek (#9)
Re: pglogical - logical replication contrib module

On 01/15/2016 12:07 PM, Petr Jelinek wrote:

That's bug, fixed.

Can you posted an updated patch with whatever fixes you have so far made?

There are several statuses the table goes through, during the COPY
it's in synchronizing status, so next logical step seemed to be
synchronized. Maybe it should be renamed to 'replicating' instead as
that's what it actually means (table has finished synchronization and
is now replicating normally).

I agree 'replicating' is clearer

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#11leo
dazhoufei@gmail.com
In reply to: Petr Jelinek (#9)
Re: pglogical - logical replication contrib module

I also run into same problem and waiting for bug fix.
please update if new patch has published.

THX

--
View this message in context: http://postgresql.nabble.com/pglogical-logical-replication-contrib-module-tp5879755p5882564.html
Sent from the PostgreSQL - hackers mailing list archive at Nabble.com.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#12Craig Ringer
craig@2ndquadrant.com
In reply to: leo (#11)
Re: pglogical - logical replication contrib module

On 17 January 2016 at 14:46, leo <dazhoufei@gmail.com> wrote:

I also run into same problem and waiting for bug fix.
please update if new patch has published.

There's a point release coming soon that'll incorporate these fixes and a
number of others. It'll be posted here in a few days.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#13Steve Singer
steve@ssinger.info
In reply to: Craig Ringer (#12)
Re: pglogical - logical replication contrib module

The following review has been posted through the commitfest application:
make installcheck-world: not tested
Implements feature: not tested
Spec compliant: not tested
Documentation: not tested

This reply will covers a 10,000 foot level review of the feature (some of my other replies to the thread cover specifics that came up in testing and code level review will come later)

1) Do we want logical replication in core/contrib

10 year ago a popular feeling in the postgresql project was that replication didn't belong in core because there were too many different styles. People then went on to complain that it were too many replication projects to choose from and that they were hard to use and had lots of corner cases. The evolution of WAL based replication showed us how popular in-core replication is. Users like being able to use in-core features and our community process tends to produce better quality in-core solutions than external projects.

I am of the opinion that if we can come up with a solution that meets some common use cases then it would be good to have those features in core/contrib. At this stage I am not going to get into a discussion of a contrib extension versus built in as not an extension. I don't think a single replication solution
will ever meet all use-cases. I feel that the extensible infrastructure we have so far built for logical replication means that people who want to develop solutions for use-cases not covered will be in a good position. This doesn't mean we can't or shouldn't try to cover some use cases in core.

2) Does this patch provide a set of logical replication features that meet many popular use-cases

Below I will review some use-cases and try to assess how pglogical meets them.

** Streaming Postgresql Upgrade

pg_upgrade is great for many situations but sometimes you don't want an in place upgrade but you want a streaming upgrade. Possibly because you don't want application downtime but instead you just want to point your applications at the upgraded database server in a controlled manner. Othertimes you
might want an option of upgrading to a newer version of PG but maintain the option of having to rollback to the older version if things go badly.

I think pglogical should be able to handle this use case pretty well (assuming the source version of PG is actually new enough to include pglogical).
Support for replicating sequences would need to be added before this is as smooth but once sequence support was added I think this would work well.
I also don't see any reason why you couldn't replicate from 9.7 -> 9.6 thought since the wire format is abstracted from the internal representation. This is of course dependent not the application not doing anything that is inherently in-compatible between the two versions

** Query only replicas (with temp tables or additional indexes)

Sometimes you want a replica for long running or heavy queries. Requirements for temp tables, additional indexes or maybe the effect on vacuum means that our existing WAL based replicas are unsuitable.

I think pglogical should be able to handle this use case pretty well with the caveat being that your replica is an asynchronous replica and will always lag
the origin by some amount.

** Replicating a subset of tables into a different database

Sometimes you wan to replicate a handful of tables from one database to another database. Maybe the first database is the system of record for the data and the second database needs an up to date copy for querying.

Pglogical should meet this use case pretty well, it has flexible support for selecting which tables get replicated from which source. Pglogical doesn't have any facilities to rename the tables between the origin and replica but they could be added later.

** Sharding

Systems that do application level sharding (or even sharding with a fdw) often have non-sharded tables that need to be available on all shards for relational integrity or joins. Logical replication is one way to make sure that the replicated data gets to all the shards. Sharding systems also sometimes want
to take the data from individual shards and replicate it to a consolidation server for reporting purposes.

Pglogical seems to meet this use case, I guess you would have a designated origin for the shared data/global data that all shards would subscribe to
with a set containing the designated data. For the consolidation use case you would have the consolidation server subscribe to all shards

I am less clear about how someone would want DDL changes to work for these cases. The DDL support in the patch is pretty limited so I am not going to think much now about how we would want DDL to work.

** Schema changes involving rewriting big tables

Sometimes you have a DDL change on a large table that will involve a table rewrite and the best way of deploying the change is to make the DDL change
on a replicate then once it is finished promote the replica to the origin in some controlled fashion. This avoids having to lock the table on the origin
for hours.

pglogical seems to allow minor schema changes on the replica such as changing a type but it doesn't seem to allow a DO INSTEAD trigger on the replica. I don't think pglogical currently meets this use case particularly well

** Failover

WAL replication is probably a better choice for someone just looking for failover support from replication. Someone who is looking at pglogical for failover related use cases probably has one or more of the other uses cases I mentioned and wants a logical node to
take over for a failed origin. If a node fails you can take some of the remaining subscribers and have them resubscribe to one of the remaining nodes but there is no support for a) Figuring out which of the remaining nodes is most ahead b) Letting the subscribers figure out which updates from the old origin that are missing and getting them from a surviving node (they can truncate and re-copy the data but that might be very expensive)

I am not sure what would be involved in taking a streaming WAL replica and have it stand it replace the failed node.

Lack of replicating sequences would also make failing over to a pglogical replica awkward.

** Geographically distributed applications

Sometimes people have database in different geographical locations and they want to perform queries and writes locally but replicate all the data to all the other locations. This is a multi-master eventually consistent use case.

The lack of sequence support would be an issue for these use cases. I think you could also only configure the cluster in a fully connected grid (with forward_origins='none'). A lot of deployments you would want some amount of cascading and structure which isn't yet supported. I also suspect that managing a grid cluster with more than a handful of nodes will be unwieldy (particularly compared to some of the eventual consistent nosql alternatives)

The features BDR has that were removed for pglogical are probably really useful for this use-case (which I think was the original BDR use-case)

** Scaling across multiple machines

Sometimes people ask for replication systems that let them support more load than a single database server supports but with consistency.
Other use-case applies if you want 'eventually consistent' this use case is for situations where you want something other than eventual consistent.

I don't think pglogical is intended to address this.

3) Do we like the design of pglogical

I like the fact that background workers are used instead of external daemon processes
I like the fact that you can configure almost everything through SQL
I like that the output plugin is separate because this patch is big enough as it is and I can see uses for it other than pglogical.

The core abstractions are
* Nodes-, every database in the pglogical cluster is a node
* Sets - A collection of tables that behave similarly
* Subscriptions - A link between a provider and a replica. There can only be one subscription between a provider and replica

Metadata is not transferred between nodes. What I mean by this is that nodes don't have a global view of the cluster they know about their own subscriptions but nothing else. This is different than a system like slony where sl_node and sl_subscription contain a global view of your cluster state. Not sending metadata to all nodes in the cluster simplifies a bunch of things (you don't have to worry about sending metadata around and if a given piece of metadata is stale) but the downside is that I think the tooling to perform a lot of cluster reconfigure operations will need to be a lot smarter.

Petr, and Craig have you thought about how you might support getting the cluster back into a sane state after a node fails with minimal pain.
A lot of the reason why slony needs all this metadata is to support that kind of thing. I don't think we need this for the first version but it would be nice to know that the design could accommodate such a thing.

4) Do we like the syntax

I think the big debate about syntax is do we want functions or pure SQL (ie CREATE SUBSCRIPTION default1 provider_dsn=...). If we want this as an extension
then it needs to be functions. I support this decision I think the benefits of keeping pglogical as an extension is the right tradeoff. In a few releases we can always add SQL syntax if we want it to no longer be an extension

If I have any comments have on the names or arguments of individual functions I will send them later.

5) Is the maintenance burden of this patch too high

The contrib module is big, significantly bigger than most (all?) of the other contrib modules and that doesn't include the output plugin. I see a lot of potential use-cases that I think pglogical can (or will eventually) be able to handle and I think that justifies the maintenance burden. If others disagree they should speak up.

I am concerned about testing, I don't think the .sql based regression tests are going to adequately test a replication system that supports concurrent activity on different databases/servers. I remember hearing talk about a python based test suite that was rejected in another thread. Having perl tests that use -DBI has also been rejected.

The other day Tom made this comment as part of the 'Releasing in September' thread:

---
I do not think we should necessarily try to include every testing tool
in the core distribution. What is important is that they be readily
available: easy to find, easy to use, documented, portable.
-----

The standard make contrib check tests need to do some testing of pglogical but I think it will require testing much more thorough than what we can get with in core test tooling. I think this is a good case to look at test tooling that doesn't live in core.

Overall I am very impressed with pglogical and see a lot of potential
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#14leo
dazhoufei@gmail.com
In reply to: Steve Singer (#13)
Re: pglogical - logical replication contrib module

Hi Steve Singer,

I find the pglogical package has updated, I reinstall the new RPM package
and test again. But I find the same error in subscription node after I run
pglogical.create_subscription command:
Error message:
< 2016-01-26 12:23:59.642 UTC >LOG: worker process: pglogical apply
19828:2377587811 (PID 12299) exited with exit code 1
< 2016-01-26 12:23:59.642 UTC >LOG: unregistering background worker
"pglogical apply 19828:2377587811"
< 2016-01-26 12:23:59.642 UTC >LOG: registering background worker
"pglogical apply 19828:2377587811"
< 2016-01-26 12:23:59.642 UTC >LOG: starting background worker process
"pglogical apply 19828:2377587811"
< 2016-01-26 12:23:59.643 UTC >ERROR: subscriber replicate_gis_data_from_11
initialization failed during nonrecoverable step (s), please try the setup
again
I also find the provide node has error message:
< 2016-01-26 04:16:51.173 UTC >LOG: exported logical decoding
snapshot: "0003F483-1" with 0 transaction IDs
< 2016-01-26 04:16:51.282 UTC >LOG: unexpected EOF on client connection
with an open transaction
< 2016-01-26 04:16:51.549 UTC >LOG: logical decoding found consistent point
at 4F/8CD1A090
< 2016-01-26 04:16:51.549 UTC >DETAIL: There are no running transactions.
< 2016-01-26 04:16:51.549 UTC >LOG: exported logical decoding snapshot:
"0003F484-1" with 0 transaction IDs
< 2016-01-26 04:16:51.675 UTC >LOG: unexpected EOF on client connection
with an open transaction
< 2016-01-26 04:16:51.968 UTC >LOG: logical decoding found consistent point
at 4F/8CD1A0F8
< 2016-01-26 04:16:51.968 UTC >DETAIL: There are no running transactions.
< 2016-01-26 04:16:51.968 UTC >LOG: exported logical decoding snapshot:
"0003F485-1" with 0 transaction IDs
< 2016-01-26 04:16:52.399 UTC >ERROR: schema "topology" already exists
< 2016-01-26 04:16:52.436 UTC >LOG: unexpected EOF on client connection
with an open transaction

I test pglogical according to README document. Could you tell me what
is wrong?

Thanks,
Leo

--
View this message in context: http://postgresql.nabble.com/pglogical-logical-replication-contrib-module-tp5879755p5884242.html
Sent from the PostgreSQL - hackers mailing list archive at Nabble.com.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#15Craig Ringer
craig@2ndquadrant.com
In reply to: leo (#14)
Re: pglogical - logical replication contrib module

On 26 January 2016 at 20:33, leo <dazhoufei@gmail.com> wrote:

Hi Steve Singer,

I find the pglogical package has updated, I reinstall the new RPM
package
and test again. But I find the same error in subscription node after I run
pglogical.create_subscription command:

Please don't side-track threads about patch review and development with
requests for support of a released version.

This thread is about getting pglogical into PostgreSQL 9.6 core, rather
than the separately released product that's been released to support 9.4
and 9.5.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#16Craig Ringer
craig@2ndquadrant.com
In reply to: Steve Singer (#13)
Re: pglogical - logical replication contrib module

On 23 January 2016 at 11:17, Steve Singer <steve@ssinger.info> wrote:

2) Does this patch provide a set of logical replication features that meet
many popular use-cases

Below I will review some use-cases and try to assess how pglogical meets
them.

** Streaming Postgresql Upgrade

pg_upgrade is great for many situations but sometimes you don't want an in
place upgrade but you want a streaming upgrade. Possibly because you don't
want application downtime but instead you just want to point your
applications at the upgraded database server in a controlled manner.
Othertimes you
might want an option of upgrading to a newer version of PG but maintain
the option of having to rollback to the older version if things go badly.

I think pglogical should be able to handle this use case pretty well
(assuming the source version of PG is actually new enough to include
pglogical).

Yep, it's designed significantly for that case. That's also why support
for 9.4 and 9.5 is maintained as a standalone extension, so you can get
data out of 9.4 and 9.5 easily (and for that matter, upgrade 9.4 to 9.5).

Support for replicating sequences would need to be added before this is as
smooth but once sequence support was added I think this would work well.

This will unfortunately have to be 9.6 only. We can work around it with
some limitations in a pglogical downstream in older versions, but I really
want to get time to write a v2 of the sequence decoding patch so I can get
that into 9.6.

** Query only replicas (with temp tables or additional indexes)

Sometimes you want a replica for long running or heavy queries.
Requirements for temp tables, additional indexes or maybe the effect on
vacuum means that our existing WAL based replicas are unsuitable.

I think pglogical should be able to handle this use case pretty well with
the caveat being that your replica is an asynchronous replica and will
always lag the origin by some amount.

You can actually run it as a synchronous replica too, with the usual
limitations that you can have only one synchronous standby at a time, etc.
Or should be able to - I haven't had a chance to write proper tests for
sync rep using pglogical yet.

Performance will currently hurt if you do big xacts. That's why we need
interleaved xact streaming support down the track.

Pglogical doesn't have any facilities to rename the tables between the
origin and replica but they could be added later.

Yep, we could do that with a hook. You couldn't use initial schema sync if
you did that, of course.

** Sharding

Systems that do application level sharding (or even sharding with a fdw)
often have non-sharded tables that need to be available on all shards for
relational integrity or joins. Logical replication is one way to make
sure that the replicated data gets to all the shards. Sharding systems
also sometimes want
to take the data from individual shards and replicate it to a
consolidation server for reporting purposes.

Pglogical seems to meet this use case, I guess you would have a designated
origin for the shared data/global data that all shards would subscribe to
with a set containing the designated data. For the consolidation use case
you would have the consolidation server subscribe to all shards

I am less clear about how someone would want DDL changes to work for these
cases. The DDL support in the patch is pretty limited so I am not going to
think much now about how we would want DDL to work.

DDL support is "version 2" material, basically.

9.5 has hooks that allow DDL deparsing to be implemented as an extension.
That extension needs to be finished off (there's some work-in-progress code
floating around from 9.5 dev) and needs to expose an API for other
extensions. Then pglogical can register hooks with the ddl deparse
extension and use that for DDL replication.

As we learned with BDR, though, DDL replication is *hard*.

For one thing PostgreSQL has global objects like users that we can't
currently capture DDL for, and then creates db-local objects that have
dependences on them. So you have to manually replicate the global objects
still. I can see some possible solutions for this, but nothing's really on
the horizon.

Additionally there a some operations that are a bit problematic for logical
replication. Full table rewrites being the main one - they clobber
replication origin information among other issues. We really need a way to
decode

ALTER TABLE blah ADD COLUMN fred integer NOT NULL DEFAULT 42;

as

BEGIN;
ALTER TABLE blah ADD COLUMN fred integer;
ALTER TABLE blah ALTER COLUMN fred DEFAULT 42;
UPDATE blah SET fred = 42;
ALTER TABLE blah ALTER COLUMN fred NOT NULL;
COMMIT;

which involves some "interesting" co-operation between DDL deparse and
logical replication. The mapping of the decoded full table rewrite to the
underlying table is a bit interesting; we just get a decode stream for a
synthetic table named "pg_temp_xxxx" where the xxxx is the table upstream
oid. A nicer API for that would be good.

** Schema changes involving rewriting big tables

Sometimes you have a DDL change on a large table that will involve a table
rewrite and the best way of deploying the change is to make the DDL change
on a replicate then once it is finished promote the replica to the origin
in some controlled fashion. This avoids having to lock the table on the
origin for hours.

pglogical seems to allow minor schema changes on the replica such as
changing a type but it doesn't seem to allow a DO INSTEAD trigger on the
replica. I don't think pglogical currently meets this use case
particularly well

I'm not sure I fully understand that one.

** Failover

WAL replication is probably a better choice for someone just looking for
failover support from replication.

"Physical" replication as I've been trying to call it, since logical rep is
also WAL based.

I agree with you. It very definitely is.

I have a roadmap in mind for logical rep based failover. We need sequence
advance replication (or even better, sequence access mehods), an
upstream<->downstream LSN mapping and failover slots and logical decoding
of logical slots. A few bits and pieces.

Someone who is looking at pglogical for failover related use cases probably

has one or more of the other uses cases I mentioned and wants a logical
node to take over for a failed origin. If a node fails you can take some
of the remaining subscribers and have them resubscribe to one of the
remaining nodes but there is no support for a) Figuring out which of the
remaining nodes is most ahead b) Letting the subscribers figure out which
updates from the old origin that are missing and getting them from a
surviving node (they can truncate and re-copy the data but that might be
very expensive)

Yep. Failover slots are part of that picture, and the logical decoding of
slot positions + lsn map stuff carries on from it.

** Geographically distributed applications

Sometimes people have database in different geographical locations and
they want to perform queries and writes locally but replicate all the data
to all the other locations. This is a multi-master eventually consistent
use case.

Yep. That's what BDR aims for, and why the plan in 2ndQ is to rebuild BDR
around pglogical to continue the work of streaming BDR into core. You can
think of pglogical and pglogical_output as _parts of BDR_ that have been
extracted to submit into core, they've just been heavily polished up, made
much more general purpose, and had things that won't work in core yet
removed.

Hopefully we'll have full MM on top in time, but that can't all be done in
one release.

The lack of sequence support would be an issue for these use cases.

That's why we need sequence access methods. There's a patch for that in the
9.6 CF too.

I think you could also only configure the cluster in a fully connected
grid (with forward_origins='none'). A lot of deployments you would want
some amount of cascading and structure which isn't yet supported. I also
suspect that managing a grid cluster with more than a handful of nodes will
be unwieldy (particularly compared to some of the eventual consistent nosql
alternatives)

I envision a management layer on top for that, where pglogical forms an
underlying component.

The features BDR has that were removed for pglogical are probably really

useful for this use-case (which I think was the original BDR use-case)

Yep. They were removed mainly because they can't work with core until some
other patches get in too. Also just to keep the first pglogical submission
vaguely practical and manageable.

** Scaling across multiple machines

Sometimes people ask for replication systems that let them support more
load than a single database server supports but with consistency. Other
use-case applies if you want 'eventually consistent' this use case is for
situations where you want something other than eventual consistent.

I don't think pglogical is intended to address this.

Correct. That's more like postgres-XL, where you have a distributed lock
manager, distributed transaction manager, etc.

pglogical (or the output plugin at least) can form part of such a solution,
and there's an experiment being contemplated right now to use pglogical as
the data replication transport in postgres-XL. But it doesn't attempt to
provide a whole solution there, only one component.

Metadata is not transferred between nodes. What I mean by this is that
nodes don't have a global view of the cluster they know about their own
subscriptions but nothing else. This is different than a system like slony
where sl_node and sl_subscription contain a global view of your cluster
state. Not sending metadata to all nodes in the cluster simplifies a bunch
of things (you don't have to worry about sending metadata around and if a
given piece of metadata is stale) but the downside is that I think the
tooling to perform a lot of cluster reconfigure operations will need to be
a lot smarter.

Yep. We're going to need a management layer on top for building and
monitoring non-trivial node graphs. Whether in core or not.

Petr and I found that trying to design a schema that could fit all use
cases while preserving a system-wide view of the node graph was
impractical, if not outright impossible. There are quite conflicting use
cases: mesh multi-master wants to see everything, whereas if you have three
upstreams feeding into a data aggregator that then replicates to other
nodes you don't particularly want the leaf nodes worrying about the
upstream origin servers.

Petr, and Craig have you thought about how you might support getting the
cluster back into a sane state after a node fails with minimal pain.

Yes.

There are really two approaches. One is having a physical standby where you
fail over to a streaming physical replica and your slot state on logical
slots is preserved. For that we need failover slots (per the patch to 9.6).

The other is to use logical failover, where there's a logical replica that
you can switch leaf nodes to point to. For that we need a way to record
slot advances on one node, send them on the wire and interpret them
usefully on another node. Hence the outlined support for logical decoding
of logical slot create/drop/update, and a lsn map. I haven't thought as
hard about this one yet.

There's another thing you need for multimaster/mesh systems where there's a
graph not a simple tree. That's the ability to lazily advance a slot so
that when a node fails you can find the peer that replayed the furthest in
that node's history and ask it to send you the changes from the lost node.
You have to be able to go back in time on the slot to the most recent point
you have a local copy of the other node's state. Turns out that's not hard,
you just delay advancing the slot. You also have to be able to replay it
again with a filter that sends you only that node's changes. That's also
not hard using replication origins. There are some hairy complexities when
it comes to multi-master conflict resolution though, where changes to data
come from more than one node. That's a "for later" problem.

I am concerned about testing, I don't think the .sql based regression
tests are going to adequately test a replication system that supports
concurrent activity on different databases/servers.

I agree that we can't rely only on that.

This is part of a bigger picture in Pg where we just don't test multi-node
stuff. Failover, replication, etc is ignored in the tests. The TAP based
stuff looks to change that and I suspect we'd have to investigate whether
it's possible to build on top of that for more comprehensive testing.

Thanks again for the review work, I know it takes serious time and effort
and I appreciate it.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#17Joshua D. Drake
jd@commandprompt.com
In reply to: Petr Jelinek (#1)
Re: pglogical - logical replication contrib module

On 12/31/2015 03:34 PM, Petr Jelinek wrote:

Hi,

I'd like to submit the replication solution which is based on the
pglogical_output [1] module (which is obviously needed for this to
compile).

This is fantastic! However, history presents itself here and PostgreSQL
in the past has not "blessed" a single solution for Replication.
Obviously that changed a bit with streaming replication but this is a
bit different than that. As I understand it, PgLogical is Logical
Replication (similar to Slony and Londiste). I wouldn't be surprised
(although I don't know) if Slony were to start using some of the
pglogical_output module features in the future.

If we were to accept PgLogical into core, it will become the default
blessed solution for PostgreSQL. While that is great in some ways it is
a different direction than the project has taken in the past. Is this
what we want to do?

Sincerely,

Joshua D. Drake

--
Command Prompt, Inc. http://the.postgres.company/
+1-503-667-4564
PostgreSQL Centered full stack support, consulting and development.

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#18Steve Singer
steve@ssinger.info
In reply to: Steve Singer (#13)
Re: pglogical - logical replication contrib module

The following review has been posted through the commitfest application:
make installcheck-world: tested, failed
Implements feature: tested, failed
Spec compliant: not tested
Documentation: tested, failed

Here is some more review

+- `pglogical.replication_set_add_table(set_name name, table_name regclass, synchronize boolean)`
+  Adds a table to replication set.
+
+  Parameters:
+  - `set_name` - name of the existing replication set
+  - `table_name` - name or OID of the table to be added to the set
+  - `synchronize` - if true, the table data is synchronized on all subscribers
+    which are subscribed to given replication set, default false
+

The argument to this function is actually named "relation" not "table_name" though we might want to update the function to name the argument table_name.

Also we don't explain what 'synchronize' means I first thought that a value of false would mean that existing data won't be copied but any new changes will be.
A value of false actually seems to mean that nothing will happen with the table until the synchronize function is manually called. We seem to be using the word 'synchronize' in different sense in different places I find it confusing (ie synchronize_data and syncronize_structure in create_subscription).

*** a/contrib/pglogical/pglogical_sync.c
--- b/contrib/pglogical/pglogical_sync.c
+ static void
+ dump_structure(PGLogicalSubscription *sub, const char *snapshot)
+ {
+   char        pg_dump[MAXPGPATH];
+   uint32      version;
+   int         res;
+   StringInfoData  command;
+
+   if (find_other_exec_version(my_exec_path, PGDUMP_BINARY, &version, pg_dump))
+       elog(ERROR, "pglogical subscriber init failed to find pg_dump relative to binary %s",
+            my_exec_path);
+
+   if (version / 100 != PG_VERSION_NUM / 100)
+       elog(ERROR, "pglogical subscriber init found pg_dump with wrong major version %d.%d, expected %d.%d",
+            version / 100 / 100, version / 100 % 100,
+            PG_VERSION_NUM / 100 / 100, PG_VERSION_NUM / 100 % 100);
+
+   initStringInfo(&command);
+ #if PG_VERSION_NUM < 90500
+   appendStringInfo(&command, "%s --snapshot=\"%s\" -s -N %s -N pglogical_origin -F c -f \"/tmp/pglogical-%d.dump\" \"%s\"",
+ #else
+   appendStringInfo(&command, "%s --snapshot=\"%s\" -s -N %s -F c -f \"/tmp/pglogical-%d.dump\" \"%s\"",

1) I am not sure we can assume/require that the pg_dump binary be in the same location as the postgres binary. I don't know think we've ever required that client binaries (ie psql, pg_dump, pg_restore ...) be in the same directory as postgres. pg_upgrade does require this so maybe this isn't a problem in practice but I thought I'd point it out. Ideally wouldn't need to call an external program to get a schema dump but turning pg_dump into a library is beyond the scope of this patch.

2) I don't think we can hard-coded /tmp as the directory for the schema dump. I don't think will work on most windows systems and even on a unix system $TMPDIR might be set to something else. Maybe writing this into pgsql_tmp would be a better choice.

Furtherdown in
pglogical_sync_subscription(PGLogicalSubscription *sub)
+   switch (status)
+   {
+       /* Already synced, nothing to do except cleanup. */
+       case SYNC_STATUS_READY:
+           MemoryContextDelete(myctx);
+           return;
+       /* We can recover from crashes during these. */
+       case SYNC_STATUS_INIT:
+       case SYNC_STATUS_CATCHUP:
+           break;
+       default:
+           elog(ERROR,
+                "subscriber %s initialization failed during nonrecoverable step (%c), please try the setup again",
+                sub->name, status);
+           break;
+   }

I think the default case needs to do something to unregister the background worker. We already discussed trying to get the error message to a user in a better way either way there isn't any sense in this background worker being launched again if the error is nonrecoverable.

+
+               tables = copy_replication_sets_data(sub->origin_if->dsn,
+                                                   sub->target_if->dsn,
+                                                   snapshot,
+                                                   sub->replication_sets);
+
+               /* Store info about all the synchronized tables. */
+               StartTransactionCommand();
+               foreach (lc, tables)

Shouldn't we be storing the info about the synchronized tables as part of the same transaction that does the sync?

I'll keeping going through the code as I have time. I think it is appropriate to move this to the next CF since the CF is past the end date and the patch has received some review. When you have an updated version of the patch post it, don't wait until March.
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#19Steve Singer
steve@ssinger.info
In reply to: Craig Ringer (#16)
Re: pglogical - logical replication contrib module

On 01/26/2016 10:43 AM, Craig Ringer wrote:

On 23 January 2016 at 11:17, Steve Singer <steve@ssinger.info
<mailto:steve@ssinger.info>> wrote:

** Schema changes involving rewriting big tables

Sometimes you have a DDL change on a large table that will involve
a table rewrite and the best way of deploying the change is to
make the DDL change
on a replicate then once it is finished promote the replica to the
origin in some controlled fashion. This avoids having to lock the
table on the origin for hours.

pglogical seems to allow minor schema changes on the replica such
as changing a type but it doesn't seem to allow a DO INSTEAD
trigger on the replica. I don't think pglogical currently meets
this use case particularly well

I'm not sure I fully understand that one.

Say you have a table A with column b

and the next version of your application you want to create a second
table B that has column B

create table B (b text);
insert into B select b from a;
alter table a drop column b;

but you want to do this on a replica because it is a very big table and
you want to minimize downtown.

You could have a trigger on the replica that performed updates on B.b
instead of A except triggers don't seem to get run on the replica.

Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

Steve

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#20Bruce Momjian
bruce@momjian.us
In reply to: Joshua D. Drake (#17)
Re: pglogical - logical replication contrib module

On Tue, Jan 26, 2016 at 08:14:26PM -0800, Joshua Drake wrote:

On 12/31/2015 03:34 PM, Petr Jelinek wrote:

Hi,

I'd like to submit the replication solution which is based on the
pglogical_output [1] module (which is obviously needed for this to
compile).

This is fantastic! However, history presents itself here and
PostgreSQL in the past has not "blessed" a single solution for
Replication. Obviously that changed a bit with streaming replication
but this is a bit different than that. As I understand it, PgLogical
is Logical Replication (similar to Slony and Londiste). I wouldn't
be surprised (although I don't know) if Slony were to start using
some of the pglogical_output module features in the future.

If we were to accept PgLogical into core, it will become the default
blessed solution for PostgreSQL. While that is great in some ways
it is a different direction than the project has taken in the past.
Is this what we want to do?

Replying late here, but I think with binary replication, we decided
that, assuming you were happy with the features provided, our streaming
binary replication solution was going to be the best and recommended way
of doing it.

I don't think we ever had that feeling with Slony or Londiste in that
there were so many limitations and so many different ways of
implementing logical replication that we never recommended a best way.

So, the question is, do we feel that PgLogical is best and recommended
way to do logical replication. If it is, then having it in core makes
sense.

--
Bruce Momjian <bruce@momjian.us> http://momjian.us
EnterpriseDB http://enterprisedb.com

+ As you are, so once was I. As I am, so you will be. +
+ Roman grave inscription                             +

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#21Oleg Bartunov
obartunov@gmail.com
In reply to: Bruce Momjian (#20)
Re: pglogical - logical replication contrib module

On Tue, Feb 16, 2016 at 5:38 PM, Bruce Momjian <bruce@momjian.us> wrote:

On Tue, Jan 26, 2016 at 08:14:26PM -0800, Joshua Drake wrote:

On 12/31/2015 03:34 PM, Petr Jelinek wrote:

Hi,

I'd like to submit the replication solution which is based on the
pglogical_output [1] module (which is obviously needed for this to
compile).

This is fantastic! However, history presents itself here and
PostgreSQL in the past has not "blessed" a single solution for
Replication. Obviously that changed a bit with streaming replication
but this is a bit different than that. As I understand it, PgLogical
is Logical Replication (similar to Slony and Londiste). I wouldn't
be surprised (although I don't know) if Slony were to start using
some of the pglogical_output module features in the future.

If we were to accept PgLogical into core, it will become the default
blessed solution for PostgreSQL. While that is great in some ways
it is a different direction than the project has taken in the past.
Is this what we want to do?

Replying late here, but I think with binary replication, we decided
that, assuming you were happy with the features provided, our streaming
binary replication solution was going to be the best and recommended way
of doing it.

I don't think we ever had that feeling with Slony or Londiste in that
there were so many limitations and so many different ways of
implementing logical replication that we never recommended a best way.

So, the question is, do we feel that PgLogical is best and recommended
way to do logical replication. If it is, then having it in core makes
sense.

DDL support is what it's missed for now.

Show quoted text

--
Bruce Momjian <bruce@momjian.us> http://momjian.us
EnterpriseDB http://enterprisedb.com

+ As you are, so once was I. As I am, so you will be. +
+ Roman grave inscription                             +

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#22Craig Ringer
craig@2ndquadrant.com
In reply to: Oleg Bartunov (#21)
Re: pglogical - logical replication contrib module

On 17 February 2016 at 00:54, Oleg Bartunov <obartunov@gmail.com> wrote:

DDL support is what it's missed for now.

TBH, based on experience with DDL replication and deparse in BDR, it's
going to be missing for a while yet too, or at least not comprehensively
present without caveats or exceptions.

Some DDL operations don't translate well to a series of replicatable
actions. The case I hit the most is

ALTER TABLE mytable ADD COLUMN somecolumn sometype NOT NULL DEFAULT
some_function();

This is executed (simplified) by taking an ACCESS EXCLUSIVE lock, changing
the catalogs but not making the changes visible yet, rewriting the table,
and committing to make the rewritten table and the catalog changes visible.

That won't work well with logical replication. We currently capture DDL
with event triggers and log them to a table for later logical decoding and
replay - that's the "recognised" way. The trouble being that replaying that
statement will result in an unnecessary full table rewrite on the
downstream. Then we have to decode and send stream of changes to a table
called pg_temp_<oid_of_mytable>, truncate the copy of mytable on the
downstream that we just rewrote and apply those rows instead.

Of course all that only works sensibly if you have exactly one upstream and
the downstream copy of the table is treated as (or enforced as) read-only.

Improving this probably needs DDL deparse to be smarter. Rather than just
emitting something that can be reconstructed into the SQL text of the DDL
it needs to emit one or more steps that are semantically the same but allow
us to skip the rewrite. Along the lines of:

* ALTER TABLE mytable ADD COLUMN somecolumn sometype;
* ALTER TABLE mytable ALTER COLUMN somecolumn DEFAULT some_function();
* <wait for rewrite data for mytable>
* ALTER TABLE mytable ALTER COLUMN somecolumn NOT NULL;

Alternately the downstream would need a hook that lets it intercept and
prevent table rewrites caused by ALTER TABLE and similar. So it can instead
just do a truncate and wait for the new rows to come from the master.

Note that all this means the standby has to hold an ACCESS EXCLUSIVE lock
on the table during all of replay. That shouldn't be necessary, all we
really need is an EXCLUSIVE lock since concurrent SELECTs are fine. No idea
how to do that.

Deparse is also just horribly complicated to get right. There are so many
clauses and subclauses and variants of statements. Each of which must be
perfect.

Not everything has a simple and obvious mapping on the downstream side
either. TRUNCATE ... CASCADE is the obvious one. You do a cascade truncate
on the master - do you want that to replicate as a cascaded truncate on the
replica, or a truncate of only those tables that actually got truncated on
the master? If the replica has additional tables with FKs pointing at
tables replica the TRUNCATE would truncate those too if you replicate it as
CASCADE; if you don't the truncate will fail instead. Really, both are
probably wrong as far as the user is concerned, but we can't truncate just
the tables truncated on the master, ignore the FK relationships, and leave
dangling FK references either.

All this means that DDL replication is probably only going to make sense in
scenarios where there's exactly one master and the replica obeys some rules
like "don't create FKs pointing from non-replicated tables to tables
replicated from somewhere else". A concept we currently have no way to
express or enforce like we do persistent-to-UNLOGGED FKs.

Then there's global objects. Something as simple as:

CREATE ROLE fred;

CREATE TABLE blah(...) OWNER fred;

will break replication because we only see the CREATE TABLE, not the CREATE
ROLE. If we instead replayed the CREATE ROLE and there were multiple
connections between different DBs on an upstream and downstream apply would
fail on all but one. But we can't anyway since there's no way to capture
that CREATE ROLE from any DB except the one it was executed in, which might
not even be one of the ones doing replication.

I strongly suspect we'll need logical decoding to be made aware of such
global DDL and decode it from the WAL writes to the system catalogs. Which
will be fun - but at least modifications to the shared catalogs are a lot
simpler than the sort of gymnastics done by ALTER TABLE, etc.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#23Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Craig Ringer (#22)
Re: pglogical - logical replication contrib module

Hi Craig,

Thanks for your explanation. I have to agree with your arguments that in
general case replication of DDL statement using logical decoding seems
to be problematic. But we are mostly considering logical decoding in
quite limited context: replication between two identical Postgres
database nodes (multimaster).

Do you think that it in this case replication of DLL can be done as
sequence of low level operations with system catalog tables
including manipulation with locks? So in your example with ALTER TABLE
statement, can we correctly replicate it to other nodes
as request to set exclusive lock + some manipulations with catalog
tables and data table itself?
If so, instead of full support of DDL in logical decoding we can only:

1. Add option whether to include operations on system catalog tables in
logical replication or not.
2. Make it possible to replicate lock requests (can be useful not only
for DDLs)

I looked how DDL was implemented in BDR and did it in similar way in our
multimaster.
But it is awful: we need to have two different channels for propagating
changes. Additionally, in multimaster we want to enforce cluster wide
ACID. It certainly includes operations with metadata. It will be very
difficult to implement if replication of DML and DDL is done in two
different ways...

Let me ask one more question concerning logical replication: how
difficult it will be from your point of view to support two phase commit
in logical replication? Are there some principle problems?

Thanks in advance,
Konstantin

On 17.02.2016 04:33, Craig Ringer wrote:

On 17 February 2016 at 00:54, Oleg Bartunov <obartunov@gmail.com
<mailto:obartunov@gmail.com>> wrote:

DDL support is what it's missed for now.

TBH, based on experience with DDL replication and deparse in BDR, it's
going to be missing for a while yet too, or at least not
comprehensively present without caveats or exceptions.

Some DDL operations don't translate well to a series of replicatable
actions. The case I hit the most is

ALTER TABLE mytable ADD COLUMN somecolumn sometype NOT NULL DEFAULT
some_function();

This is executed (simplified) by taking an ACCESS EXCLUSIVE lock,
changing the catalogs but not making the changes visible yet,
rewriting the table, and committing to make the rewritten table and
the catalog changes visible.

That won't work well with logical replication. We currently capture
DDL with event triggers and log them to a table for later logical
decoding and replay - that's the "recognised" way. The trouble being
that replaying that statement will result in an unnecessary full table
rewrite on the downstream. Then we have to decode and send stream of
changes to a table called pg_temp_<oid_of_mytable>, truncate the copy
of mytable on the downstream that we just rewrote and apply those rows
instead.

Of course all that only works sensibly if you have exactly one
upstream and the downstream copy of the table is treated as (or
enforced as) read-only.

Improving this probably needs DDL deparse to be smarter. Rather than
just emitting something that can be reconstructed into the SQL text of
the DDL it needs to emit one or more steps that are semantically the
same but allow us to skip the rewrite. Along the lines of:

* ALTER TABLE mytable ADD COLUMN somecolumn sometype;
* ALTER TABLE mytable ALTER COLUMN somecolumn DEFAULT some_function();
* <wait for rewrite data for mytable>
* ALTER TABLE mytable ALTER COLUMN somecolumn NOT NULL;

Alternately the downstream would need a hook that lets it intercept
and prevent table rewrites caused by ALTER TABLE and similar. So it
can instead just do a truncate and wait for the new rows to come from
the master.

Note that all this means the standby has to hold an ACCESS EXCLUSIVE
lock on the table during all of replay. That shouldn't be necessary,
all we really need is an EXCLUSIVE lock since concurrent SELECTs are
fine. No idea how to do that.

Deparse is also just horribly complicated to get right. There are so
many clauses and subclauses and variants of statements. Each of which
must be perfect.

Not everything has a simple and obvious mapping on the downstream side
either. TRUNCATE ... CASCADE is the obvious one. You do a cascade
truncate on the master - do you want that to replicate as a cascaded
truncate on the replica, or a truncate of only those tables that
actually got truncated on the master? If the replica has additional
tables with FKs pointing at tables replica the TRUNCATE would truncate
those too if you replicate it as CASCADE; if you don't the truncate
will fail instead. Really, both are probably wrong as far as the user
is concerned, but we can't truncate just the tables truncated on the
master, ignore the FK relationships, and leave dangling FK references
either.

All this means that DDL replication is probably only going to make
sense in scenarios where there's exactly one master and the replica
obeys some rules like "don't create FKs pointing from non-replicated
tables to tables replicated from somewhere else". A concept we
currently have no way to express or enforce like we do
persistent-to-UNLOGGED FKs.

Then there's global objects. Something as simple as:

CREATE ROLE fred;

CREATE TABLE blah(...) OWNER fred;

will break replication because we only see the CREATE TABLE, not the
CREATE ROLE. If we instead replayed the CREATE ROLE and there were
multiple connections between different DBs on an upstream and
downstream apply would fail on all but one. But we can't anyway since
there's no way to capture that CREATE ROLE from any DB except the one
it was executed in, which might not even be one of the ones doing
replication.

I strongly suspect we'll need logical decoding to be made aware of
such global DDL and decode it from the WAL writes to the system
catalogs. Which will be fun - but at least modifications to the shared
catalogs are a lot simpler than the sort of gymnastics done by ALTER
TABLE, etc.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#24Andres Freund
andres@anarazel.de
In reply to: Craig Ringer (#22)
Re: pglogical - logical replication contrib module

On 2016-02-17 09:33:56 +0800, Craig Ringer wrote:

Some DDL operations don't translate well to a series of replicatable
actions. The case I hit the most is

ALTER TABLE mytable ADD COLUMN somecolumn sometype NOT NULL DEFAULT
some_function();

This is executed (simplified) by taking an ACCESS EXCLUSIVE lock, changing
the catalogs but not making the changes visible yet, rewriting the table,
and committing to make the rewritten table and the catalog changes visible.

That won't work well with logical replication.

FWIW, I think this is much less a fundamental, and more an
implementation issue. Falling back to just re-replicating the table, and
then optimizing a few common cases (only immutable DEFALUT/USING
involved) should be enough for a while.

Lets get the basics right, before reaching for the moon.

Greetings,

Andres Freund

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#25Craig Ringer
craig@2ndquadrant.com
In reply to: Konstantin Knizhnik (#23)
Re: pglogical - logical replication contrib module

On 17 February 2016 at 16:24, Konstantin Knizhnik <k.knizhnik@postgrespro.ru

wrote:

Thanks for your explanation. I have to agree with your arguments that in
general case replication of DDL statement using logical decoding seems to
be problematic. But we are mostly considering logical decoding in quite
limited context: replication between two identical Postgres database nodes
(multimaster).

Yep, much like BDR. Where all this infrastructure came from and is/was
aimed at.

Do you think that it in this case replication of DLL can be done as
sequence of low level operations with system catalog tables
including manipulation with locks?

No.

For one thing logical decoding doesn't see catalog tuple changes right now.
Though I imagine that could be changed easily enough.

More importantly - oids. You add a column to a table:

ALTER TABLE mytable ADD COLUMN mycolumn some_type UNIQUE NOT NULL DEFAULT
some_function()

This writes to catalogs including:

pg_attribute
pg_constraint
pg_index
pg_class (for the index relation)

... probably more. It also refers to pg_class (for the definition of
mytable), pg_type (definition of some_type), pg_proc (definition of
some_function), the b-tree operator class for some_type in pg_opclass, the
b-tree indexam in pg_am, ... more.

Everything is linked by oids, and the oids are all node local. You can't
just blindly re-use them. If "some_type" is hstore, the oid of hstore in
pg_type might be different on the upstream and downstream. The only
exception is the oids of built-in types and even then that's not guaranteed
across major versions.

So if you blindly replicate catalog row changes you'll get a horrible mess.
That's before considering a table's relfilenode, which is initially the
same as its oid, but subject to change if truncated or rewritten.

To even begin to do this half-sanely you'd have to maintain a mapping of
upstream object oids->names on the downstream, with invalidations
replicated from the upstream. That's only the beginning. There's handling
of extensions and lots more fun.

So in your example with ALTER TABLE statement, can we correctly replicate
it to other nodes
as request to set exclusive lock + some manipulations with catalog tables
and data table itself?

Nope. No hope, not unless "some manipulations with catalog tables and data
table its self" is a lot more comprehensive than I think you mean.

1. Add option whether to include operations on system catalog tables in
logical replication or not.

I would like to have this anyway.

2. Make it possible to replicate lock requests (can be useful not only for
DDLs)

I have no idea how you'd even begin to do that.

I looked how DDL was implemented in BDR and did it in similar way in our
multimaster.
But it is awful: we need to have two different channels for propagating
changes.

Yeah, it's not beautiful, but maybe you misunderstood something? The DDL is
written to a table, and that table's changes are replayed along with
everything else. It's consistent and keeps DDL changes as part of the xact
that performed them. Maybe you misunderstood how it works in BDR and missed
the indirection via a table?

Additionally, in multimaster we want to enforce cluster wide ACID. It
certainly includes operations with metadata. It will be very difficult to
implement if replication of DML and DDL is done in two different ways...

That's pretty much why BDR does it this way, warts and all. Though it
doesn't offer cluster-wide ACID it does need atomic commit of xacts that
may contain DML, DDL, or some mix of the two.

Let me ask one more question concerning logical replication: how difficult
it will be from your point of view to support two phase commit in logical
replication? Are there some principle problems?

I haven't looked closely yet. Andres will know more.

I very, very badly want to be able to decode 2PC prepared xacts myself.

The main issue I'm aware of is locking - specifically the inability to
impersonate another backend and treat locks held by that backend (which
might be a fake backend for a pg_prepared_xacts entry) as held by ourselves
for the purpose of being able to access relations, etc.

The work Robert is doing on group locking looks absolutely ideal for this,
but won't land before 9.7.

(Closely related, I also want to be able to hook into commit and transform
a normal COMMIT into a PREPARE TRANSACTION, <do some stuff>, COMMIT
PREPARED with the application that issued the commit none the wiser. This
will allow pessimistic 2PC-based conflict handling for must-succeed xacts
like those that do DDL).

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#26Craig Ringer
craig@2ndquadrant.com
In reply to: Andres Freund (#24)
Re: pglogical - logical replication contrib module

On 17 February 2016 at 16:27, Andres Freund <andres@anarazel.de> wrote:

On 2016-02-17 09:33:56 +0800, Craig Ringer wrote:

Some DDL operations don't translate well to a series of replicatable
actions. The case I hit the most is

ALTER TABLE mytable ADD COLUMN somecolumn sometype NOT NULL DEFAULT
some_function();

This is executed (simplified) by taking an ACCESS EXCLUSIVE lock,

changing

the catalogs but not making the changes visible yet, rewriting the table,
and committing to make the rewritten table and the catalog changes

visible.

That won't work well with logical replication.

FWIW, I think this is much less a fundamental, and more an
implementation issue. Falling back to just re-replicating the table

Do you mean taking a new schema dump from the upstream? Or just the table
data?

We already receive the table data in a pg_temp_<nnnn> virtual relation.
While it'd be nice to have a better way to map that to the relation being
rewritten without having to do string compares on table names all the time,
it works. If we do a low level truncate on the table *then* execute the DDL
on the empty table and finally rewrite it based on that stream as we
receive it that should work OK.

Lets get the basics right, before reaching for the moon.

Yeah, it's got to be incremental. Though I do think we'll need to address
DDL affecting shared catalogs.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#27Konstantin Knizhnik
k.knizhnik@postgrespro.ru
In reply to: Craig Ringer (#25)
Re: pglogical - logical replication contrib module

Ok, what about the following plan:

1. Support custom WAL records (as far as I know 2ndQuadrant has such patch).
2. Add one more function to logical decoding allowing to deal with
custom records.

So the idea is that we somehow record DDL in WAL (for example using
executor hook),
then them are proceeded using logical decoding, calling special logical
deocding plugin function to handle this records.
For example we can store DDL in WAL just as SQL statements and so easily
replay them.

In this case DDL will be replicated using the same mechanism and through
the same channel as DML.

On 17.02.2016 12:16, Craig Ringer wrote:

On 17 February 2016 at 16:24, Konstantin Knizhnik
<k.knizhnik@postgrespro.ru <mailto:k.knizhnik@postgrespro.ru>> wrote:

Thanks for your explanation. I have to agree with your arguments
that in general case replication of DDL statement using logical
decoding seems to be problematic. But we are mostly considering
logical decoding in quite limited context: replication between two
identical Postgres database nodes (multimaster).

Yep, much like BDR. Where all this infrastructure came from and is/was
aimed at.

Do you think that it in this case replication of DLL can be done
as sequence of low level operations with system catalog tables
including manipulation with locks?

No.

For one thing logical decoding doesn't see catalog tuple changes right
now. Though I imagine that could be changed easily enough.

More importantly - oids. You add a column to a table:

ALTER TABLE mytable ADD COLUMN mycolumn some_type UNIQUE NOT NULL
DEFAULT some_function()

This writes to catalogs including:

pg_attribute
pg_constraint
pg_index
pg_class (for the index relation)

... probably more. It also refers to pg_class (for the definition of
mytable), pg_type (definition of some_type), pg_proc (definition of
some_function), the b-tree operator class for some_type in pg_opclass,
the b-tree indexam in pg_am, ... more.

Everything is linked by oids, and the oids are all node local. You
can't just blindly re-use them. If "some_type" is hstore, the oid of
hstore in pg_type might be different on the upstream and downstream.
The only exception is the oids of built-in types and even then that's
not guaranteed across major versions.

So if you blindly replicate catalog row changes you'll get a horrible
mess. That's before considering a table's relfilenode, which is
initially the same as its oid, but subject to change if truncated or
rewritten.

To even begin to do this half-sanely you'd have to maintain a mapping
of upstream object oids->names on the downstream, with invalidations
replicated from the upstream. That's only the beginning. There's
handling of extensions and lots more fun.

So in your example with ALTER TABLE statement, can we correctly
replicate it to other nodes
as request to set exclusive lock + some manipulations with catalog
tables and data table itself?

Nope. No hope, not unless "some manipulations with catalog tables and
data table its self" is a lot more comprehensive than I think you mean.

1. Add option whether to include operations on system catalog
tables in logical replication or not.

I would like to have this anyway.

2. Make it possible to replicate lock requests (can be useful not
only for DDLs)

I have no idea how you'd even begin to do that.

I looked how DDL was implemented in BDR and did it in similar way
in our multimaster.
But it is awful: we need to have two different channels for
propagating changes.

Yeah, it's not beautiful, but maybe you misunderstood something? The
DDL is written to a table, and that table's changes are replayed along
with everything else. It's consistent and keeps DDL changes as part of
the xact that performed them. Maybe you misunderstood how it works in
BDR and missed the indirection via a table?

Additionally, in multimaster we want to enforce cluster wide ACID.
It certainly includes operations with metadata. It will be very
difficult to implement if replication of DML and DDL is done in
two different ways...

That's pretty much why BDR does it this way, warts and all. Though it
doesn't offer cluster-wide ACID it does need atomic commit of xacts
that may contain DML, DDL, or some mix of the two.

Let me ask one more question concerning logical replication: how
difficult it will be from your point of view to support two phase
commit in logical replication? Are there some principle problems?

I haven't looked closely yet. Andres will know more.

I very, very badly want to be able to decode 2PC prepared xacts myself.

The main issue I'm aware of is locking - specifically the inability to
impersonate another backend and treat locks held by that backend
(which might be a fake backend for a pg_prepared_xacts entry) as held
by ourselves for the purpose of being able to access relations, etc.

The work Robert is doing on group locking looks absolutely ideal for
this, but won't land before 9.7.

(Closely related, I also want to be able to hook into commit and
transform a normal COMMIT into a PREPARE TRANSACTION, <do some stuff>,
COMMIT PREPARED with the application that issued the commit none the
wiser. This will allow pessimistic 2PC-based conflict handling for
must-succeed xacts like those that do DDL).

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Konstantin Knizhnik
Postgres Professional: http://www.postgrespro.com
The Russian Postgres Company

#28Craig Ringer
craig@2ndquadrant.com
In reply to: Konstantin Knizhnik (#27)
Re: pglogical - logical replication contrib module

On 17 February 2016 at 18:39, Konstantin Knizhnik <k.knizhnik@postgrespro.ru

wrote:

Ok, what about the following plan:

1. Support custom WAL records (as far as I know 2ndQuadrant has such
patch).
2. Add one more function to logical decoding allowing to deal with custom
records.

So the idea is that we somehow record DDL in WAL (for example using
executor hook),
then them are proceeded using logical decoding, calling special logical
deocding plugin function to handle this records.
For example we can store DDL in WAL just as SQL statements and so easily
replay them.

In this case DDL will be replicated using the same mechanism and through
the same channel as DML.

Sure, you can do that, but you don't need to.

Go read the relevant BDR code again, you've missed how it works.

When DDL is fired the registered event trigger captures the DDL and passes
it to DDL deparse to extract a normalized representation. It is inserted
into a queued ddl commands table in the BDR schema during the transaction
that performed the DDL.

Later, when that transaction is decoded by logical decoding we see an
insert into the queued ddl commands table and replicate that to the
client(s).

Clients see the inserts into the queued DDL commands table and special-case
them on replay. As well as executing the original insert they also execute
the DDL command that was inserted into the table. This happens at the same
point in the transaction as the original insert, which is when the DDL was
run. So it's all consistent.

--
Craig Ringer http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

#29Alvaro Herrera
alvherre@2ndquadrant.com
In reply to: Craig Ringer (#22)
Re: pglogical - logical replication contrib module

Craig Ringer wrote:

Improving this probably needs DDL deparse to be smarter. Rather than just
emitting something that can be reconstructed into the SQL text of the DDL
it needs to emit one or more steps that are semantically the same but allow
us to skip the rewrite. Along the lines of:

* ALTER TABLE mytable ADD COLUMN somecolumn sometype;
* ALTER TABLE mytable ALTER COLUMN somecolumn DEFAULT some_function();
* <wait for rewrite data for mytable>
* ALTER TABLE mytable ALTER COLUMN somecolumn NOT NULL;

Compared to the effort involved in getting the current DDL-event capture
stuff in event triggers, and writing the extension that creates the JSON
representation that expands to SQL commands, this seems easy to do.
What currently happens is that we get a list of ALTER TABLE subcommands
and then produce a single ALTER TABLE that covers them all. To fix this
problem we could mark those ALTER TABLE subcommands that require a table
rewrite, and mark them specially; emit a list of the ones before the
rewrite as one command, then emit some sort of token that indicates the
table rewrite, then emit a list of the ones after the rewrite. The
replay code can then "wait" for the rewrite to occur.

Since this is all in extension code, it's possible to tailor to the
needs you have. (This is the point where I'm extremely happy we ended
up creating the hooks and the new pseudo-type separately from the
extension containing the JSON-generating bits, instead of having it all
be a single piece.)

One slight pain point in the above is the handling of ALTER COLUMN / SET
NOT NULL. That one currently requires a table scan, which would be nice
to avoid, but I don't see any way to do that.

--
�lvaro Herrera http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

#30Petr Jelinek
petr@2ndquadrant.com
In reply to: Steve Singer (#18)
Re: pglogical - logical replication contrib module

Hi

On 03/02/16 03:25, Steve Singer wrote:

The following review has been posted through the commitfest application:
make installcheck-world: tested, failed
Implements feature: tested, failed
Spec compliant: not tested
Documentation: tested, failed

Here is some more review

+- `pglogical.replication_set_add_table(set_name name, table_name regclass, synchronize boolean)`
+  Adds a table to replication set.
+
+  Parameters:
+  - `set_name` - name of the existing replication set
+  - `table_name` - name or OID of the table to be added to the set
+  - `synchronize` - if true, the table data is synchronized on all subscribers
+    which are subscribed to given replication set, default false
+

The argument to this function is actually named "relation" not "table_name" though we might want to update the function to name the argument table_name.

Also we don't explain what 'synchronize' means I first thought that a value of false would mean that existing data won't be copied but any new changes will be.
A value of false actually seems to mean that nothing will happen with the table until the synchronize function is manually called. We seem to be using the word 'synchronize' in different sense in different places I find it confusing (ie synchronize_data and syncronize_structure in create_subscription).

False should mean exactly what you thought it would, will have to look
what's the issue there. Obviously UPDATEs or DELETEs won't really do
anything when there are no data but INSERTs should be replicated even
with false.

But I agree we need to define sychronized better, as we discussed we
also want to change status to replicated instead of synchronized. I am
btw thinking that default value for synchronizing schema should be false
in the create_subsription.

*** a/contrib/pglogical/pglogical_sync.c
--- b/contrib/pglogical/pglogical_sync.c
+ static void
+ dump_structure(PGLogicalSubscription *sub, const char *snapshot)
+ {
+   char        pg_dump[MAXPGPATH];
+   uint32      version;
+   int         res;
+   StringInfoData  command;
+
+   if (find_other_exec_version(my_exec_path, PGDUMP_BINARY, &version, pg_dump))
+       elog(ERROR, "pglogical subscriber init failed to find pg_dump relative to binary %s",
+            my_exec_path);
+
+   if (version / 100 != PG_VERSION_NUM / 100)
+       elog(ERROR, "pglogical subscriber init found pg_dump with wrong major version %d.%d, expected %d.%d",
+            version / 100 / 100, version / 100 % 100,
+            PG_VERSION_NUM / 100 / 100, PG_VERSION_NUM / 100 % 100);
+
+   initStringInfo(&command);
+ #if PG_VERSION_NUM < 90500
+   appendStringInfo(&command, "%s --snapshot=\"%s\" -s -N %s -N pglogical_origin -F c -f \"/tmp/pglogical-%d.dump\" \"%s\"",
+ #else
+   appendStringInfo(&command, "%s --snapshot=\"%s\" -s -N %s -F c -f \"/tmp/pglogical-%d.dump\" \"%s\"",

1) I am not sure we can assume/require that the pg_dump binary be in the same location as the postgres binary. I don't know think we've ever required that client binaries (ie psql, pg_dump, pg_restore ...) be in the same directory as postgres. pg_upgrade does require this so maybe this isn't a problem in practice but I thought I'd point it out. Ideally wouldn't need to call an external program to get a schema dump but turning pg_dump into a library is beyond the scope of this patch.

Well for now I don't see that as big issue, especially given that the
pg_dump needs to be same version as the server. We can make it GUC if
needed but that's not something that seems problematic so far. I agree
ideal solution would be to have library but that's something that will
take much longer I am afraid.

2) I don't think we can hard-coded /tmp as the directory for the schema dump. I don't think will work on most windows systems and even on a unix system $TMPDIR might be set to something else. Maybe writing this into pgsql_tmp would be a better choice.

Yeah I turned that into GUC.

Furtherdown in
pglogical_sync_subscription(PGLogicalSubscription *sub)
+   switch (status)
+   {
+       /* Already synced, nothing to do except cleanup. */
+       case SYNC_STATUS_READY:
+           MemoryContextDelete(myctx);
+           return;
+       /* We can recover from crashes during these. */
+       case SYNC_STATUS_INIT:
+       case SYNC_STATUS_CATCHUP:
+           break;
+       default:
+           elog(ERROR,
+                "subscriber %s initialization failed during nonrecoverable step (%c), please try the setup again",
+                sub->name, status);
+           break;
+   }

I think the default case needs to do something to unregister the background worker. We already discussed trying to get the error message to a user in a better way either way there isn't any sense in this background worker being launched again if the error is nonrecoverable.

Agreed, for this specific case we can actually pretty easily put the
error into some catalog and just disable the subscription.

+
+               tables = copy_replication_sets_data(sub->origin_if->dsn,
+                                                   sub->target_if->dsn,
+                                                   snapshot,
+                                                   sub->replication_sets);
+
+               /* Store info about all the synchronized tables. */
+               StartTransactionCommand();
+               foreach (lc, tables)

Shouldn't we be storing the info about the synchronized tables as part of the same transaction that does the sync?

Well that's complicated as we also have post copy stuff to do (creating
indexes and stuff), so far we wan to begin from beginning I think if the
table fails so we consider it unsynced until also post-data part is
done. But I think the initial sync needs a lot of work in general.

I'll keeping going through the code as I have time. I think it is appropriate to move this to the next CF since the CF is past the end date and the patch has received some review. When you have an updated version of the patch post it, don't wait until March.

Sorry for not being very active in this thread, I really appreciate that
you take time to review this, I was just quite busy last few weeks (and
stolen laptop during business trip didn't help that much either). I
wasn't specifically waiting for March, but I have more WIP things
(privately) on this that I wanted to submit as a whole but not enough
time to get it to -hackers (one of those things is replica trigger
firing that you mentioned upthread). If you are interested I have the
"hackers preparation" branch at
https://github.com/2ndQuadrant/postgres/tree/dev/pglogical , it does not
have WIP stuff, mostly only things I am already happy with and it's what
I use for git format-patch for hackers submission.

--
Petr Jelinek http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Training & Services

--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers