From 310b3a210dd3c15ac8836d203a9d3bb3c40a1491 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Tue, 14 Apr 2026 11:31:03 +0200 Subject: [PATCH 1/3] Add regression and TAP tests for lo_lseek, lo_tell, lo_truncate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Go-based test suite covered three large-object operations — lo_lseek, lo_tell, and lo_truncate — that had no equivalent in the SQL regression or TAP tests. Add coverage for all three before the Go tests are removed. sql/lolor.sql and expected/lolor.out: three new sections exercise lo_lseek (seek to offset 15, overwrite 11 bytes, read back and verify the splice), lo_tell (confirm cursor is at 0 on a fresh open and at 11 after a lowrite of 11 bytes), and lo_truncate (write 36 bytes, truncate to 10, verify only the prefix survives). t/005_logical_replication.pl: extend the logical-replication scenario with the same three operations on the publisher side, wait for subscriber catchup after each mutating step, and assert the correct content on both nodes. Also add catalog-consistency checks that confirm lolor.pg_largeobject_metadata and lolor.pg_largeobject row counts match between publisher and subscriber after all operations.~ --- expected/lolor.out | 149 +++++++++++++++++++++++++++++++++++ sql/lolor.sql | 57 ++++++++++++++ t/005_logical_replication.pl | 102 ++++++++++++++++++++++++ 3 files changed, 308 insertions(+) diff --git a/expected/lolor.out b/expected/lolor.out index 1409abe..22c4dc7 100644 --- a/expected/lolor.out +++ b/expected/lolor.out @@ -66,6 +66,155 @@ SELECT lo_close(:fd); 0 (1 row) +END; +-- +-- lo_lseek: seek to an offset, overwrite partial content, verify result. +-- Expected: first 15 chars unchanged, next 11 replaced by '', +-- trailing chars from original string preserved. +-- +SELECT lo_creat(-1) AS loid \gset +BEGIN; +SELECT lo_open(:loid, x'60000'::int) AS fd \gset +SELECT lowrite(:fd, '0123456789abcdefghijklmnopqrstuvwxyz'); + lowrite +--------- + 36 +(1 row) + +SELECT lo_lseek(:fd, 15, 0); + lo_lseek +---------- + 15 +(1 row) + +SELECT lowrite(:fd, ''); + lowrite +--------- + 11 +(1 row) + +SELECT lo_close(:fd); + lo_close +---------- + 0 +(1 row) + +END; +BEGIN; +SELECT lo_open(:loid, 262144) AS fd \gset +SELECT convert_from(loread(:fd, 1024), 'UTF8'); + convert_from +-------------------------------------- + 0123456789abcdeqrstuvwxyz +(1 row) + +SELECT lo_close(:fd); + lo_close +---------- + 0 +(1 row) + +END; +-- +-- lo_tell: verify cursor position before and after a write. +-- Expected: position 0 before write, position 11 after writing 11 bytes; +-- content has first 11 chars overwritten by ''. +-- +SELECT lo_creat(-1) AS loid \gset +BEGIN; +SELECT lo_open(:loid, x'60000'::int) AS fd \gset +SELECT lowrite(:fd, '0123456789abcdefghijklmnopqrstuvwxyz'); + lowrite +--------- + 36 +(1 row) + +SELECT lo_lseek(:fd, 0, 0); + lo_lseek +---------- + 0 +(1 row) + +SELECT lo_tell(:fd); + lo_tell +--------- + 0 +(1 row) + +SELECT lowrite(:fd, ''); + lowrite +--------- + 11 +(1 row) + +SELECT lo_tell(:fd); + lo_tell +--------- + 11 +(1 row) + +SELECT lo_close(:fd); + lo_close +---------- + 0 +(1 row) + +END; +BEGIN; +SELECT lo_open(:loid, 262144) AS fd \gset +SELECT convert_from(loread(:fd, 1024), 'UTF8'); + convert_from +-------------------------------------- + bcdefghijklmnopqrstuvwxyz +(1 row) + +SELECT lo_close(:fd); + lo_close +---------- + 0 +(1 row) + +END; +-- +-- lo_truncate: truncate to 10 bytes, verify only the prefix survives. +-- Expected: only "0123456789" readable after truncation. +-- +SELECT lo_creat(-1) AS loid \gset +BEGIN; +SELECT lo_open(:loid, x'60000'::int) AS fd \gset +SELECT lowrite(:fd, '0123456789abcdefghijklmnopqrstuvwxyz'); + lowrite +--------- + 36 +(1 row) + +SELECT lo_truncate(:fd, 10); + lo_truncate +------------- + 0 +(1 row) + +SELECT lo_close(:fd); + lo_close +---------- + 0 +(1 row) + +END; +BEGIN; +SELECT lo_open(:loid, 262144) AS fd \gset +SELECT convert_from(loread(:fd, 1024), 'UTF8'); + convert_from +-------------- + 0123456789 +(1 row) + +SELECT lo_close(:fd); + lo_close +---------- + 0 +(1 row) + END; DROP EXTENSION lolor; -- Check extension upgrade diff --git a/sql/lolor.sql b/sql/lolor.sql index 78546ee..35fbd3e 100644 --- a/sql/lolor.sql +++ b/sql/lolor.sql @@ -33,6 +33,63 @@ SELECT convert_from(loread(:fd, 1024), 'UTF8'); SELECT lo_close(:fd); END; +-- +-- lo_lseek: seek to an offset, overwrite partial content, verify result. +-- Expected: first 15 chars unchanged, next 11 replaced by '', +-- trailing chars from original string preserved. +-- +SELECT lo_creat(-1) AS loid \gset +BEGIN; +SELECT lo_open(:loid, x'60000'::int) AS fd \gset +SELECT lowrite(:fd, '0123456789abcdefghijklmnopqrstuvwxyz'); +SELECT lo_lseek(:fd, 15, 0); +SELECT lowrite(:fd, ''); +SELECT lo_close(:fd); +END; +BEGIN; +SELECT lo_open(:loid, 262144) AS fd \gset +SELECT convert_from(loread(:fd, 1024), 'UTF8'); +SELECT lo_close(:fd); +END; + +-- +-- lo_tell: verify cursor position before and after a write. +-- Expected: position 0 before write, position 11 after writing 11 bytes; +-- content has first 11 chars overwritten by ''. +-- +SELECT lo_creat(-1) AS loid \gset +BEGIN; +SELECT lo_open(:loid, x'60000'::int) AS fd \gset +SELECT lowrite(:fd, '0123456789abcdefghijklmnopqrstuvwxyz'); +SELECT lo_lseek(:fd, 0, 0); +SELECT lo_tell(:fd); +SELECT lowrite(:fd, ''); +SELECT lo_tell(:fd); +SELECT lo_close(:fd); +END; +BEGIN; +SELECT lo_open(:loid, 262144) AS fd \gset +SELECT convert_from(loread(:fd, 1024), 'UTF8'); +SELECT lo_close(:fd); +END; + +-- +-- lo_truncate: truncate to 10 bytes, verify only the prefix survives. +-- Expected: only "0123456789" readable after truncation. +-- +SELECT lo_creat(-1) AS loid \gset +BEGIN; +SELECT lo_open(:loid, x'60000'::int) AS fd \gset +SELECT lowrite(:fd, '0123456789abcdefghijklmnopqrstuvwxyz'); +SELECT lo_truncate(:fd, 10); +SELECT lo_close(:fd); +END; +BEGIN; +SELECT lo_open(:loid, 262144) AS fd \gset +SELECT convert_from(loread(:fd, 1024), 'UTF8'); +SELECT lo_close(:fd); +END; + DROP EXTENSION lolor; -- Check extension upgrade diff --git a/t/005_logical_replication.pl b/t/005_logical_replication.pl index 8f518f8..91de725 100644 --- a/t/005_logical_replication.pl +++ b/t/005_logical_replication.pl @@ -69,6 +69,108 @@ is($result, 'post-subscription LO', "Post-subscription LO replicated via logical streaming"); +# lo_lseek: seek to offset 15, overwrite 11 bytes, verify on both nodes +$publisher->safe_psql('postgres', + qq(SELECT lo_from_bytea(3, '0123456789abcdefghijklmnopqrstuvwxyz'))); +$publisher->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(3, x'60000'::int) AS fd \\gset + SELECT lo_lseek(:fd, 15, 0); + SELECT lowrite(:fd, ''); + SELECT lo_close(:fd); + END; +)); +$publisher->wait_for_catchup('lolor_sub'); + +$result = $publisher->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(3, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, '0123456789abcdeqrstuvwxyz', + "lo_lseek: overwritten content correct on publisher"); + +$result = $subscriber->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(3, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, '0123456789abcdeqrstuvwxyz', + "lo_lseek: seeked/overwritten content replicated to subscriber"); + +# lo_tell: verify cursor positions (publisher only — tell is a local cursor op). +# Two separate transactions: first confirms position is 0 on a fresh open; +# second confirms position advances to 11 after writing 11 bytes. +$publisher->safe_psql('postgres', + qq(SELECT lo_from_bytea(4, '0123456789abcdefghijklmnopqrstuvwxyz'))); + +# \gset suppresses lo_open output; only lo_tell and lo_close print a row each. +my $pos = $publisher->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(4, 262144) AS fd \\gset + SELECT lo_tell(:fd); + SELECT lo_close(:fd); + END; +)); +is((split /\n/, $pos)[0], '0', "lo_tell: position at open is 0"); + +# lowrite prints one row, then lo_tell prints one row, then lo_close one row. +$pos = $publisher->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(4, x'60000'::int) AS fd \\gset + SELECT lowrite(:fd, ''); + SELECT lo_tell(:fd); + SELECT lo_close(:fd); + END; +)); +is((split /\n/, $pos)[1], '11', "lo_tell: position after 11-byte write is 11"); + +# lo_truncate: truncate to 10 bytes, verify prefix on both nodes +$publisher->safe_psql('postgres', + qq(SELECT lo_from_bytea(5, '0123456789abcdefghijklmnopqrstuvwxyz'))); +$publisher->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(5, x'60000'::int) AS fd \\gset + SELECT lo_truncate(:fd, 10); + SELECT lo_close(:fd); + END; +)); +$publisher->wait_for_catchup('lolor_sub'); + +$result = $publisher->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(5, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, '0123456789', "lo_truncate: only prefix survives on publisher"); + +$result = $subscriber->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(5, 262144) AS fd \\gset + SELECT convert_from(loread(:fd, 1024), 'UTF8'); + END; +)); +is($result, '0123456789', "lo_truncate: truncated content replicated to subscriber"); + +# Catalog consistency: pg_largeobject_metadata and pg_largeobject row counts +# must match between publisher and subscriber after all operations. +my $pub_meta = $publisher->safe_psql('postgres', + "SELECT count(*) FROM lolor.pg_largeobject_metadata"); +my $sub_meta = $subscriber->safe_psql('postgres', + "SELECT count(*) FROM lolor.pg_largeobject_metadata"); +is($sub_meta, $pub_meta, + "catalog consistency: pg_largeobject_metadata row count matches across nodes"); + +my $pub_lo = $publisher->safe_psql('postgres', + "SELECT count(*) FROM lolor.pg_largeobject"); +my $sub_lo = $subscriber->safe_psql('postgres', + "SELECT count(*) FROM lolor.pg_largeobject"); +is($sub_lo, $pub_lo, + "catalog consistency: pg_largeobject row count matches across nodes"); + $subscriber->stop; $publisher->stop; From f41203522d5dff57b80570cb69acce425e059202 Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Tue, 14 Apr 2026 11:36:41 +0200 Subject: [PATCH 2/3] Remove Go test suite and infrastructure The Go tests covered lo_lseek, lo_tell, and lo_truncate operations that were not tested elsewhere. Those gaps have now been filled by SQL regression tests and TAP tests, making the Go suite redundant. Remove tests/go/ (main_test.go, go.mod, test.properties, README), docker/test-configs/test.properties.go, the Golang section from docker/run-tests.sh, and replace the golang:1.22.4 Docker base image in docker/Dockerfile.tester with debian:bookworm, which provides the same OS environment without the Go toolchain. --- docker/Dockerfile.tester | 2 +- docker/run-tests.sh | 9 - docker/test-configs/test.properties.go | 9 - tests/go/README.md | 59 ---- tests/go/go.mod | 18 - tests/go/main_test.go | 470 ------------------------- tests/go/test.properties | 14 - 7 files changed, 1 insertion(+), 580 deletions(-) delete mode 100644 docker/test-configs/test.properties.go delete mode 100644 tests/go/README.md delete mode 100644 tests/go/go.mod delete mode 100644 tests/go/main_test.go delete mode 100644 tests/go/test.properties diff --git a/docker/Dockerfile.tester b/docker/Dockerfile.tester index 1f2d224..930d52b 100644 --- a/docker/Dockerfile.tester +++ b/docker/Dockerfile.tester @@ -1,4 +1,4 @@ -FROM golang:1.22.4 +FROM debian:bookworm RUN apt -y update && \ apt -y install \ diff --git a/docker/run-tests.sh b/docker/run-tests.sh index 31591d9..d149e13 100755 --- a/docker/run-tests.sh +++ b/docker/run-tests.sh @@ -44,12 +44,3 @@ cd /home/pgedge/lolor/tests/python cp ../../docker/test-configs/test.properties.py test.properties python3 lolor_tests.py -v &>> /home/pgedge/lolor/tests/out.txt -sleep 10 - -#==================== Golang tests ==================== - -cd /home/pgedge/lolor/tests/go -go get github.com/jackc/pgx/v5 -go get github.com/magiconair/properties -cp ../../docker/test-configs/test.properties.go test.properties -go test -v &>> /home/pgedge/lolor/tests/out.txt diff --git a/docker/test-configs/test.properties.go b/docker/test-configs/test.properties.go deleted file mode 100644 index ef23d53..0000000 --- a/docker/test-configs/test.properties.go +++ /dev/null @@ -1,9 +0,0 @@ -n1.url=postgres://admin:password@n1:5432/demo -n2.url=postgres://admin:password@n2:5432/demo -n3.url=postgres://admin:password@n3:5432/demo - -n1.sub1=sub_n2n1 -n1.sub2=sub_n3n1 - -# sync delay (seconds) -sync_delay=10 diff --git a/tests/go/README.md b/tests/go/README.md deleted file mode 100644 index 6b6ba74..0000000 --- a/tests/go/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# Test suite - -These go tests are written to test pgEdge lolor extension. - -`lolor_tests` package relies on Large Object functionality provided -by `github.com/jackc/pgx/v5` module instead of calling lo_* methods directly. - -## How to run Unit Tests - -There is a need to download the go package `github.com/jackc/pgx/v5` on the -system i.e. - -``` -go get github.com/jackc/pgx/v5 -go get github.com/magiconair/properties -``` - -### Settings - -There is a need to configure GUC `lolor.node` in `postgresql.conf` e.g. -``` -lolor.node = 1 -``` - -Use `test.properties` for test suite settings e.g. database connection, etc. - -To enable replication of `lolor` large objects, there is a need to add tables -`lolor.pg_largeobject` and `lolor.pg_largeobject_metadata` to `spock` -replication set e.g. - -Run the following commands to create replication set i.e. -``` -# All Nodes -./pgedge spock repset-create lolor_tables_rs test_db - -# Node1 -./pgedge spock sub-add-repset sub_n1n2 lolor_tables_rs test_db -./pgedge spock sub-add-repset sub_n1n3 lolor_tables_rs test_db - -# Node2 -./pgedge spock sub-add-repset sub_n2n1 lolor_tables_rs test_db -./pgedge spock sub-add-repset sub_n2n3 lolor_tables_rs test_db - -# Node3 -./pgedge spock sub-add-repset sub_n3n1 lolor_tables_rs test_db -./pgedge spock sub-add-repset sub_n3n2 lolor_tables_rs test_db - -# All Nodes -# psql -d test_db -CREATE EXTENSION lolor; -./pgedge spock repset-add-table lolor_tables_rs 'lolor.pg_largeobject' test_db -./pgedge spock repset-add-table lolor_tables_rs 'lolor.pg_largeobject_metadata' test_db -``` - -### Run test suite - -``` -$ go test -v -``` diff --git a/tests/go/go.mod b/tests/go/go.mod deleted file mode 100644 index 8670f9d..0000000 --- a/tests/go/go.mod +++ /dev/null @@ -1,18 +0,0 @@ -module lolor_tests - -go 1.23.9 - -require github.com/jackc/pgx/v5 v5.7.6 - -require ( - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.14.3 // indirect - github.com/jackc/pgio v1.0.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.3 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgtype v1.14.0 // indirect - github.com/magiconair/properties v1.8.7 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/text v0.27.0 // indirect -) diff --git a/tests/go/main_test.go b/tests/go/main_test.go deleted file mode 100644 index d9b9c8d..0000000 --- a/tests/go/main_test.go +++ /dev/null @@ -1,470 +0,0 @@ -package lolor_tests - -import ( - "bytes" - "context" - "fmt" - "os" - "testing" - - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgtype" - "github.com/magiconair/properties" -) - -var conn *pgx.Conn -var prop *properties.Properties -var conna [3]*pgx.Conn - -// Node 1 subscriptions -var n1subs [2]string - -// test input data -var data1 string = "0123456789abcdefghijklmnopqrstuvwxyz" - -type rowPgLargeObjectMetadata struct { - oid, lomowner pgtype.Uint32 -} - -func (rs *rowPgLargeObjectMetadata) ScanRow(rows pgx.Rows) error { - return rows.Scan(&rs.oid, &rs.lomowner) -} - -/* - * Connect with PG - */ -func connectPG() { - var err error - ctx := context.Background() - // urlExample := "postgres://username:password@localhost:5432/database_name" - conna[0], err = pgx.Connect(ctx, prop.MustGetString("n1.url")) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) - os.Exit(1) - } - conna[1], err = pgx.Connect(ctx, prop.MustGetString("n2.url")) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) - os.Exit(1) - } - conna[2], err = pgx.Connect(ctx, prop.MustGetString("n3.url")) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) - os.Exit(1) - } - conn = conna[0] -} - -func waitForSync() { - for i := 0; i < len(n1subs); i++ { - // Wait for subscription to finish synchronization - query := fmt.Sprint("SELECT spock.sub_wait_for_sync('", n1subs[i], "');") - executeSQL1(query, 0) - } -} - -/* - * Execute the query on the database server node - */ -func executeSQL1(query string, n int) { - ctx := context.Background() - rows, err := conna[n].Query(ctx, query) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to execute query: Node %d, Query %v, Error %v\n", n, query, err) - os.Exit(1) - } - - rows.Close() -} - -/* - * Execute the query on all database server nodes - */ -func executeSQL2(query string) { - for i := 0; i < len(conna); i++ { - executeSQL1(query, i) - } -} - -/* - * Query pg_largeobject_metadata table - */ -func pg_largeobject_metadata(c *pgx.Conn, loid uint32) rowPgLargeObjectMetadata { - var r rowPgLargeObjectMetadata - query := fmt.Sprint("select oid, lomowner from lolor.pg_largeobject_metadata where oid = ", loid, ";") - ctx := context.Background() - err := c.QueryRow(ctx, query).Scan(&r) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to execute query: Query %v, Error %v\n", query, err) - os.Exit(1) - } - return r -} - -/* - * Query pg_largeobject table - */ -func pg_largeobject(c *pgx.Conn, loid uint32, size int) []byte { - data := make([]byte, size) - query := fmt.Sprint("select data from lolor.pg_largeobject where loid = ", loid, ";") - ctx := context.Background() - err := c.QueryRow(ctx, query).Scan(&data) - if err != nil { - fmt.Fprintf(os.Stderr, "Unable to execute query: Query %v, Error %v\n", query, err) - os.Exit(1) - } - return data -} - -/* - * Verify large object oid by checking pg_largeobject_metadata table - */ -func check_pg_largeobject_metadata(loid uint32, t *testing.T) { - var row1 rowPgLargeObjectMetadata - - waitForSync() - for _, c := range conna { - row1 = pg_largeobject_metadata(c, loid) - if loid != uint32(row1.oid.Uint32) { - t.Errorf("lo_data = %d; want %d", loid, uint32(row1.oid.Uint32)) - } - } -} - -/* - * Verify large object data by checking pg_largeobject table - */ -func check_pg_largeobject(loid uint32, datain []byte, t *testing.T) { - var dataout []byte - - for _, c := range conna { - dataout = pg_largeobject(c, loid, len(datain)) - if !bytes.Equal(dataout, datain) { - t.Errorf("lo_data = %s; want %s", string(dataout), string(datain)) - } - } -} - -/* - * Initialize database - */ -func initDB() { - createExt := "CREATE EXTENSION IF NOT EXISTS lolor;" - executeSQL2(createExt) -} - -// Perform initializations -func do_init() { - prop = properties.MustLoadFile("test.properties", properties.UTF8) - n1subs[0] = string(prop.MustGetString("n1.sub1")) - n1subs[1] = string(prop.MustGetString("n1.sub2")) - connectPG() - initDB() -} - -/* - * Create large object and return the id - */ -func createlo(data string, t *testing.T) uint32 { - ctx := context.Background() - tx, err := conn.Begin(ctx) - if err != nil { - t.Fatalf("%v", err) - } - - lo := tx.LargeObjects() - id, err := lo.Create(ctx, 0) - if err != nil { - t.Fatalf("%v", err) - } - - obj, err := lo.Open(ctx, id, pgx.LargeObjectModeWrite) - if err != nil { - t.Fatalf("%v", err) - } - - n, err := obj.Write([]byte(data)) - _ = n - if err != nil { - t.Fatalf("%v", err) - } - - err = obj.Close() - if err != nil { - t.Fatalf("%v", err) - } - - // Commit the transaction - err = tx.Commit(ctx) - if err != nil { - t.Fatalf("%v", err) - } - - return id -} - -/* - * Read the large object and return the data - */ -func readlo(loid uint32, size int, t *testing.T) string { - var n int - ctx := context.Background() - tx, err := conn.Begin(ctx) - if err != nil { - t.Fatalf("%v", err) - } - - lo := tx.LargeObjects() - - obj, err := lo.Open(ctx, loid, pgx.LargeObjectModeRead) - if err != nil { - t.Fatalf("%v", err) - } - - data := make([]byte, size) - n, err = obj.Read(data) - _ = n - if err != nil { - t.Fatalf("%v", err) - } - - err = obj.Close() - if err != nil { - t.Fatalf("%v", err) - } - - // Commit the transaction - err = tx.Commit(ctx) - if err != nil { - t.Fatalf("%v", err) - } - - return string(data) -} - -/* - * Drop the large object - */ -func droplo(loid uint32, t *testing.T) { - ctx := context.Background() - tx, err := conn.Begin(ctx) - if err != nil { - t.Fatalf("%v", err) - } - - lo := tx.LargeObjects() - - err = lo.Unlink(ctx, loid) - if err != nil { - t.Fatalf("%v", err) - } - - // Commit the transaction - err = tx.Commit(ctx) - if err != nil { - t.Fatalf("%v", err) - } -} - -/* - * Perform seek operation on large object and return the new cursor position - */ -func seeklo(loid uint32, data string, t *testing.T) int64 { - ctx := context.Background() - tx, err := conn.Begin(ctx) - if err != nil { - t.Fatalf("%v", err) - } - - lo := tx.LargeObjects() - - obj, err := lo.Open(ctx, loid, pgx.LargeObjectModeRead|pgx.LargeObjectModeWrite) - if err != nil { - t.Fatalf("%v", err) - } - - pos, err := obj.Seek(15, 0) - if err != nil { - t.Fatalf("%v", err) - } - - n, err := obj.Write([]byte(data)) - _ = n - if err != nil { - t.Fatalf("%v", err) - } - - err = obj.Close() - if err != nil { - t.Fatalf("%v", err) - } - - // Commit the transaction - err = tx.Commit(ctx) - if err != nil { - t.Fatalf("%v", err) - } - - return pos -} - -/* - * Perform the tell operatoin on large object and return the current positions - */ -func telllo(loid uint32, data string, t *testing.T) (int64, int64) { - var pos1, pos2 int64 - ctx := context.Background() - tx, err := conn.Begin(ctx) - if err != nil { - t.Fatalf("%v", err) - } - - lo := tx.LargeObjects() - - obj, err := lo.Open(ctx, loid, pgx.LargeObjectModeRead|pgx.LargeObjectModeWrite) - if err != nil { - t.Fatalf("%v", err) - } - - pos1, err = obj.Tell() - if err != nil { - t.Fatalf("%v", err) - } - - n, err := obj.Write([]byte(data)) - _ = n - if err != nil { - t.Fatalf("%v", err) - } - - pos2, err = obj.Tell() - if err != nil { - t.Fatalf("%v", err) - } - - err = obj.Close() - if err != nil { - t.Fatalf("%v", err) - } - - // Commit the transaction - err = tx.Commit(ctx) - if err != nil { - t.Fatalf("%v", err) - } - - return pos1, pos2 -} - -/* - * Perform truncate operation on large object - */ -func truncatelo(loid uint32, size int, t *testing.T) { - ctx := context.Background() - tx, err := conn.Begin(ctx) - if err != nil { - t.Fatalf("%v", err) - } - - lo := tx.LargeObjects() - - obj, err := lo.Open(ctx, loid, pgx.LargeObjectModeRead|pgx.LargeObjectModeWrite) - if err != nil { - t.Fatalf("%v", err) - } - - err = obj.Truncate(int64(size)) - if err != nil { - t.Fatalf("%v", err) - } - - err = obj.Close() - if err != nil { - t.Fatalf("%v", err) - } - - // Commit the transaction - err = tx.Commit(ctx) - if err != nil { - t.Fatalf("%v", err) - } -} - -/* - * Basic data lo write and read test - */ -func TestLOReadWrite(t *testing.T) { - var loid uint32 = createlo(data1, t) - var lo_data string = readlo(loid, len(data1), t) - check_pg_largeobject_metadata(loid, t) - check_pg_largeobject(loid, []byte(data1), t) - droplo(loid, t) - if data1 != lo_data { - t.Errorf("lo_data = %s; want %s", lo_data, data1) - } -} - -/* - * Basic data lo seek test - */ -func TestLOSeek(t *testing.T) { - var expectedData string = "0123456789abcdeqrstuvwxyz" - var loid uint32 = createlo(data1, t) - var pos int64 = seeklo(loid, "", t) - var lo_data string = readlo(loid, len(data1), t) - check_pg_largeobject_metadata(loid, t) - check_pg_largeobject(loid, []byte(expectedData), t) - droplo(loid, t) - if expectedData != lo_data { - t.Errorf("lo_data = %s; want %s", lo_data, expectedData) - } - if pos != 15 { - t.Errorf("seek position = %d; want %d", pos, 15) - } -} - -/* - * Basic data lo tell test - */ -func TestLOTell(t *testing.T) { - var expectedData string = "bcdefghijklmnopqrstuvwxyz" - var loid uint32 = createlo(data1, t) - pos1, pos2 := telllo(loid, "", t) - var lo_data string = readlo(loid, len(data1), t) - check_pg_largeobject_metadata(loid, t) - check_pg_largeobject(loid, []byte(expectedData), t) - droplo(loid, t) - if expectedData != lo_data { - t.Errorf("lo_data = %s; want %s", lo_data, expectedData) - } - if pos1 != 0 { - t.Errorf("seek position 1 = %d; want %d", pos1, 0) - } - if pos2 != 11 { - t.Errorf("seek position 2 = %d; want %d", pos2, 11) - } -} - -/* - * Basic data lo truncate test - */ -func TestLOTruncate(t *testing.T) { - var expectedData string = "0123456789" - var size int = 10 - var loid uint32 = createlo(data1, t) - truncatelo(loid, size, t) - var lo_data string = readlo(loid, size, t) - check_pg_largeobject_metadata(loid, t) - check_pg_largeobject(loid, []byte(expectedData), t) - droplo(loid, t) - if expectedData != lo_data { - t.Errorf("lo_data = %s; want %s", lo_data, expectedData) - } -} - -func TestMain(m *testing.M) { - do_init() - code := m.Run() - defer conn.Close(context.Background()) - os.Exit(code) -} diff --git a/tests/go/test.properties b/tests/go/test.properties deleted file mode 100644 index 18ae192..0000000 --- a/tests/go/test.properties +++ /dev/null @@ -1,14 +0,0 @@ -# Node 1 -# Connection string -n1.url=postgres://username:password@192.168.1.8:5432/test_db -# Subscription names -n1.sub1=sub_n1n2 -n1.sub2=sub_n1n3 - -# Node 2 -# Connection string -n2.url=postgres://username:password@192.168.1.9:5432/test_db - -# Node 3 -# Connection string -n3.url=postgres://username:password@192.168.1.10:5432/test_db From 9c82c734eca32ff76ebd887599d3e5891d6600ad Mon Sep 17 00:00:00 2001 From: "Andrei V. Lepikhov" Date: Tue, 14 Apr 2026 12:06:53 +0200 Subject: [PATCH 3/3] CodeRabbit Review --- t/005_logical_replication.pl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/t/005_logical_replication.pl b/t/005_logical_replication.pl index 91de725..e60d019 100644 --- a/t/005_logical_replication.pl +++ b/t/005_logical_replication.pl @@ -127,6 +127,19 @@ )); is((split /\n/, $pos)[1], '11', "lo_tell: position after 11-byte write is 11"); +$publisher->wait_for_catchup('lolor_sub'); + +$pos = $subscriber->safe_psql('postgres', qq( + BEGIN; + SELECT lo_open(4, x'60000'::int) AS fd \\gset + SELECT lowrite(:fd, ''); + SELECT lo_tell(:fd); + SELECT lo_close(:fd); + END; +)); +is((split /\n/, $pos)[1], '11', + "lo_tell: position after 11-byte write is 11 on subscriber"); + # lo_truncate: truncate to 10 bytes, verify prefix on both nodes $publisher->safe_psql('postgres', qq(SELECT lo_from_bytea(5, '0123456789abcdefghijklmnopqrstuvwxyz')));