diff --git a/t/003_dump_restore.pl b/t/003_dump_restore.pl new file mode 100644 index 0000000..9dcdd22 --- /dev/null +++ b/t/003_dump_restore.pl @@ -0,0 +1,96 @@ +# Check dump and restore of lolor large objects +# +# Copyright (c) 2022-2026, pgEdge, Inc. +# + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $src = PostgreSQL::Test::Cluster->new('src_node'); +my $dst = PostgreSQL::Test::Cluster->new('dst_node'); +my ($result, $stdout, $stderr); + +# Setup source node with lolor extension +$src->init; +$src->append_conf('postgresql.conf', qq{lolor.node = 1}); +$src->start; +$src->safe_psql('postgres', "CREATE EXTENSION lolor"); + +# Create lolor large objects with known content +$src->safe_psql('postgres', + qq(SELECT lo_from_bytea(1, 'lolor LO object - 1'))); +$src->safe_psql('postgres', + qq(SELECT lo_from_bytea(2, 'lolor LO object - 2'))); + +# Verify content on source before dump +$result = $src->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(1, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, 'lolor LO object - 1', "Verify first LO content on source"); + +$result = $src->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(2, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, 'lolor LO object - 2', "Verify second LO content on source"); + +# Dump the source database +my $dump_file = $src->data_dir . '/dump.sql'; +command_ok( + ['pg_dump', '-f', $dump_file, '-d', $src->connstr('postgres')], + 'pg_dump succeeds on source with lolor objects'); + +$src->stop; + +# Setup destination node and restore +$dst->init; +$dst->append_conf('postgresql.conf', qq{lolor.node = 1}); +$dst->start; + +command_ok( + ['psql', '-X', '-f', $dump_file, '-d', $dst->connstr('postgres')], + 'restore dump on destination node succeeds'); + +# Verify lolor objects survived dump/restore +$result = $dst->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(1, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, 'lolor LO object - 1', + "First lolor LO preserved after dump/restore"); + +$result = $dst->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(2, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, 'lolor LO object - 2', + "Second lolor LO preserved after dump/restore"); + +# Verify new lolor objects can be created on destination +$dst->safe_psql('postgres', + qq(SELECT lo_from_bytea(3, 'new object on dst'))); +$result = $dst->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(3, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, 'new object on dst', + "Can create and read new lolor objects after restore"); + +$dst->stop; + +done_testing(); \ No newline at end of file diff --git a/t/004_streaming_replication.pl b/t/004_streaming_replication.pl new file mode 100644 index 0000000..2d04ec6 --- /dev/null +++ b/t/004_streaming_replication.pl @@ -0,0 +1,73 @@ +# Check streaming replication of lolor large objects +# +# Copyright (c) 2022-2026, pgEdge, Inc. +# + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $primary = PostgreSQL::Test::Cluster->new('primary'); +my ($result, $stdout, $stderr); + +# Setup primary node with lolor extension +$primary->init(allows_streaming => 1); +$primary->append_conf('postgresql.conf', qq{lolor.node = 1}); +$primary->start; +$primary->safe_psql('postgres', "CREATE EXTENSION lolor"); + +# Create a lolor large object BEFORE setting up the replica +$primary->safe_psql('postgres', + qq(SELECT lo_from_bytea(1, 'pre-backup LO object'))); + +$result = $primary->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(1, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, 'pre-backup LO object', "Pre-backup LO created on primary"); + +# Take a backup and create streaming standby +my $backup_name = 'my_backup'; +$primary->backup($backup_name); + +my $standby = PostgreSQL::Test::Cluster->new('standby'); +$standby->init_from_backup($primary, $backup_name, + has_streaming => 1); +$standby->start; + +# Create another lolor object on the primary AFTER standby is running +$primary->safe_psql('postgres', + qq(SELECT lo_from_bytea(2, 'post-backup LO object'))); + +# Wait for standby to catch up +$primary->wait_for_replay_catchup($standby); + +# Verify the pre-backup object is available on standby +$result = $standby->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(1, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, 'pre-backup LO object', + "Pre-backup LO available on standby"); + +# Verify the post-backup object was streamed to standby +$result = $standby->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(2, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, 'post-backup LO object', + "Post-backup LO replicated to standby via streaming"); + +$standby->stop; +$primary->stop; + +done_testing(); diff --git a/t/005_logical_replication.pl b/t/005_logical_replication.pl new file mode 100644 index 0000000..8f518f8 --- /dev/null +++ b/t/005_logical_replication.pl @@ -0,0 +1,75 @@ +# Check logical replication of lolor large objects +# +# Copyright (c) 2022-2026, pgEdge, Inc. +# + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +my $publisher = PostgreSQL::Test::Cluster->new('publisher'); +my $subscriber = PostgreSQL::Test::Cluster->new('subscriber'); +my ($result, $stdout, $stderr); + +# Setup publisher with logical replication support +$publisher->init(allows_streaming => 'logical'); +$publisher->append_conf('postgresql.conf', qq{lolor.node = 1}); +$publisher->start; +$publisher->safe_psql('postgres', "CREATE EXTENSION lolor"); + +# Create a lolor large object BEFORE setting up subscription +$publisher->safe_psql('postgres', + qq(SELECT lo_from_bytea(1, 'pre-subscription LO'))); + +# Create publication for lolor tables +$publisher->safe_psql('postgres', + "CREATE PUBLICATION lolor_pub FOR TABLE lolor.pg_largeobject, lolor.pg_largeobject_metadata"); + +# Setup subscriber with lolor extension (tables must exist before subscription) +$subscriber->init; +$subscriber->append_conf('postgresql.conf', qq{lolor.node = 2}); +$subscriber->start; +$subscriber->safe_psql('postgres', "CREATE EXTENSION lolor"); + +# Create subscription +my $publisher_connstr = $publisher->connstr . ' dbname=postgres'; +$subscriber->safe_psql('postgres', + "CREATE SUBSCRIPTION lolor_sub CONNECTION '$publisher_connstr' PUBLICATION lolor_pub"); + +# Wait for initial table sync to complete +$subscriber->wait_for_subscription_sync($publisher, 'lolor_sub'); + +# Verify pre-subscription object replicated via initial sync +$result = $subscriber->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(1, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, 'pre-subscription LO', + "Pre-subscription LO replicated via initial sync"); + +# Create another object on publisher AFTER subscription is active +$publisher->safe_psql('postgres', + qq(SELECT lo_from_bytea(2, 'post-subscription LO'))); + +# Wait for subscriber to catch up with ongoing changes +$publisher->wait_for_catchup('lolor_sub'); + +# Verify post-subscription object replicated via streaming +$result = $subscriber->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(2, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, 'post-subscription LO', + "Post-subscription LO replicated via logical streaming"); + +$subscriber->stop; +$publisher->stop; + +done_testing();