public inbox for [email protected]
help / color / mirror / Atom feedFrom: Andrew Dunstan <[email protected]>
To: Andres Freund <[email protected]>
Cc: [email protected]
Cc: Noah Misch <[email protected]>
Subject: Re: BackgroundPsql swallowing errors on windows
Date: Tue, 2 Jun 2026 15:03:57 -0400
Message-ID: <[email protected]> (raw)
In-Reply-To: <[email protected]>
References: <wmovm6xcbwh7twdtymxuboaoarbvwj2haasd3sikzlb3dkgz76@n45rzycluzft>
<[email protected]>
<gcjjfm54opgd4kohazc5sowpieta77sg7oywooaf75rux2hgky@qt6mqpfquuhj>
<[email protected]>
<5dgt6vir7n664qruw3ndywpmfxsmfuu4oeh43syocgo3g6v72n@hjxahrqk4w7t>
<[email protected]>
<tir2xsciqfiypagzortygjrwaomwdflbw3tjpz7iji2pefxye7@wkgxlxm7e2bg>
<[email protected]>
On 2026-02-18 We 2:41 PM, Andrew Dunstan wrote:
>
> On 2026-02-17 Tu 4:56 PM, Andres Freund wrote:
>> Hi,
>>
>> On 2026-02-17 16:31:02 -0500, Andrew Dunstan wrote:
>>> On 2026-02-16 Mo 7:17 PM, Andres Freund wrote:
>>>> I briefly tried this out. The overall resource usage of the test is
>>>> noticeably
>>>> reduced - and that's on linux with fast fork, so it should be
>>>> considerably
>>>> better on windows. However, the tests take a lot longer than
>>>> before, I think
>>>> mostly due to polling for results rather than waiting for them to
>>>> be ready
>>>> using PQsocketPoll() or such.
>>>>
>>>> E.g. bloom/001_wal takes about 15s on HEAD for me, but 138s with
>>>> the patch. I
>>>> think that's just due to the various usleep(100_000);
>>>>
>>>>
>>>> FWIW, oauth_validator/001_server fails with the patch at the moment.
>>>>
>>> Try this version. On my machine it's now a few percent faster. I
>>> fixed the
>>> polling. I also added pipeline support for large sets of commands, to
>>> minimize roundtrips.
>> Nice! Will try it out.
>>
>>
>> Have you tried it on windows already? That's where we pay by far the
>> biggest
>> price due to all the unnecessary process creations...
>>
>> It looks like strawberry perl has FFI::Platypus, but not FFI::C.
>> There is
>> perl/vendor/lib/FFI/Platypus/Lang/C.pm, but that just seems like it's
>> documentation. There is however FFI::Platypus::Record, which maybe
>> could
>> suffice?
>>
>> Do we actually need FFI::C, or can we work around not having it?
>> Looks like
>> it's just used for notify related stuff.
>>
>> It looks like mingw doesn't have packages for FFI::Platypus, but it'll
>> probably be a lot easier to build that than when using msvc.
>>
>>
>
> I replaced the use of FFI::C with FFI::Platypus::Record. That comes
> for free with FFI::Platypus, so there would be no extra dependency. It
> means a little extra housekeeping so we don't lose track of the
> pointer for later use with PQfreemem, but it's not too bad.
>
> I have tried it out with Windows, seemed to work OK although the
> xid_wraparound tests 2 and 3 timed out.
>
> Latest is attached.
>
>
>
Here is v12. I removed the XS variant in this version, which makes
things simpler. We can restore it if necessary.
Patch 1 adds the new framework
Patch 2 adapts Cluster.pm to it, as well as handling some instability at
global destruction time that was exacerbated by using FFI::Platypus.
Patch 3 makes improvements in the individual TAP tests using the
framework, including removing every one of the calls to background_psql().
I'm going to add this to the CF and will start testing (again) on Windows.
cheers
andrew
--
Andrew Dunstan
EDB: https://www.enterprisedb.com
Attachments:
[text/x-patch] v12-0001-Add-PostgreSQL-Test-Session-for-libpq-based-TAP-.patch (58.1K, ../[email protected]/2-v12-0001-Add-PostgreSQL-Test-Session-for-libpq-based-TAP-.patch)
download | inline diff:
From a428c5f8e194595cd6a8d8ffca79a9189bc8aa9f Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <[email protected]>
Date: Tue, 2 Jun 2026 11:27:38 -0400
Subject: [PATCH v12 1/3] Add PostgreSQL::Test::Session for libpq-based TAP
test sessions
Introduce a session object for TAP tests that talks to the server
directly through libpq instead of spawning a psql child process for
each background session. Connecting in-process avoids the overhead and
fragility of driving psql over pipes and gives tests synchronous and
asynchronous query interfaces, notice capture, and pipelined queries.
libpq is reached through an FFI::Platypus wrapper, so no compiled XS
module is required. The new modules are:
PostgreSQL::Test::Session - the libpq session object
PostgreSQL::PqFFI - FFI bindings to libpq
PostgreSQL::PqConstants - libpq constants
PostgreSQL::PGTypes - libpq type OID definitions
PostgreSQL::FindLib - locate the libpq shared library
Install the new modules from both the make and meson builds.
---
src/test/perl/Makefile | 10 +
src/test/perl/PostgreSQL/FindLib.pm | 164 ++++
src/test/perl/PostgreSQL/PGTypes.pm | 356 +++++++
src/test/perl/PostgreSQL/PqConstants.pm | 185 ++++
src/test/perl/PostgreSQL/PqFFI.pm | 437 +++++++++
src/test/perl/PostgreSQL/Test/Session.pm | 1075 ++++++++++++++++++++++
src/test/perl/meson.build | 5 +
7 files changed, 2232 insertions(+)
create mode 100644 src/test/perl/PostgreSQL/FindLib.pm
create mode 100644 src/test/perl/PostgreSQL/PGTypes.pm
create mode 100644 src/test/perl/PostgreSQL/PqConstants.pm
create mode 100644 src/test/perl/PostgreSQL/PqFFI.pm
create mode 100644 src/test/perl/PostgreSQL/Test/Session.pm
diff --git a/src/test/perl/Makefile b/src/test/perl/Makefile
index fd4fdaf700b..d234cc59608 100644
--- a/src/test/perl/Makefile
+++ b/src/test/perl/Makefile
@@ -25,9 +25,14 @@ install: all installdirs
$(INSTALL_DATA) $(srcdir)/PostgreSQL/Test/Kerberos.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/Kerberos.pm'
$(INSTALL_DATA) $(srcdir)/PostgreSQL/Test/Cluster.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/Cluster.pm'
$(INSTALL_DATA) $(srcdir)/PostgreSQL/Test/BackgroundPsql.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/BackgroundPsql.pm'
+ $(INSTALL_DATA) $(srcdir)/PostgreSQL/Test/Session.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/Session.pm'
$(INSTALL_DATA) $(srcdir)/PostgreSQL/Test/AdjustDump.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/AdjustDump.pm'
$(INSTALL_DATA) $(srcdir)/PostgreSQL/Test/AdjustUpgrade.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/AdjustUpgrade.pm'
$(INSTALL_DATA) $(srcdir)/PostgreSQL/Version.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Version.pm'
+ $(INSTALL_DATA) $(srcdir)/PostgreSQL/FindLib.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/FindLib.pm'
+ $(INSTALL_DATA) $(srcdir)/PostgreSQL/PqFFI.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/PqFFI.pm'
+ $(INSTALL_DATA) $(srcdir)/PostgreSQL/PqConstants.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/PqConstants.pm'
+ $(INSTALL_DATA) $(srcdir)/PostgreSQL/PGTypes.pm '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/PGTypes.pm'
uninstall:
rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/Utils.pm'
@@ -36,8 +41,13 @@ uninstall:
rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/Kerberos.pm'
rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/Cluster.pm'
rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/BackgroundPsql.pm'
+ rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/Session.pm'
rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/AdjustDump.pm'
rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Test/AdjustUpgrade.pm'
rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/Version.pm'
+ rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/FindLib.pm'
+ rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/PqFFI.pm'
+ rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/PqConstants.pm'
+ rm -f '$(DESTDIR)$(pgxsdir)/$(subdir)/PostgreSQL/PGTypes.pm'
endif
diff --git a/src/test/perl/PostgreSQL/FindLib.pm b/src/test/perl/PostgreSQL/FindLib.pm
new file mode 100644
index 00000000000..b4290c84c4f
--- /dev/null
+++ b/src/test/perl/PostgreSQL/FindLib.pm
@@ -0,0 +1,164 @@
+
+# Copyright (c) 2021-2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+PostgreSQL::FindLib - find shared libraries for PostgreSQL TAP tests
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::FindLib;
+
+ my $libpath = find_lib_or_die(
+ lib => 'pq',
+ libpath => ['/usr/local/pgsql/lib'],
+ );
+
+=head1 DESCRIPTION
+
+This module provides a simple mechanism to locate shared libraries,
+used as a lightweight replacement for C<FFI::CheckLib>. It searches
+for libraries in specified paths and common system locations.
+
+=head1 EXPORTED FUNCTIONS
+
+=over
+
+=item find_lib_or_die(%args)
+
+Searches for a shared library and returns its full path. Dies if the
+library cannot be found.
+
+Arguments:
+
+=over
+
+=item lib => $name
+
+Required. The library name without prefix or suffix (e.g., C<'pq'> for
+C<libpq.so>).
+
+=item libpath => \@paths
+
+Optional. Array of directories to search first.
+
+=item systempath => \@paths
+
+Optional. If set to an empty array C<[]>, system paths will not be searched.
+
+=back
+
+=back
+
+=cut
+
+package PostgreSQL::FindLib;
+
+use strict;
+use warnings FATAL => qw(all);
+
+use Exporter qw(import);
+use File::Spec;
+use Config;
+
+our @EXPORT = qw(find_lib_or_die);
+
+sub find_lib_or_die
+{
+ my %args = @_;
+
+ my $libname = $args{lib} or die "find_lib_or_die: 'lib' argument required";
+ my $libpath = $args{libpath} // [];
+ my $systempath = $args{systempath};
+
+ my @search_paths = @$libpath;
+
+ # Add system paths unless explicitly disabled
+ unless (defined $systempath && ref($systempath) eq 'ARRAY' && @$systempath == 0)
+ {
+ push @search_paths, _get_system_lib_paths();
+ }
+
+ # Determine library file patterns based on OS
+ my @patterns = _get_lib_patterns($libname);
+
+ for my $dir (@search_paths)
+ {
+ next unless -d $dir;
+
+ for my $pattern (@patterns)
+ {
+ my @matches = glob(File::Spec->catfile($dir, $pattern));
+ for my $match (@matches)
+ {
+ return $match if -f $match && -r $match;
+ }
+ }
+ }
+
+ die "find_lib_or_die: unable to find lib$libname in: " . join(", ", @search_paths);
+}
+
+sub _get_lib_patterns
+{
+ my $libname = shift;
+
+ if ($^O eq 'darwin')
+ {
+ return ("lib$libname.dylib", "lib$libname.*.dylib");
+ }
+ elsif ($^O eq 'MSWin32' || $^O eq 'cygwin')
+ {
+ return ("$libname.dll", "lib$libname.dll");
+ }
+ else
+ {
+ # Linux and other Unix-like systems
+ return ("lib$libname.so", "lib$libname.so.*");
+ }
+}
+
+sub _get_system_lib_paths
+{
+ my @paths;
+
+ # Common system library paths
+ push @paths, '/usr/lib', '/usr/local/lib', '/lib';
+
+ # Add architecture-specific paths on Linux
+ if ($^O eq 'linux')
+ {
+ push @paths, '/usr/lib/x86_64-linux-gnu', '/usr/lib/aarch64-linux-gnu';
+ push @paths, '/usr/lib64', '/lib64';
+ }
+
+ # Add paths from LD_LIBRARY_PATH
+ if ($ENV{LD_LIBRARY_PATH})
+ {
+ push @paths, split(/:/, $ENV{LD_LIBRARY_PATH});
+ }
+
+ # macOS specific
+ if ($^O eq 'darwin')
+ {
+ push @paths, '/opt/homebrew/lib', '/usr/local/opt/libpq/lib';
+ if ($ENV{DYLD_LIBRARY_PATH})
+ {
+ push @paths, split(/:/, $ENV{DYLD_LIBRARY_PATH});
+ }
+ }
+
+ return @paths;
+}
+
+=pod
+
+=head1 SEE ALSO
+
+L<PostgreSQL::PqFFI>
+
+=cut
+
+1;
diff --git a/src/test/perl/PostgreSQL/PGTypes.pm b/src/test/perl/PostgreSQL/PGTypes.pm
new file mode 100644
index 00000000000..2bc007c323a
--- /dev/null
+++ b/src/test/perl/PostgreSQL/PGTypes.pm
@@ -0,0 +1,356 @@
+
+# Copyright (c) 2021-2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+PostgreSQL::PGTypes - PostgreSQL backend type OID constants
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::PGTypes;
+
+ if ($type_oid == TEXTOID) { ... }
+
+ if ($type_oid == INT4ARRAYOID) { ... }
+
+=head1 DESCRIPTION
+
+This module provides constants for PostgreSQL backend type OIDs, as defined
+in C<src/include/catalog/pg_type_d.h>. These can be used to identify column
+types in query results via C<PQftype()>.
+
+All constants are exported by default.
+
+=head1 EXPORTED CONSTANTS
+
+=head2 Basic Types
+
+C<BOOLOID>, C<BYTEAOID>, C<CHAROID>, C<NAMEOID>, C<INT8OID>, C<INT2OID>,
+C<INT2VECTOROID>, C<INT4OID>, C<TEXTOID>, C<OIDOID>, C<TIDOID>, C<XIDOID>,
+C<CIDOID>, C<OIDVECTOROID>, C<JSONOID>, C<XMLOID>, C<XID8OID>, C<POINTOID>,
+C<LSEGOID>, C<PATHOID>, C<BOXOID>, C<POLYGONOID>, C<LINEOID>, C<FLOAT4OID>,
+C<FLOAT8OID>, C<UNKNOWNOID>, C<CIRCLEOID>, C<MONEYOID>, C<MACADDROID>,
+C<INETOID>, C<CIDROID>, C<MACADDR8OID>, C<ACLITEMOID>, C<BPCHAROID>,
+C<VARCHAROID>, C<DATEOID>, C<TIMEOID>, C<TIMESTAMPOID>, C<TIMESTAMPTZOID>,
+C<INTERVALOID>, C<TIMETZOID>, C<BITOID>, C<VARBITOID>, C<NUMERICOID>,
+C<REFCURSOROID>, C<UUIDOID>, C<TSVECTOROID>, C<GTSVECTOROID>, C<TSQUERYOID>,
+C<JSONBOID>, C<JSONPATHOID>, C<TXID_SNAPSHOTOID>
+
+=head2 Range Types
+
+C<INT4RANGEOID>, C<NUMRANGEOID>, C<TSRANGEOID>, C<TSTZRANGEOID>,
+C<DATERANGEOID>, C<INT8RANGEOID>
+
+=head2 Multirange Types
+
+C<INT4MULTIRANGEOID>, C<NUMMULTIRANGEOID>, C<TSMULTIRANGEOID>,
+C<TSTZMULTIRANGEOID>, C<DATEMULTIRANGEOID>, C<INT8MULTIRANGEOID>
+
+=head2 Pseudo Types
+
+C<RECORDOID>, C<RECORDARRAYOID>, C<CSTRINGOID>, C<VOIDOID>, C<TRIGGEROID>,
+C<EVENT_TRIGGEROID>
+
+=head2 Array Types
+
+Array type OIDs follow the pattern C<{BASENAME}ARRAYOID>, e.g., C<TEXTARRAYOID>,
+C<INT4ARRAYOID>, C<JSONBARRAYOID>.
+
+=cut
+
+package PostgreSQL::PGTypes;
+
+use strict;
+use warnings FATAL => qw(all);
+
+use Exporter qw(import);
+
+our @EXPORT = qw(
+
+ BOOLOID
+ BYTEAOID
+ CHAROID
+ NAMEOID
+ INT8OID
+ INT2OID
+ INT2VECTOROID
+ INT4OID
+ TEXTOID
+ OIDOID
+ TIDOID
+ XIDOID
+ CIDOID
+ OIDVECTOROID
+ JSONOID
+ XMLOID
+ XID8OID
+ POINTOID
+ LSEGOID
+ PATHOID
+ BOXOID
+ POLYGONOID
+ LINEOID
+ FLOAT4OID
+ FLOAT8OID
+ UNKNOWNOID
+ CIRCLEOID
+ MONEYOID
+ MACADDROID
+ INETOID
+ CIDROID
+ MACADDR8OID
+ ACLITEMOID
+ BPCHAROID
+ VARCHAROID
+ DATEOID
+ TIMEOID
+ TIMESTAMPOID
+ TIMESTAMPTZOID
+ INTERVALOID
+ TIMETZOID
+ BITOID
+ VARBITOID
+ NUMERICOID
+ REFCURSOROID
+ UUIDOID
+ TSVECTOROID
+ GTSVECTOROID
+ TSQUERYOID
+ JSONBOID
+ JSONPATHOID
+ TXID_SNAPSHOTOID
+ INT4RANGEOID
+ NUMRANGEOID
+ TSRANGEOID
+ TSTZRANGEOID
+ DATERANGEOID
+ INT8RANGEOID
+ INT4MULTIRANGEOID
+ NUMMULTIRANGEOID
+ TSMULTIRANGEOID
+ TSTZMULTIRANGEOID
+ DATEMULTIRANGEOID
+ INT8MULTIRANGEOID
+ RECORDOID
+ RECORDARRAYOID
+ CSTRINGOID
+ VOIDOID
+ TRIGGEROID
+ EVENT_TRIGGEROID
+
+ BOOLARRAYOID
+ BYTEAARRAYOID
+ CHARARRAYOID
+ NAMEARRAYOID
+ INT8ARRAYOID
+ INT2ARRAYOID
+ INT2VECTORARRAYOID
+ INT4ARRAYOID
+ TEXTARRAYOID
+ OIDARRAYOID
+ TIDARRAYOID
+ XIDARRAYOID
+ CIDARRAYOID
+ OIDVECTORARRAYOID
+ JSONARRAYOID
+ XMLARRAYOID
+ XID8ARRAYOID
+ POINTARRAYOID
+ LSEGARRAYOID
+ PATHARRAYOID
+ BOXARRAYOID
+ POLYGONARRAYOID
+ LINEARRAYOID
+ FLOAT4ARRAYOID
+ FLOAT8ARRAYOID
+ CIRCLEARRAYOID
+ MONEYARRAYOID
+ MACADDRARRAYOID
+ INETARRAYOID
+ CIDRARRAYOID
+ MACADDR8ARRAYOID
+ ACLITEMARRAYOID
+ BPCHARARRAYOID
+ VARCHARARRAYOID
+ DATEARRAYOID
+ TIMEARRAYOID
+ TIMESTAMPARRAYOID
+ TIMESTAMPTZARRAYOID
+ INTERVALARRAYOID
+ TIMETZARRAYOID
+ BITARRAYOID
+ VARBITARRAYOID
+ NUMERICARRAYOID
+ REFCURSORARRAYOID
+ UUIDARRAYOID
+ TSVECTORARRAYOID
+ GTSVECTORARRAYOID
+ TSQUERYARRAYOID
+ JSONBARRAYOID
+ JSONPATHARRAYOID
+ TXID_SNAPSHOTARRAYOID
+ INT4RANGEARRAYOID
+ NUMRANGEARRAYOID
+ TSRANGEARRAYOID
+ TSTZRANGEARRAYOID
+ DATERANGEARRAYOID
+ INT8RANGEARRAYOID
+ INT4MULTIRANGEARRAYOID
+ NUMMULTIRANGEARRAYOID
+ TSMULTIRANGEARRAYOID
+ TSTZMULTIRANGEARRAYOID
+ DATEMULTIRANGEARRAYOID
+ INT8MULTIRANGEARRAYOID
+ CSTRINGARRAYOID
+
+);
+
+use constant {
+ BOOLOID => 16,
+ BYTEAOID => 17,
+ CHAROID => 18,
+ NAMEOID => 19,
+ INT8OID => 20,
+ INT2OID => 21,
+ INT2VECTOROID => 22,
+ INT4OID => 23,
+ TEXTOID => 25,
+ OIDOID => 26,
+ TIDOID => 27,
+ XIDOID => 28,
+ CIDOID => 29,
+ OIDVECTOROID => 30,
+ JSONOID => 114,
+ XMLOID => 142,
+ XID8OID => 5069,
+ POINTOID => 600,
+ LSEGOID => 601,
+ PATHOID => 602,
+ BOXOID => 603,
+ POLYGONOID => 604,
+ LINEOID => 628,
+ FLOAT4OID => 700,
+ FLOAT8OID => 701,
+ UNKNOWNOID => 705,
+ CIRCLEOID => 718,
+ MONEYOID => 790,
+ MACADDROID => 829,
+ INETOID => 869,
+ CIDROID => 650,
+ MACADDR8OID => 774,
+ ACLITEMOID => 1033,
+ BPCHAROID => 1042,
+ VARCHAROID => 1043,
+ DATEOID => 1082,
+ TIMEOID => 1083,
+ TIMESTAMPOID => 1114,
+ TIMESTAMPTZOID => 1184,
+ INTERVALOID => 1186,
+ TIMETZOID => 1266,
+ BITOID => 1560,
+ VARBITOID => 1562,
+ NUMERICOID => 1700,
+ REFCURSOROID => 1790,
+ UUIDOID => 2950,
+ TSVECTOROID => 3614,
+ GTSVECTOROID => 3642,
+ TSQUERYOID => 3615,
+ JSONBOID => 3802,
+ JSONPATHOID => 4072,
+ TXID_SNAPSHOTOID => 2970,
+ INT4RANGEOID => 3904,
+ NUMRANGEOID => 3906,
+ TSRANGEOID => 3908,
+ TSTZRANGEOID => 3910,
+ DATERANGEOID => 3912,
+ INT8RANGEOID => 3926,
+ INT4MULTIRANGEOID => 4451,
+ NUMMULTIRANGEOID => 4532,
+ TSMULTIRANGEOID => 4533,
+ TSTZMULTIRANGEOID => 4534,
+ DATEMULTIRANGEOID => 4535,
+ INT8MULTIRANGEOID => 4536,
+ RECORDOID => 2249,
+ RECORDARRAYOID => 2287,
+ CSTRINGOID => 2275,
+ VOIDOID => 2278,
+ TRIGGEROID => 2279,
+ EVENT_TRIGGEROID => 3838,
+
+ BOOLARRAYOID => 1000,
+ BYTEAARRAYOID => 1001,
+ CHARARRAYOID => 1002,
+ NAMEARRAYOID => 1003,
+ INT8ARRAYOID => 1016,
+ INT2ARRAYOID => 1005,
+ INT2VECTORARRAYOID => 1006,
+ INT4ARRAYOID => 1007,
+ TEXTARRAYOID => 1009,
+ OIDARRAYOID => 1028,
+ TIDARRAYOID => 1010,
+ XIDARRAYOID => 1011,
+ CIDARRAYOID => 1012,
+ OIDVECTORARRAYOID => 1013,
+ JSONARRAYOID => 199,
+ XMLARRAYOID => 143,
+ XID8ARRAYOID => 271,
+ POINTARRAYOID => 1017,
+ LSEGARRAYOID => 1018,
+ PATHARRAYOID => 1019,
+ BOXARRAYOID => 1020,
+ POLYGONARRAYOID => 1027,
+ LINEARRAYOID => 629,
+ FLOAT4ARRAYOID => 1021,
+ FLOAT8ARRAYOID => 1022,
+ CIRCLEARRAYOID => 719,
+ MONEYARRAYOID => 791,
+ MACADDRARRAYOID => 1040,
+ INETARRAYOID => 1041,
+ CIDRARRAYOID => 651,
+ MACADDR8ARRAYOID => 775,
+ ACLITEMARRAYOID => 1034,
+ BPCHARARRAYOID => 1014,
+ VARCHARARRAYOID => 1015,
+ DATEARRAYOID => 1182,
+ TIMEARRAYOID => 1183,
+ TIMESTAMPARRAYOID => 1115,
+ TIMESTAMPTZARRAYOID => 1185,
+ INTERVALARRAYOID => 1187,
+ TIMETZARRAYOID => 1270,
+ BITARRAYOID => 1561,
+ VARBITARRAYOID => 1563,
+ NUMERICARRAYOID => 1231,
+ REFCURSORARRAYOID => 2201,
+ UUIDARRAYOID => 2951,
+ TSVECTORARRAYOID => 3643,
+ GTSVECTORARRAYOID => 3644,
+ TSQUERYARRAYOID => 3645,
+ JSONBARRAYOID => 3807,
+ JSONPATHARRAYOID => 4073,
+ TXID_SNAPSHOTARRAYOID => 2949,
+ INT4RANGEARRAYOID => 3905,
+ NUMRANGEARRAYOID => 3907,
+ TSRANGEARRAYOID => 3909,
+ TSTZRANGEARRAYOID => 3911,
+ DATERANGEARRAYOID => 3913,
+ INT8RANGEARRAYOID => 3927,
+ INT4MULTIRANGEARRAYOID => 6150,
+ NUMMULTIRANGEARRAYOID => 6151,
+ TSMULTIRANGEARRAYOID => 6152,
+ TSTZMULTIRANGEARRAYOID => 6153,
+ DATEMULTIRANGEARRAYOID => 6155,
+ INT8MULTIRANGEARRAYOID => 6157,
+ CSTRINGARRAYOID => 1263,
+};
+
+=pod
+
+=head1 SEE ALSO
+
+L<PostgreSQL::PqFFI>, L<PostgreSQL::Test::Session>
+
+=cut
+
+1;
diff --git a/src/test/perl/PostgreSQL/PqConstants.pm b/src/test/perl/PostgreSQL/PqConstants.pm
new file mode 100644
index 00000000000..f81913b580a
--- /dev/null
+++ b/src/test/perl/PostgreSQL/PqConstants.pm
@@ -0,0 +1,185 @@
+
+# Copyright (c) 2021-2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+PostgreSQL::PqConstants - libpq constants for PostgreSQL TAP tests
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::PqConstants;
+
+ if (PQstatus($conn) == CONNECTION_OK) { ... }
+
+ if (PQresultStatus($result) == PGRES_TUPLES_OK) { ... }
+
+=head1 DESCRIPTION
+
+This module provides libpq constants used by the FFI backend
+(C<PostgreSQL::PqFFI>). All constants are exported by default.
+
+=head1 EXPORTED CONSTANTS
+
+=head2 Connection Status (ConnStatusType)
+
+C<CONNECTION_OK>, C<CONNECTION_BAD>, C<CONNECTION_STARTED>,
+C<CONNECTION_MADE>, C<CONNECTION_AWAITING_RESPONSE>, C<CONNECTION_AUTH_OK>,
+C<CONNECTION_SETENV>, C<CONNECTION_SSL_STARTUP>, C<CONNECTION_NEEDED>,
+C<CONNECTION_CHECK_WRITABLE>, C<CONNECTION_CONSUME>, C<CONNECTION_GSS_STARTUP>,
+C<CONNECTION_CHECK_TARGET>, C<CONNECTION_CHECK_STANDBY>, C<CONNECTION_ALLOCATED>
+
+=head2 Execution Status (ExecStatusType)
+
+C<PGRES_EMPTY_QUERY>, C<PGRES_COMMAND_OK>, C<PGRES_TUPLES_OK>,
+C<PGRES_COPY_OUT>, C<PGRES_COPY_IN>, C<PGRES_BAD_RESPONSE>,
+C<PGRES_NONFATAL_ERROR>, C<PGRES_FATAL_ERROR>, C<PGRES_COPY_BOTH>,
+C<PGRES_SINGLE_TUPLE>, C<PGRES_PIPELINE_SYNC>, C<PGRES_PIPELINE_ABORTED>,
+C<PGRES_TUPLES_CHUNK>
+
+=head2 Polling Status (PostgresPollingStatusType)
+
+C<PGRES_POLLING_FAILED>, C<PGRES_POLLING_READING>, C<PGRES_POLLING_WRITING>,
+C<PGRES_POLLING_OK>, C<PGRES_POLLING_ACTIVE>
+
+=head2 Ping Status (PGPing)
+
+C<PQPING_OK>, C<PQPING_REJECT>, C<PQPING_NO_RESPONSE>, C<PQPING_NO_ATTEMPT>
+
+=head2 Transaction Status (PGTransactionStatusType)
+
+C<PQTRANS_IDLE>, C<PQTRANS_ACTIVE>, C<PQTRANS_INTRANS>, C<PQTRANS_INERROR>,
+C<PQTRANS_UNKNOWN>
+
+=cut
+
+package PostgreSQL::PqConstants;
+
+use strict;
+use warnings FATAL => qw(all);
+
+use Exporter qw(import);
+
+our @EXPORT = qw(
+
+ CONNECTION_OK
+ CONNECTION_BAD
+ CONNECTION_STARTED
+ CONNECTION_MADE
+ CONNECTION_AWAITING_RESPONSE
+ CONNECTION_AUTH_OK
+ CONNECTION_SETENV
+ CONNECTION_SSL_STARTUP
+ CONNECTION_NEEDED
+ CONNECTION_CHECK_WRITABLE
+ CONNECTION_CONSUME
+ CONNECTION_GSS_STARTUP
+ CONNECTION_CHECK_TARGET
+ CONNECTION_CHECK_STANDBY
+ CONNECTION_ALLOCATED
+
+ PGRES_EMPTY_QUERY
+ PGRES_COMMAND_OK
+ PGRES_TUPLES_OK
+ PGRES_COPY_OUT
+ PGRES_COPY_IN
+ PGRES_BAD_RESPONSE
+ PGRES_NONFATAL_ERROR
+ PGRES_FATAL_ERROR
+ PGRES_COPY_BOTH
+ PGRES_SINGLE_TUPLE
+ PGRES_PIPELINE_SYNC
+ PGRES_PIPELINE_ABORTED
+ PGRES_TUPLES_CHUNK
+
+ PGRES_POLLING_FAILED
+ PGRES_POLLING_READING
+ PGRES_POLLING_WRITING
+ PGRES_POLLING_OK
+ PGRES_POLLING_ACTIVE
+
+ PQPING_OK
+ PQPING_REJECT
+ PQPING_NO_RESPONSE
+ PQPING_NO_ATTEMPT
+
+ PQTRANS_IDLE
+ PQTRANS_ACTIVE
+ PQTRANS_INTRANS
+ PQTRANS_INERROR
+ PQTRANS_UNKNOWN
+
+);
+
+# ConnStatusType:
+use constant {
+ CONNECTION_OK => 0,
+ CONNECTION_BAD => 1,
+ CONNECTION_STARTED => 2,
+ CONNECTION_MADE => 3,
+ CONNECTION_AWAITING_RESPONSE => 4,
+ CONNECTION_AUTH_OK => 5,
+ CONNECTION_SETENV => 6,
+ CONNECTION_SSL_STARTUP => 7,
+ CONNECTION_NEEDED => 8,
+ CONNECTION_CHECK_WRITABLE => 9,
+ CONNECTION_CONSUME => 10,
+ CONNECTION_GSS_STARTUP => 11,
+ CONNECTION_CHECK_TARGET => 12,
+ CONNECTION_CHECK_STANDBY => 13,
+ CONNECTION_ALLOCATED => 14,
+};
+
+# ExecStatusType:
+use constant {
+ PGRES_EMPTY_QUERY => 0,
+ PGRES_COMMAND_OK => 1,
+ PGRES_TUPLES_OK => 2,
+ PGRES_COPY_OUT => 3,
+ PGRES_COPY_IN => 4,
+ PGRES_BAD_RESPONSE => 5,
+ PGRES_NONFATAL_ERROR => 6,
+ PGRES_FATAL_ERROR => 7,
+ PGRES_COPY_BOTH => 8,
+ PGRES_SINGLE_TUPLE => 9,
+ PGRES_PIPELINE_SYNC => 10,
+ PGRES_PIPELINE_ABORTED => 11,
+ PGRES_TUPLES_CHUNK => 12,
+};
+
+# PostgresPollingStatusType:
+use constant {
+ PGRES_POLLING_FAILED => 0,
+ PGRES_POLLING_READING => 1,
+ PGRES_POLLING_WRITING => 2,
+ PGRES_POLLING_OK => 3,
+ PGRES_POLLING_ACTIVE => 4,
+};
+
+# PGPing:
+use constant {
+ PQPING_OK => 0,
+ PQPING_REJECT => 1,
+ PQPING_NO_RESPONSE => 2,
+ PQPING_NO_ATTEMPT => 3,
+};
+
+# PGTransactionStatusType:
+use constant {
+ PQTRANS_IDLE => 0,
+ PQTRANS_ACTIVE => 1,
+ PQTRANS_INTRANS => 2,
+ PQTRANS_INERROR => 3,
+ PQTRANS_UNKNOWN => 4,
+};
+
+=pod
+
+=head1 SEE ALSO
+
+L<PostgreSQL::PqFFI>, L<PostgreSQL::Test::Session>
+
+=cut
+
+1;
diff --git a/src/test/perl/PostgreSQL/PqFFI.pm b/src/test/perl/PostgreSQL/PqFFI.pm
new file mode 100644
index 00000000000..f16470c46d9
--- /dev/null
+++ b/src/test/perl/PostgreSQL/PqFFI.pm
@@ -0,0 +1,437 @@
+
+# Copyright (c) 2021-2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+PostgreSQL::PqFFI - FFI wrapper for libpq
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::PqFFI;
+
+ # Initialize the FFI bindings (required before use)
+ PostgreSQL::PqFFI::setup($libdir);
+
+ # Connect to database
+ my $conn = PQconnectdb("dbname=postgres");
+ die PQerrorMessage($conn) unless PQstatus($conn) == CONNECTION_OK;
+
+ # Execute query
+ my $result = PQexec($conn, "SELECT 1");
+ if (PQresultStatus($result) == PGRES_TUPLES_OK) {
+ print PQgetvalue($result, 0, 0), "\n";
+ }
+ PQclear($result);
+
+ PQfinish($conn);
+
+=head1 DESCRIPTION
+
+This module provides Perl bindings to libpq using L<FFI::Platypus>.
+It is the backend used by L<PostgreSQL::Test::Session>.
+
+The module must be initialized by calling C<setup()> before any libpq
+functions can be used.
+
+=head1 FUNCTIONS
+
+=head2 setup($libdir [, $use_system_path])
+
+Initialize the FFI bindings. C<$libdir> specifies where to find libpq.
+If C<$use_system_path> is false, only C<$libdir> is searched.
+
+=head2 libpq Functions
+
+All standard libpq functions are exported. See the PostgreSQL libpq
+documentation for details. Commonly used functions include:
+
+B<Connection:> C<PQconnectdb>, C<PQconnectStart>, C<PQconnectPoll>,
+C<PQfinish>, C<PQstatus>, C<PQerrorMessage>
+
+B<Execution:> C<PQexec>, C<PQexecParams>, C<PQprepare>, C<PQexecPrepared>,
+C<PQsendQuery>, C<PQgetResult>, C<PQclear>
+
+B<Results:> C<PQresultStatus>, C<PQntuples>, C<PQnfields>, C<PQfname>,
+C<PQftype>, C<PQgetvalue>, C<PQgetisnull>
+
+B<Pipeline:> C<PQenterPipelineMode>, C<PQexitPipelineMode>,
+C<PQpipelineSync>, C<PQsendQueryParams>
+
+B<Notifications:> C<PQnotifies>, C<PQnotify_channel>, C<PQnotify_payload>,
+C<PQnotify_be_pid>, C<PQnotify_free>
+
+B<Notice Processing:> C<PQsetNoticeProcessor>, C<create_notice_processor>
+
+=head2 create_notice_processor($callback)
+
+Creates a notice processor closure that can be passed to
+C<PQsetNoticeProcessor>. The callback receives C<($arg, $message)>.
+The caller must keep a reference to the returned closure to prevent
+garbage collection.
+
+=head1 EXPORTED CONSTANTS
+
+This module re-exports all constants from L<PostgreSQL::PqConstants>
+and L<PostgreSQL::PGTypes>.
+
+=cut
+
+package PostgreSQL::PqFFI;
+
+use strict;
+use warnings FATAL => qw(all);
+
+use FFI::Platypus;
+use FFI::Platypus::Record;
+use PostgreSQL::FindLib;
+
+# PGnotify struct for notification support
+# typedef struct pgNotify {
+# char *relname; /* notification channel name */
+# int be_pid; /* process ID of notifying server process */
+# char *extra; /* notification payload string */
+# } PGnotify;
+package PGnotify {
+ use FFI::Platypus::Record;
+ record_layout_1(
+ 'opaque' => 'relname',
+ 'sint32' => 'be_pid',
+ 'opaque' => 'extra',
+ );
+}
+package PostgreSQL::PqFFI;
+use PostgreSQL::PqConstants;
+use PostgreSQL::PGTypes;
+
+use Exporter qw(import);
+
+our @EXPORT = (
+ @PostgreSQL::PqConstants::EXPORT,
+ @PostgreSQL::PGTypes::EXPORT,
+);
+
+
+
+my @procs = qw(
+
+ PQnotifies
+ PQfreemem
+ PQnotify_channel
+ PQnotify_payload
+ PQnotify_be_pid
+ PQnotify_free
+
+ PQconnectdb
+ PQconnectdbParams
+ PQsetdbLogin
+ PQfinish
+ PQreset
+ PQdb
+ PQuser
+ PQpass
+ PQhost
+ PQhostaddr
+ PQport
+ PQtty
+ PQoptions
+ PQstatus
+ PQtransactionStatus
+ PQparameterStatus
+ PQping
+ PQpingParams
+
+ PQexec
+ PQexecParams
+ PQprepare
+ PQexecPrepared
+
+ PQdescribePrepared
+ PQdescribePortal
+
+ PQclosePrepared
+ PQclosePortal
+ PQclear
+
+ PQsendQuery
+ PQgetResult
+ PQisBusy
+ PQconsumeInput
+
+ PQprotocolVersion
+ PQserverVersion
+ PQerrorMessage
+ PQsocket
+ PQsocketPoll
+ PQgetCurrentTimeUSec
+ PQbackendPID
+ PQconnectionNeedsPassword
+ PQconnectionUsedPassword
+ PQconnectionUsedGSSAPI
+ PQclientEncoding
+ PQsetClientEncoding
+
+ PQresultStatus
+ PQresStatus
+ PQresultErrorMessage
+ PQresultErrorField
+ PQntuples
+ PQnfields
+ PQbinaryTuples
+ PQfname
+ PQfnumber
+ PQftable
+ PQftablecol
+ PQfformat
+ PQftype
+ PQfsize
+ PQfmod
+ PQcmdStatus
+ PQoidValue
+ PQcmdTuples
+ PQgetvalue
+ PQgetlength
+ PQgetisnull
+ PQnparams
+ PQparamtype
+ PQchangePassword
+
+ PQpipelineStatus
+ PQenterPipelineMode
+ PQexitPipelineMode
+ PQpipelineSync
+ PQsendFlushRequest
+ PQsendPipelineSync
+ PQsendQueryParams
+
+ PQsetnonblocking
+ PQisnonblocking
+ PQflush
+
+ PQconnectStart
+ PQconnectStartParams
+ PQconnectPoll
+
+ PQsetNoticeProcessor
+ create_notice_processor
+
+);
+
+push(@EXPORT, @procs);
+
+sub setup
+{
+ my $libdir = shift;
+ my $use_system_path = shift;
+
+ my $ffi = FFI::Platypus->new(api => 1);
+
+ my @system_path;
+ @system_path = (systempath => []) unless $use_system_path;
+
+ $ffi->type('opaque' => 'PGconn');
+ $ffi->type('opaque' => 'PGresult');
+ $ffi->type('uint32' => 'Oid');
+ $ffi->type('int' => 'ExecStatusType');
+
+ # Register the PGnotify record type for struct access
+ $ffi->type('record(PGnotify)' => 'PGnotify_record');
+
+ my $lib = find_lib_or_die(
+ lib => 'pq',
+ libpath => [$libdir],
+ @system_path,
+ );
+ $ffi->lib($lib);
+
+ $ffi->attach('PQconnectdb' => ['string'] => 'PGconn');
+ $ffi->attach(
+ 'PQconnectdbParams' => [ 'string[]', 'string[]', 'int' ] => 'PGconn');
+ $ffi->attach(
+ 'PQsetdbLogin' => [
+ 'string', 'string', 'string', 'string',
+ 'string', 'string', 'string',
+ ] => 'PGconn');
+ $ffi->attach('PQfinish' => ['PGconn'] => 'void');
+ $ffi->attach('PQreset' => ['PGconn'] => 'void');
+ $ffi->attach('PQdb' => ['PGconn'] => 'string');
+ $ffi->attach('PQuser' => ['PGconn'] => 'string');
+ $ffi->attach('PQpass' => ['PGconn'] => 'string');
+ $ffi->attach('PQhost' => ['PGconn'] => 'string');
+ $ffi->attach('PQhostaddr' => ['PGconn'] => 'string');
+ $ffi->attach('PQport' => ['PGconn'] => 'string');
+ $ffi->attach('PQtty' => ['PGconn'] => 'string');
+ $ffi->attach('PQoptions' => ['PGconn'] => 'string');
+ $ffi->attach('PQstatus' => ['PGconn'] => 'int');
+ $ffi->attach('PQtransactionStatus' => ['PGconn'] => 'int');
+ $ffi->attach('PQparameterStatus' => [ 'PGconn', 'string' ] => 'string');
+ $ffi->attach('PQping' => ['string'] => 'int');
+ $ffi->attach(
+ 'PQpingParams' => [ 'string[]', 'string[]', 'int' ] => 'int');
+
+ $ffi->attach('PQprotocolVersion' => ['PGconn'] => 'int');
+ $ffi->attach('PQserverVersion' => ['PGconn'] => 'int');
+ $ffi->attach('PQerrorMessage' => ['PGconn'] => 'string');
+ $ffi->attach('PQsocket' => ['PGconn'] => 'int');
+ $ffi->attach('PQsocketPoll' => ['int', 'int', 'int', 'sint64'] => 'int');
+ $ffi->attach('PQgetCurrentTimeUSec' => [] => 'sint64');
+ $ffi->attach('PQbackendPID' => ['PGconn'] => 'int');
+ $ffi->attach('PQconnectionNeedsPassword' => ['PGconn'] => 'int');
+ $ffi->attach('PQconnectionUsedPassword' => ['PGconn'] => 'int');
+ $ffi->attach('PQconnectionUsedGSSAPI' => ['PGconn'] => 'int');
+ $ffi->attach('PQclientEncoding' => ['PGconn'] => 'int');
+ $ffi->attach('PQsetClientEncoding' => [ 'PGconn', 'string' ] => 'int');
+
+ $ffi->attach('PQexec' => [ 'PGconn', 'string' ] => 'PGresult');
+ $ffi->attach(
+ 'PQexecParams' => [
+ 'PGconn', 'string', 'int', 'int[]',
+ 'string[]', 'int[]', 'int[]', 'int'
+ ] => 'PGresult');
+ $ffi->attach(
+ 'PQprepare' => [ 'PGconn', 'string', 'string', 'int', 'int[]' ] =>
+ 'PGresult');
+ $ffi->attach(
+ 'PQexecPrepared' => [ 'PGconn', 'string', 'int',
+ 'string[]', 'int[]', 'int[]', 'int' ] => 'PGresult');
+
+ $ffi->attach('PQresultStatus' => ['PGresult'] => 'ExecStatusType');
+ $ffi->attach('PQresStatus' => ['ExecStatusType'] => 'string');
+ $ffi->attach('PQresultErrorMessage' => ['PGresult'] => 'string');
+ $ffi->attach('PQresultErrorField' => [ 'PGresult', 'int' ] => 'string');
+ $ffi->attach('PQntuples' => ['PGresult'] => 'int');
+ $ffi->attach('PQnfields' => ['PGresult'] => 'int');
+ $ffi->attach('PQbinaryTuples' => ['PGresult'] => 'int');
+ $ffi->attach('PQfname' => [ 'PGresult', 'int' ] => 'string');
+ $ffi->attach('PQfnumber' => [ 'PGresult', 'string' ] => 'int');
+ $ffi->attach('PQftable' => [ 'PGresult', 'int' ] => 'Oid');
+ $ffi->attach('PQftablecol' => [ 'PGresult', 'int' ] => 'int');
+ $ffi->attach('PQfformat' => [ 'PGresult', 'int' ] => 'int');
+ $ffi->attach('PQftype' => [ 'PGresult', 'int' ] => 'Oid');
+ $ffi->attach('PQfsize' => [ 'PGresult', 'int' ] => 'int');
+ $ffi->attach('PQfmod' => [ 'PGresult', 'int' ] => 'int');
+ $ffi->attach('PQcmdStatus' => ['PGresult'] => 'string');
+ $ffi->attach('PQoidValue' => ['PGresult'] => 'Oid');
+ $ffi->attach('PQcmdTuples' => ['PGresult'] => 'string');
+ $ffi->attach('PQgetvalue' => [ 'PGresult', 'int', 'int' ] => 'string');
+ $ffi->attach('PQgetlength' => [ 'PGresult', 'int', 'int' ] => 'int');
+ $ffi->attach('PQgetisnull' => [ 'PGresult', 'int', 'int' ] => 'int');
+ $ffi->attach('PQnparams' => ['PGresult'] => 'int');
+ $ffi->attach('PQparamtype' => [ 'PGresult', 'int' ] => 'Oid');
+
+
+ $ffi->attach(
+ 'PQdescribePrepared' => [ 'PGconn', 'string' ] => 'PGresult');
+ $ffi->attach('PQdescribePortal' => [ 'PGconn', 'string' ] => 'PGresult');
+
+ $ffi->attach('PQclosePrepared' => [ 'PGconn', 'string' ] => 'PGresult');
+ $ffi->attach('PQclosePortal' => [ 'PGconn', 'string' ] => 'PGresult');
+ $ffi->attach('PQclear' => ['PGresult'] => 'void');
+
+ $ffi->attach('PQconnectStart' => [ 'string' ] => 'PGconn');
+ $ffi->attach(
+ 'PQconnectStartParams' => [ 'string[]', 'string[]', 'int' ] => 'PGconn');
+ $ffi->attach('PQconnectPoll' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQresetStart' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQresetPoll' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQsendQuery' => [ 'PGconn', 'string' ] => 'int');
+ $ffi->attach('PQsendQueryParams' => [
+ 'PGconn', 'string', 'int', 'Oid*', 'string*',
+ 'int*', 'int*', 'int' ] => 'int');
+ $ffi->attach('PQsendPrepare' => [ 'PGconn', 'string', 'string', 'int', 'Oid[]' ] => 'int');
+ $ffi->attach('PQgetResult' => [ 'PGconn' ] => 'PGresult');
+
+ $ffi->attach('PQisBusy' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQconsumeInput' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQchangePassword' => [ 'PGconn', 'string', 'string' ] => 'PGresult');
+
+ $ffi->attach('PQpipelineStatus' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQenterPipelineMode' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQexitPipelineMode' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQpipelineSync' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQsendFlushRequest' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQsendPipelineSync' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQsetnonblocking' => [ 'PGconn', 'int' ] => 'int');
+ $ffi->attach('PQisnonblocking' => [ 'PGconn' ] => 'int');
+ $ffi->attach('PQflush' => [ 'PGconn' ] => 'int');
+
+ # Notification support - PQnotifies returns a pointer to PGnotify struct
+ # We return opaque to preserve the original pointer for freeing with PQfreemem
+ $ffi->attach('PQnotifies' => [ 'PGconn' ] => 'opaque');
+ $ffi->attach('PQfreemem' => [ 'opaque' ] => 'void');
+
+ # Notice processor callback support
+ # typedef void (*PQnoticeProcessor)(void *arg, const char *message);
+ $ffi->type('(opaque,string)->void' => 'PQnoticeProcessor');
+ $ffi->attach('PQsetNoticeProcessor' => [ 'PGconn', 'PQnoticeProcessor', 'opaque' ] => 'opaque');
+
+ # Store the $ffi instance for use by helper functions
+ $PostgreSQL::PqFFI::_ffi = $ffi;
+}
+
+# Helper functions to extract values from PGnotify struct
+# The opaque pointer is cast to the record type to access fields
+
+sub _cast_to_pgnotify
+{
+ my $ptr = shift;
+ return undef unless $ptr;
+ return $PostgreSQL::PqFFI::_ffi->cast('opaque', 'record(PGnotify)*', $ptr);
+}
+
+sub PQnotify_channel
+{
+ my $ptr = shift;
+ return undef unless $ptr;
+ my $notify = _cast_to_pgnotify($ptr);
+ my $str_ptr = $notify->relname;
+ return undef unless $str_ptr;
+ return $PostgreSQL::PqFFI::_ffi->cast('opaque', 'string', $str_ptr);
+}
+
+sub PQnotify_payload
+{
+ my $ptr = shift;
+ return undef unless $ptr;
+ my $notify = _cast_to_pgnotify($ptr);
+ my $str_ptr = $notify->extra;
+ return undef unless $str_ptr;
+ return $PostgreSQL::PqFFI::_ffi->cast('opaque', 'string', $str_ptr);
+}
+
+sub PQnotify_be_pid
+{
+ my $ptr = shift;
+ return undef unless $ptr;
+ my $notify = _cast_to_pgnotify($ptr);
+ return $notify->be_pid;
+}
+
+# Free a PGnotify struct using the original pointer
+sub PQnotify_free
+{
+ my $ptr = shift;
+ return unless $ptr;
+ PQfreemem($ptr);
+}
+
+# Create a notice processor closure that can be passed to PQsetNoticeProcessor.
+# The callback will be invoked with (arg, message) where arg is opaque and message is string.
+# Returns the closure - caller must keep a reference to prevent garbage collection.
+sub create_notice_processor
+{
+ my $callback = shift; # sub { my ($arg, $message) = @_; ... }
+ return $PostgreSQL::PqFFI::_ffi->closure($callback);
+}
+
+=pod
+
+=head1 SEE ALSO
+
+L<PostgreSQL::Test::Session>,
+L<PostgreSQL::PqConstants>, L<PostgreSQL::PGTypes>, L<FFI::Platypus>
+
+=cut
+
+1;
diff --git a/src/test/perl/PostgreSQL/Test/Session.pm b/src/test/perl/PostgreSQL/Test/Session.pm
new file mode 100644
index 00000000000..5a674bd2371
--- /dev/null
+++ b/src/test/perl/PostgreSQL/Test/Session.pm
@@ -0,0 +1,1075 @@
+
+# Copyright (c) 2021-2026, PostgreSQL Global Development Group
+
+=pod
+
+=head1 NAME
+
+PostgreSQL::Test::Session - class for a PostgreSQL libpq session
+
+=head1 SYNOPSIS
+
+ use PostgreSQL::Test::Session;
+
+ use PostgreSQL::Test::Cluster;
+
+ my $node = PostgreSQL::Test::Cluster->new('mynode');
+
+ # create a new session. defult dbname is 'postgres'
+ my $session = PostgreSQL::Test::Session->new(node => $node
+ [, dbname => $dbname] );
+
+ # close the session
+ $session->close;
+
+ # reopen the session, after closing it if not closed
+ $session->reconnect;
+
+ # check if the session is ok
+ # my $status = $session->conn_status;
+
+ # run some SQL, not producing tuples
+ my $result = $session->do($sql, ...);
+
+ # run an SQL statement asynchronously
+ my $result = $session->do_async($sql);
+
+ # wait for and async SQL to complete
+ $session->wait_for_completion;
+
+ # set a password for a user
+ my $result = $session->set_password($user, $password);
+
+ # get some data
+ my $result = $session->query($sql);
+
+ # get a single value, default croaks if no value found
+ my $val = $session->query_oneval($sql [, $missing_ok ]);
+
+ #return lines of tuples like "psql -A -t"
+ my @lines = $session->query_tuples($sql, ...);
+
+=head1 DESCRIPTION
+
+C<PostgreSQL::Test::Session> encapsulates a C<libpq> session for use in
+PostgreSQL TAP tests, allowing the test to connect without having to spawn
+C<psql> in a child process.
+
+The session object is automatically closed when the object goes out of scope,
+including at script end.
+
+Several methods return a hashref as a result, which will have the following
+fields:
+
+=over
+
+=item * status
+
+=item * error_message (only if there is an error)
+
+=item * names
+
+=item * types
+
+=item * rows
+
+=item * psqlout
+
+=back
+
+The last 4 will be empty unless the SQL produces tuples.
+
+=cut
+
+package PostgreSQL::Test::Session;
+
+use strict;
+use warnings FATAL => 'all';
+
+use Carp;
+
+my $setup_ok;
+
+BEGIN
+{
+ # Use the FFI libpq wrapper. This will fail if the FFI libraries are not
+ # available.
+ #
+ # The actual setup is done per session, because we get the libdir from
+ # the node object (in most cases).
+ require PostgreSQL::PqFFI;
+ PostgreSQL::PqFFI->import;
+}
+
+sub _setup
+{
+ return if $setup_ok;
+ my $libdir = shift;
+ PostgreSQL::PqFFI::setup($libdir);
+ $setup_ok = 1;
+}
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item PostgreSQL::Test::Session->new(node=> $node [, dbname=> $dbname ])
+
+Set up a new session for the node, which must be a C<PostgreSQL::Test::Cluster>
+instance. The default dbame is C<postgres>.
+
+=item PostgreSQL::Test::Session->new(connstr => $connstr [, libdir => $libdir])
+
+Set up a new session for the connection string. If using the FFI libpq wrapper,
+C<$libdir> must point to the directory where the libpq library is installed.
+
+=cut
+
+sub new
+{
+ my $class = shift;
+ my $self = {};
+ bless $self, $class;
+ my %args = @_;
+ my $node = $args{node};
+ my $dbname = $args{dbname} || 'postgres';
+ my $libdir = $args{libdir};
+ my $connstr = $args{connstr};
+ my $wait = $args{wait} // 1;
+ unless ($setup_ok)
+ {
+ unless ($libdir)
+ {
+ croak "bad node" unless $node->isa("PostgreSQL::Test::Cluster");
+ $libdir = $node->config_data($^O eq 'MSWin32' ? '--bindir' : '--libdir');
+ }
+ _setup($libdir);
+ }
+ unless ($connstr)
+ {
+ croak "bad node" unless $node->isa("PostgreSQL::Test::Cluster");
+ $connstr = $node->connstr($dbname);
+ }
+ $self->{connstr} = $connstr;
+ $self->{notices} = [];
+
+ if ($wait)
+ {
+ $self->{conn} = PQconnectdb($connstr);
+ # The destructor will clean up for us even if we fail
+ return undef unless PQstatus($self->{conn}) == CONNECTION_OK;
+ $self->_setup_notice_processor();
+ return $self;
+ }
+ else
+ {
+ $self->{conn} = PQconnectStart($connstr);
+ return PQstatus($self->{conn}) != CONNECTION_BAD ? $self : undef;
+ }
+}
+
+# Set up a notice processor to capture notices/warnings
+sub _setup_notice_processor
+{
+ my $self = shift;
+
+ # Only available with FFI backend
+ return unless defined &create_notice_processor;
+
+ my $notices = $self->{notices};
+
+ # Create closure that captures notices into our array
+ $self->{_notice_closure} = create_notice_processor(sub {
+ my ($arg, $message) = @_;
+ push @$notices, $message;
+ });
+
+ PQsetNoticeProcessor($self->{conn}, $self->{_notice_closure}, undef);
+}
+
+
+# for a connection started with PQconnectStart, wait until it is in CONNECTION_OK state.
+# Uses PQconnectPoll to drive the async connection forward.
+sub wait_connect
+{
+ my $self = shift;
+ my $conn = $self->{conn};
+ my $timeout = $PostgreSQL::Test::Utils::timeout_default;
+ my $start = time;
+ while (1)
+ {
+ my $poll_res = PQconnectPoll($conn);
+ my $status = PQstatus($conn);
+
+ # Connection is complete
+ if ($poll_res == PGRES_POLLING_OK || $status == CONNECTION_OK)
+ {
+ $self->_setup_notice_processor();
+ return;
+ }
+
+ # Connection failed
+ if ($poll_res == PGRES_POLLING_FAILED || $status == CONNECTION_BAD)
+ {
+ die "connection failed: " . PQerrorMessage($conn);
+ }
+
+ die "timed out waiting for connection" if time - $start > $timeout;
+
+ # Wait on socket based on what PQconnectPoll needs
+ my $socket = PQsocket($conn);
+ if ($socket >= 0)
+ {
+ my $forRead = ($poll_res == PGRES_POLLING_READING) ? 1 : 0;
+ my $forWrite = ($poll_res == PGRES_POLLING_WRITING) ? 1 : 0;
+ my $end_time = PQgetCurrentTimeUSec() + 1_000_000; # 1 second
+ PQsocketPoll($socket, $forRead, $forWrite, $end_time);
+ }
+ }
+}
+
+# Single step of async connection polling - drives the connection state machine
+# forward without blocking. Returns the poll result (PGRES_POLLING_* constant).
+sub poll_connect
+{
+ my $self = shift;
+ my $conn = $self->{conn};
+ return PQconnectPoll($conn);
+}
+
+=pod
+
+=item $session->close()
+
+Close the connection
+
+=cut
+
+sub close
+{
+ my $self = shift;
+ PQfinish($self->{conn});
+ delete $self->{conn};
+}
+
+# Alias for compatibility with BackgroundPsql
+*quit = \&close;
+
+# close the session if the object goes out of scope
+sub DESTROY
+{
+ my $self = shift;
+
+ # During global destruction the FFI::Platypus bindings (and any notice
+ # processor closure registered with libpq) may already have been torn
+ # down in an unpredictable order. Calling PQfinish() then can invoke a
+ # freed notice callback or a freed FFI thunk, dying with "Can't use an
+ # undefined value as a subroutine reference". The process is exiting
+ # anyway, so just let the OS reclaim the socket.
+ return if ${^GLOBAL_PHASE} eq 'DESTRUCT';
+
+ $self->close if exists $self->{conn};
+}
+
+=pod
+
+=item $session->reconnect()
+
+Reopen the session using the original connstr. If the session is still open,
+close it before reopening.
+
+=cut
+
+sub reconnect
+{
+ my $self = shift;
+ $self->close if exists $self->{conn};
+ $self->{conn} = PQconnectdb($self->{connstr});
+ my $status = PQstatus($self->{conn});
+ if ($status == CONNECTION_OK)
+ {
+ $self->_setup_notice_processor();
+ }
+ return $status;
+}
+
+=pod
+
+=item $session->reconnect_and_clear()
+
+Reconnect and clear all captured notices. Returns the connection status.
+
+=cut
+
+sub reconnect_and_clear
+{
+ my $self = shift;
+ my $status = $self->reconnect();
+ $self->clear_notices();
+ return $status;
+}
+
+=pod
+
+=item $session->get_notices()
+
+Return an arrayref of all captured notices/warnings since the last clear.
+
+=cut
+
+sub get_notices
+{
+ my $self = shift;
+ return $self->{notices};
+}
+
+=pod
+
+=item $session->get_notices_str()
+
+Return all captured notices/warnings as a single string (joined by empty string).
+This is similar to how BackgroundPsql's {stderr} field works.
+
+=cut
+
+sub get_notices_str
+{
+ my $self = shift;
+ return join('', @{$self->{notices}});
+}
+
+=pod
+
+=item $session->clear_notices()
+
+Clear all captured notices/warnings.
+
+=cut
+
+sub clear_notices
+{
+ my $self = shift;
+ # Clear in place - don't replace the array, as the notice processor
+ # closure has a reference to it
+ @{$self->{notices}} = ();
+}
+
+=pod
+
+=item $session->get_stderr()
+
+Return a stderr-like string containing all notices plus any error message
+from the last query. This mimics BackgroundPsql's {stderr} behavior.
+
+=cut
+
+sub get_stderr
+{
+ my $self = shift;
+ my $stderr = $self->get_notices_str();
+ if (exists $self->{last_error} && defined $self->{last_error})
+ {
+ $stderr .= $self->{last_error};
+ }
+ return $stderr;
+}
+
+=pod
+
+=item $session->clear_stderr()
+
+Clear notices and last error, like setting $psql->{stderr} = ''.
+
+=cut
+
+sub clear_stderr
+{
+ my $self = shift;
+ $self->clear_notices();
+ delete $self->{last_error};
+}
+
+=pod
+
+=item $session->conn_status()
+
+Return the connection status. This will be a libpq status value like
+C<CONNECTION_OK>.
+
+=cut
+
+sub conn_status
+{
+ my $self = shift;
+ return exists $self->{conn} ? PQstatus($self->{conn}) : undef;
+}
+
+=pod
+
+=item $session->backend_pid()
+
+Return the backend process ID for this connection.
+
+=cut
+
+sub backend_pid
+{
+ my $self = shift;
+ return PQbackendPID($self->{conn});
+}
+
+=pod
+
+=item $session->do($sql, ...)
+
+Run one or more SQL statements synchronously (using C<PQexec>). The statements
+should not return any tuples. Returns the status, which will be
+C<PGRES_COMMAND_OK> (i.e. 1) in the case of success.
+
+=cut
+
+sub do
+{
+ my $self = shift;
+ my $conn = $self->{conn};
+ my $status;
+ foreach my $sql (@_)
+ {
+ my $result = PQexec($conn, $sql);
+ $status = PQresultStatus($result);
+ PQclear($result);
+ return $status unless $status == PGRES_COMMAND_OK;
+ }
+ return $status;
+}
+
+=pod
+
+=item $session->do_async($sql)
+
+Run a single statement asynchronously, using C<PQsendQuery>. The return value
+is a boolean indicating success.
+
+=cut
+
+sub do_async
+{
+ my $self = shift;
+ my $conn = $self->{conn};
+ my $sql = shift;
+ my $result = PQsendQuery($conn, $sql);
+ return $result; # 1 or 0
+}
+
+# get the next resultset from some async commands
+# wait if necessary using PQsocketPoll
+# c.f. libpqsrv_get_result
+sub _get_result
+{
+ my $conn = shift;
+ my $socket = PQsocket($conn);
+ while (PQisBusy($conn))
+ {
+ # Wait for the socket to become readable (no timeout = -1)
+ PQsocketPoll($socket, 1, 0, -1);
+ last if PQconsumeInput($conn) == 0;
+ }
+ return PQgetResult($conn);
+}
+
+=pod
+
+=item $session->wait_for_completion()
+
+Wait until all asynchronous SQL has completed
+
+=cut
+
+sub wait_for_completion
+{
+ # wait for all the resultsets and clear them
+ # c.f. libpqsrv_get_result_last
+ my $self = shift;
+ my $conn = $self->{conn};
+ while (my $res = _get_result($conn))
+ {
+ PQclear($res);
+ }
+}
+
+=pod
+
+=item $session->get_async_result()
+
+Wait for and return the result of an async query as a result hash.
+Clears any subsequent results.
+
+=cut
+
+sub get_async_result
+{
+ my $self = shift;
+ my $conn = $self->{conn};
+ my $result = _get_result($conn);
+ return undef unless $result;
+ my $res = _get_result_data($result, $conn);
+ PQclear($result);
+ # Clear any remaining results
+ while (my $r = _get_result($conn))
+ {
+ PQclear($r);
+ }
+ return $res;
+}
+
+=pod
+
+=item $session->wait_for_async_pattern($pattern, $timeout)
+
+Wait for an async query result whose psqlout matches the given regex pattern.
+Returns the matching output string, or dies on timeout/error.
+Default timeout is from $PostgreSQL::Test::Utils::timeout_default.
+
+=cut
+
+sub wait_for_async_pattern
+{
+ my $self = shift;
+ my $pattern = shift;
+ my $timeout = shift // $PostgreSQL::Test::Utils::timeout_default;
+ my $conn = $self->{conn};
+ my $socket = PQsocket($conn);
+ my $start = time;
+
+ while (1)
+ {
+ # Check if result is ready (non-blocking)
+ PQconsumeInput($conn);
+ if (!PQisBusy($conn))
+ {
+ my $result = PQgetResult($conn);
+ if ($result)
+ {
+ my $res = _get_result_data($result, $conn);
+ PQclear($result);
+ # Clear any remaining results
+ while (my $r = PQgetResult($conn))
+ {
+ PQclear($r);
+ }
+ my $output = $res->{psqlout};
+ if ($output =~ $pattern)
+ {
+ return $output;
+ }
+ # If we got a result but it didn't match, return it anyway
+ # (caller may want to check error)
+ return $output;
+ }
+ }
+
+ die "timed out waiting for async result" if time - $start > $timeout;
+
+ # Wait for the socket to become readable, with 1 second timeout
+ # to allow periodic timeout checks
+ my $end_time = PQgetCurrentTimeUSec() + 1_000_000; # 1 second
+ PQsocketPoll($socket, 1, 0, $end_time);
+ }
+}
+
+=pod
+
+=item $session->try_get_async_result()
+
+Non-blocking check for async query result. Returns result hash if available,
+undef if query is still running.
+
+=cut
+
+sub try_get_async_result
+{
+ my $self = shift;
+ my $conn = $self->{conn};
+
+ PQconsumeInput($conn);
+ return undef if PQisBusy($conn);
+
+ my $result = PQgetResult($conn);
+ return undef unless $result;
+
+ my $res = _get_result_data($result, $conn);
+ PQclear($result);
+ # Clear any remaining results
+ while (my $r = PQgetResult($conn))
+ {
+ PQclear($r);
+ }
+ return $res;
+}
+
+=pod
+
+=item $session->set_password($user, $password)
+
+Set the user's password by calling C<PQchangePassword>.
+
+Returns a result hash.
+
+=cut
+
+# set password for user
+sub set_password
+{
+ my $self = shift;
+ my $user = shift;
+ my $password = shift;
+ my $conn = $self->{conn};
+ my $result = PQchangePassword($conn, $user, $password);
+ my $ret = _get_result_data($result);
+ PQclear($result);
+ return $ret;
+}
+
+# Common internal routine to process result data.
+# The returned object is dead and will be garbage collected as necessary.
+
+sub _get_result_data
+{
+ my $result = shift;
+ my $conn = shift;
+ my $status = PQresultStatus($result);
+ my $res = { status => $status, names => [], types => [], rows => [],
+ psqlout => ""};
+ unless ($status == PGRES_TUPLES_OK || $status == PGRES_COMMAND_OK)
+ {
+ $res->{error_message} = PQerrorMessage($conn);
+ return $res;
+ }
+ if ($status == PGRES_COMMAND_OK)
+ {
+ return $res;
+ }
+ my $ntuples = PQntuples($result);
+ my $nfields = PQnfields($result);
+ # assuming here that the strings returned by PQfname and PQgetvalue
+ # are mapped into perl space using setsvpv or similar and thus won't
+ # be affect by us calling PQclear on the result object.
+ foreach my $field (0 .. $nfields-1)
+ {
+ push(@{$res->{names}}, PQfname($result, $field));
+ push(@{$res->{types}}, PQftype($result, $field));
+ }
+ my @textrows;
+ foreach my $nrow (0 .. $ntuples - 1)
+ {
+ my $row = [];
+ foreach my $field ( 0 .. $nfields - 1)
+ {
+ my $val = PQgetvalue($result, $nrow, $field);
+ if (($val // "") eq "")
+ {
+ $val = undef if PQgetisnull($result, $nrow, $field);
+ }
+ push(@$row, $val);
+ }
+ push(@{$res->{rows}}, $row);
+ no warnings qw(uninitialized);
+ push(@textrows, join('|', @$row));
+ }
+ $res->{psqlout} = join("\n",@textrows) if $ntuples;
+ return $res;
+}
+
+
+=pod
+
+=item $session->query($sql)
+
+Runs sql that might return tuples.
+
+Returns a result hash.
+
+=cut
+
+sub query
+{
+ my $self = shift;
+ my $sql = shift;
+ my $conn = $self->{conn};
+
+ # Use PQsendQuery + PQgetResult to handle multi-statement SQL properly.
+ # This collects results from all statements and returns the last one
+ # that had tuples, similar to how psql works.
+ PQsendQuery($conn, $sql) or do {
+ return { status => -1, error_message => PQerrorMessage($conn),
+ names => [], types => [], rows => [], psqlout => "" };
+ };
+
+ my $final_res;
+ my $last_error;
+ my @all_psqlout;
+
+ while (my $result = _get_result($conn))
+ {
+ my $res = _get_result_data($result, $conn);
+ PQclear($result);
+
+ # Collect output from all statements
+ push @all_psqlout, $res->{psqlout} if $res->{psqlout} ne "";
+
+ # Track errors
+ $last_error = $res->{error_message} if exists $res->{error_message};
+
+ # Keep the last result that had tuples, or the last result overall
+ if ($res->{status} == PGRES_TUPLES_OK || !defined $final_res)
+ {
+ $final_res = $res;
+ }
+ }
+
+ $final_res //= { status => PGRES_COMMAND_OK, names => [], types => [],
+ rows => [], psqlout => "" };
+
+ # Combine all output (like psql does)
+ $final_res->{psqlout} = join("\n", @all_psqlout) if @all_psqlout;
+
+ # If there was any error, include it in the result for query_safe
+ $final_res->{error_message} = $last_error if defined $last_error;
+
+ # Store error for get_stderr()
+ $self->{last_error} = $last_error;
+
+ # If we're in an aborted transaction state, roll it back to clean up.
+ # This mimics psql's behavior with on_error_stop => 0.
+ if (PQtransactionStatus($conn) == PQTRANS_INERROR)
+ {
+ my $rb = PQexec($conn, "ROLLBACK");
+ PQclear($rb) if $rb;
+ }
+
+ return $final_res;
+}
+
+=pod
+
+=item $session->query_safe($sql)
+
+Runs sql that might return tuples, croaking if there's an error.
+Returns the psqlout string (like psql -At output) on success.
+
+=cut
+
+sub query_safe
+{
+ my $self = shift;
+ my $sql = shift;
+ my $res = $self->query($sql);
+ if (exists $res->{error_message}) {
+ # Debug: show where the error occurred
+ my $short_sql = substr($sql, 0, 100);
+ $short_sql =~ s/\s+/ /g;
+ croak "query_safe failed on [$short_sql...]: $res->{error_message}";
+ }
+ return $res->{psqlout};
+}
+
+=pod
+
+=item $session->query_oneval($sql [, $missing_ok ] )
+
+Run a query that is expected to return no more than one tuple with one value;
+
+If C<$missing_ok> is true, return undef if the query returns no tuple. Otherwise
+croak if there is not exactly one tuple, or of the tuple does not have
+exctly one value.
+
+If none of these apply, return the single value from the query. A NULL value
+will result in undef, so if C<$missing_ok> is true you won't be able to
+distinguish between a null value and a missing tuple.
+
+A non NULL value is returned as the string value obtained from C<PQgetvalue>.
+
+=cut
+
+sub query_oneval
+{
+ my $self = shift;
+ my $sql = shift;
+ my $missing_ok = shift; # default is not ok
+ my $conn = $self->{conn};
+ my $result = PQexec($conn, $sql);
+ my $status = PQresultStatus($result);
+ unless ($status == PGRES_TUPLES_OK)
+ {
+ PQclear($result) if $result;
+ croak PQerrorMessage($conn);
+ }
+ my $ntuples = PQntuples($result);
+ return undef if ($missing_ok && !$ntuples);
+ my $nfields = PQnfields($result);
+ croak "$ntuples tuples != 1 or $nfields fields != 1"
+ if $ntuples != 1 || $nfields != 1;
+ my $val = PQgetvalue($result, 0, 0);
+ if ($val eq "")
+ {
+ $val = undef if PQgetisnull($result, 0, 0);
+ }
+ PQclear($result);
+ return $val;
+}
+
+=pod
+
+=item $session->query_tuples($sql, ...)
+
+Run the sql commands and return the output as a single piece of text in the
+same format as C<psql -A -t>.
+
+Fields within tuples are separated by a "|", tuples are spearated by "\n"
+
+=cut
+
+sub query_tuples
+{
+ my $self = shift;
+ # Use pipelined version for 4+ queries where the overhead is worth it
+ return $self->query_tuples_pipelined(@_) if @_ >= 4;
+
+ my @results;
+ foreach my $sql (@_)
+ {
+ my $res = $self->query($sql);
+ croak $res->{error_message}
+ unless $res->{status} == PGRES_TUPLES_OK;
+ my $rows = $res->{rows};
+ unless (@$rows)
+ {
+ # unfortunately breaks at least one test
+ # push(@results,"-- empty");
+ next;
+ }
+ # join will render undef as an empty string here
+ no warnings qw(uninitialized);
+ my @tuples = map { join('|', @$_); } @$rows;
+ push(@results, join("\n",@tuples));
+ }
+ return join("\n",@results);
+}
+
+=pod
+
+=item $session->query_tuples_pipelined($sql, ...)
+
+Run multiple SQL queries using pipeline mode for efficiency. Returns output
+in the same format as C<query_tuples> but with only one network round-trip
+for all queries.
+
+=cut
+
+sub query_tuples_pipelined
+{
+ my $self = shift;
+ my @queries = @_;
+ my $conn = $self->{conn};
+ my @results;
+
+ # Enter pipeline mode
+ PQenterPipelineMode($conn) or croak "Failed to enter pipeline mode";
+
+ # Send all queries using PQsendQueryParams (PQsendQuery not allowed in pipeline mode)
+ for my $sql (@queries)
+ {
+ PQsendQueryParams($conn, $sql, 0, undef, undef, undef, undef, 0) or do {
+ PQexitPipelineMode($conn);
+ croak "Failed to send query: " . PQerrorMessage($conn);
+ };
+ }
+
+ # Mark end of pipeline
+ PQpipelineSync($conn) or do {
+ PQexitPipelineMode($conn);
+ croak "Failed to sync pipeline";
+ };
+
+ # Collect results for each query
+ for my $i (0 .. $#queries)
+ {
+ my $result = _get_result($conn);
+ if (!$result)
+ {
+ PQexitPipelineMode($conn);
+ croak "No result for query $i";
+ }
+
+ my $status = PQresultStatus($result);
+ if ($status == PGRES_PIPELINE_ABORTED)
+ {
+ PQclear($result);
+ PQexitPipelineMode($conn);
+ croak "Pipeline aborted at query $i";
+ }
+
+ if ($status == PGRES_TUPLES_OK)
+ {
+ my $res = _get_result_data($result, $conn);
+ my $rows = $res->{rows};
+ if (@$rows)
+ {
+ no warnings qw(uninitialized);
+ my @tuples = map { join('|', @$_); } @$rows;
+ push(@results, join("\n", @tuples));
+ }
+ }
+ elsif ($status != PGRES_COMMAND_OK)
+ {
+ my $err = PQerrorMessage($conn);
+ PQclear($result);
+ PQexitPipelineMode($conn);
+ croak "Query $i failed: $err";
+ }
+ PQclear($result);
+
+ # Consume the NULL result that marks end of this query's results
+ while (my $extra = PQgetResult($conn))
+ {
+ PQclear($extra);
+ }
+ }
+
+ # Consume the pipeline sync result
+ my $sync_result = _get_result($conn);
+ if ($sync_result)
+ {
+ my $status = PQresultStatus($sync_result);
+ PQclear($sync_result);
+ if ($status != PGRES_PIPELINE_SYNC)
+ {
+ PQexitPipelineMode($conn);
+ croak "Expected PGRES_PIPELINE_SYNC, got $status";
+ }
+ }
+
+ # Exit pipeline mode
+ PQexitPipelineMode($conn) or croak "Failed to exit pipeline mode";
+
+ return join("\n", @results);
+}
+
+
+sub setnonblocking
+{
+ my $self = shift;
+ my $val = shift;
+ my $res = PQsetnonblocking($self->{conn}, $val);
+ croak "problem setting non-blocking"
+ if $res;
+ return;
+}
+
+sub isnonblocking
+{
+ my $self = shift;
+ return PQisnonblocking($self->{conn});
+}
+
+sub enterPipelineMode
+{
+ my $self = shift;
+ return PQenterPipelineMode($self->{conn});
+}
+
+sub exitPipelineMode
+{
+ my $self = shift;
+ return PQexitPipelineMode($self->{conn});
+}
+
+sub pipelineStatus
+{
+ my $self = shift;
+ return PQpipelineStatus($self->{conn});
+}
+
+sub pipelineSync
+{
+ my $self = shift;
+ return PQpipelineSync($self->{conn});
+}
+
+
+sub do_pipeline
+{
+ my $self = shift;
+ my $statement = shift;
+ my @args = @_;
+ my $nargs = scalar(@args);
+ my $res = PQsendQueryParams($self->{conn}, $statement, $nargs, undef, \@args, undef , undef, 0);
+ return $res;
+}
+
+=pod
+
+=item $session->get_notification()
+
+Check for a pending notification and return it as a hashref with keys
+C<channel>, C<pid>, and C<payload>. Returns undef if no notification is
+available.
+
+Automatically consumes any pending input before checking for notifications.
+
+=cut
+
+sub get_notification
+{
+ my $self = shift;
+ my $conn = $self->{conn};
+
+ # Consume any pending input
+ PQconsumeInput($conn);
+
+ my $notify = PQnotifies($conn);
+ return undef unless $notify;
+
+ my $result = {
+ channel => PQnotify_channel($notify),
+ pid => PQnotify_be_pid($notify),
+ payload => PQnotify_payload($notify),
+ };
+
+ PQnotify_free($notify);
+
+ return $result;
+}
+
+=pod
+
+=item $session->get_all_notifications()
+
+Consume input and return all pending notifications as an arrayref of hashrefs.
+Each hashref has keys C<channel>, C<pid>, and C<payload>.
+
+=cut
+
+sub get_all_notifications
+{
+ my $self = shift;
+ my @notifications;
+
+ while (my $notify = $self->get_notification())
+ {
+ push @notifications, $notify;
+ }
+
+ return \@notifications;
+}
+
+=pod
+
+=back
+
+=cut
+
+
+1;
diff --git a/src/test/perl/meson.build b/src/test/perl/meson.build
index 0fd36c9e570..37d8ac2a36a 100644
--- a/src/test/perl/meson.build
+++ b/src/test/perl/meson.build
@@ -4,6 +4,10 @@
install_data(
'PostgreSQL/Version.pm',
+ 'PostgreSQL/FindLib.pm',
+ 'PostgreSQL/PqFFI.pm',
+ 'PostgreSQL/PqConstants.pm',
+ 'PostgreSQL/PGTypes.pm',
install_dir: dir_pgxs / 'src/test/perl/PostgreSQL')
install_data(
@@ -15,4 +19,5 @@ install_data(
'PostgreSQL/Test/BackgroundPsql.pm',
'PostgreSQL/Test/AdjustDump.pm',
'PostgreSQL/Test/AdjustUpgrade.pm',
+ 'PostgreSQL/Test/Session.pm',
install_dir: dir_pgxs / 'src/test/perl/PostgreSQL/Test')
--
2.43.0
[text/x-patch] v12-0002-Use-PostgreSQL-Test-Session-within-the-TAP-test-.patch (6.1K, ../[email protected]/3-v12-0002-Use-PostgreSQL-Test-Session-within-the-TAP-test-.patch)
download | inline diff:
From d0d04d080992c42478d228a4f73ff19a4703297e Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <[email protected]>
Date: Tue, 2 Jun 2026 11:28:05 -0400
Subject: [PATCH v12 2/3] Use PostgreSQL::Test::Session within the TAP test
framework
Make the test framework itself benefit from in-process libpq access:
* PostgreSQL::Test::Cluster::safe_psql() runs a single, parameterless
statement through a Session instead of spawning psql, falling back
to psql for multi-statement or parameterized calls.
* poll_query_until() polls through a Session.
* add poll_until_connection() to wait for the server to accept
connections.
Also harden PostgreSQL::Test::Utils against errors raised during global
destruction. Such errors happen after the test body has finished (for
example FFI::Platypus bindings or callback closures being freed in an
unpredictable order as the process exits); reporting them by calling
done_testing() a second time would spuriously fail an otherwise-passing
test, so they are now logged and ignored.
---
src/test/perl/PostgreSQL/Test/Cluster.pm | 114 ++++++++++++++++-------
src/test/perl/PostgreSQL/Test/Utils.pm | 13 +++
2 files changed, 91 insertions(+), 36 deletions(-)
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 4fcb1f6be56..a5bf18e6a16 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -112,6 +112,7 @@ use Socket;
use Test::More;
use PostgreSQL::Test::Utils ();
use PostgreSQL::Test::BackgroundPsql ();
+use PostgreSQL::Test::Session;
use Text::ParseWords qw(shellwords);
use Time::HiRes qw(usleep);
use Scalar::Util qw(blessed);
@@ -2071,20 +2072,40 @@ sub safe_psql
my ($stdout, $stderr);
- my $ret = $self->psql(
- $dbname, $sql,
- %params,
- stdout => \$stdout,
- stderr => \$stderr,
- on_error_die => 1,
- on_error_stop => 1);
-
- # psql can emit stderr from NOTICEs etc
- if ($stderr ne "")
+ # for now only use a Session object for single statement sql without
+ # any special params
+ if ($sql =~ /\w/ && $sql !~ /\\bind|;.*\w/s && !scalar(keys(%params)))
{
- print "#### Begin standard error\n";
- print $stderr;
- print "\n#### End standard error\n";
+
+ my $session = PostgreSQL::Test::Session->new(node=> $self,
+ dbname => $dbname);
+ my $res = $session->query($sql);
+ my $status = $res->{status};
+ $stdout = $res->{psqlout} // "";
+ $stderr = $res->{error_message} // "";
+ die "error: status = $status stderr: '$stderr'\nwhile running '$sql'"
+ if ($status != 1 && $status != 2); # COMMAND_OK or COMMAND_TUPLES
+
+ }
+ else
+ {
+ # diag "safe_psql call has params or multiple statements";
+
+ my $ret = $self->psql(
+ $dbname, $sql,
+ %params,
+ stdout => \$stdout,
+ stderr => \$stderr,
+ on_error_die => 1,
+ on_error_stop => 1);
+
+ # psql can emit stderr from NOTICEs etc
+ if ($stderr ne "")
+ {
+ print "#### Begin standard error\n";
+ print $stderr;
+ print "\n#### End standard error\n";
+ }
}
return $stdout;
@@ -2188,6 +2209,9 @@ sub psql
local %ENV = $self->_get_env();
+ # uncomment to get a count of calls to psql
+ # note("counting psql");
+
my $stdout = $params{stdout};
my $stderr = $params{stderr};
my $replication = $params{replication};
@@ -2761,33 +2785,20 @@ sub poll_query_until
{
my ($self, $dbname, $query, $expected) = @_;
- local %ENV = $self->_get_env();
-
$expected = 't' unless defined($expected); # default value
- my $cmd = [
- $self->installed_command('psql'), '--no-psqlrc',
- '--no-align', '--tuples-only',
- '--dbname' => $self->connstr($dbname)
- ];
- my ($stdout, $stderr);
+ my $session = PostgreSQL::Test::Session->new(node => $self,
+ dbname => $dbname);
my $max_attempts = 10 * $PostgreSQL::Test::Utils::timeout_default;
my $attempts = 0;
+ my $query_value;
+
while ($attempts < $max_attempts)
{
- my $result = IPC::Run::run $cmd,
- '<' => \$query,
- '>' => \$stdout,
- '2>' => \$stderr;
-
- chomp($stdout);
- chomp($stderr);
-
- if ($stdout eq $expected && $stderr eq '')
- {
- return 1;
- }
+ my $result = $session->query($query);
+ $query_value = ($result->{psqlout} // "");
+ return 1 if $query_value eq $expected;
# Wait 0.1 second before retrying.
usleep(100_000);
@@ -2802,9 +2813,40 @@ $query
expecting this output:
$expected
last actual query output:
-$stdout
-with stderr:
-$stderr);
+$query_value
+);
+ return 0;
+}
+
+=pod
+
+=item $node->poll_until_connection($dbname)
+
+Try to connect repeatedly, until it we succeed.
+Times out after $PostgreSQL::Test::Utils::timeout_default seconds.
+Returns 1 if successful, 0 if timed out.
+
+=cut
+
+sub poll_until_connection
+{
+ my ($self, $dbname) = @_;
+
+ my $max_attempts = 10 * $PostgreSQL::Test::Utils::timeout_default;
+ my $attempts = 0;
+
+ while ($attempts < $max_attempts)
+ {
+ my $session = PostgreSQL::Test::Session->new(node => $self,
+ dbname => $dbname);
+ return 1 if $session;
+
+ # Wait 0.1 second before retrying.
+ usleep(100_000);
+
+ $attempts++;
+ }
+
return 0;
}
diff --git a/src/test/perl/PostgreSQL/Test/Utils.pm b/src/test/perl/PostgreSQL/Test/Utils.pm
index d3e6abf7a68..c091ce0326d 100644
--- a/src/test/perl/PostgreSQL/Test/Utils.pm
+++ b/src/test/perl/PostgreSQL/Test/Utils.pm
@@ -262,6 +262,19 @@ INIT
# Ignore dies inside evals
return if $^S == 1;
+ # Dies during global destruction happen after the test body has
+ # finished (done_testing() has already run). They are typically
+ # harmless teardown-ordering artifacts -- for example FFI::Platypus
+ # bindings or callback closures being freed in an unpredictable order
+ # while the process exits. Reporting them by calling done_testing()
+ # again would spuriously fail an otherwise-passing test, so just log
+ # them and return.
+ if (${^GLOBAL_PHASE} eq 'DESTRUCT')
+ {
+ diag("die during global destruction (ignored): $_[0]");
+ return;
+ }
+
diag("die: $_[0]");
# Also call done_testing() to avoid the confusing "no plan was declared"
# message in TAP output when a test dies.
--
2.43.0
[text/x-patch] v12-0003-Convert-TAP-tests-to-PostgreSQL-Test-Session.patch (172.5K, ../[email protected]/4-v12-0003-Convert-TAP-tests-to-PostgreSQL-Test-Session.patch)
download | inline diff:
From 7e755fdb9c2ffe5bd2a3fe5db8dccd51eeac8d04 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <[email protected]>
Date: Tue, 2 Jun 2026 11:28:15 -0400
Subject: [PATCH v12 3/3] Convert TAP tests to PostgreSQL::Test::Session
Convert tests that drove a long-lived background psql process (or
repeatedly spawned psql) to use the libpq Session object instead. This
covers concurrent-session and asynchronous-query tests across contrib,
src/bin and src/test: amcheck, bloom, pg_visibility, test_decoding,
pg_amcheck, pg_upgrade, authentication, oauth_validator, test_aio,
test_checksums, test_misc, test_slru, xid_wraparound, postmaster,
recovery and subscription.
---
contrib/amcheck/t/001_verify_heapam.pl | 36 +-
contrib/amcheck/t/002_cic.pl | 7 +-
contrib/amcheck/t/003_cic_2pc.pl | 64 ++-
contrib/bloom/t/001_wal.pl | 44 +-
.../t/001_concurrent_transaction.pl | 9 +-
contrib/test_decoding/t/001_repl_stats.pl | 11 +-
src/bin/pg_amcheck/t/004_verify_heapam.pl | 97 +++--
.../pg_upgrade/t/007_multixact_conversion.pl | 30 +-
src/test/authentication/t/001_password.pl | 35 +-
src/test/authentication/t/007_pre_auth.pl | 34 +-
.../modules/oauth_validator/t/001_server.pl | 29 +-
src/test/modules/test_aio/t/001_aio.pl | 165 ++++---
.../modules/test_aio/t/004_read_stream.pl | 75 ++--
.../modules/test_checksums/t/002_restarts.pl | 7 +-
.../test_checksums/t/003_standby_restarts.pl | 13 +-
.../modules/test_checksums/t/004_offline.pl | 7 +-
src/test/modules/test_misc/t/005_timeouts.pl | 45 +-
.../modules/test_misc/t/007_catcache_inval.pl | 24 +-
.../t/010_index_concurrently_upsert.pl | 406 ++++++------------
.../modules/test_misc/t/011_lock_stats.pl | 96 ++---
.../test_misc/t/013_temp_obj_multisession.pl | 26 +-
src/test/modules/test_slru/t/001_multixact.pl | 17 +-
.../xid_wraparound/t/001_emergency_vacuum.pl | 16 +-
.../modules/xid_wraparound/t/002_limits.pl | 21 +-
.../xid_wraparound/t/004_notify_freeze.pl | 27 +-
.../postmaster/t/002_connection_limits.pl | 26 +-
src/test/recovery/t/009_twophase.pl | 16 +-
src/test/recovery/t/013_crash_restart.pl | 4 +-
src/test/recovery/t/022_crash_temp_files.pl | 4 +-
src/test/recovery/t/031_recovery_conflict.pl | 39 +-
src/test/recovery/t/037_invalid_database.pl | 45 +-
.../t/040_standby_failover_slots_sync.pl | 26 +-
.../recovery/t/041_checkpoint_at_promote.pl | 11 +-
src/test/recovery/t/042_low_level_backup.pl | 12 +-
.../recovery/t/046_checkpoint_logical_slot.pl | 66 ++-
.../t/047_checkpoint_physical_slot.pl | 18 +-
.../recovery/t/048_vacuum_horizon_floor.pl | 40 +-
src/test/recovery/t/049_wait_for_lsn.pl | 156 ++++---
.../recovery/t/050_redo_segment_missing.pl | 14 +-
.../recovery/t/051_effective_wal_level.pl | 23 +-
src/test/subscription/t/015_stream.pl | 35 +-
src/test/subscription/t/035_conflicts.pl | 23 +-
.../t/038_walsnd_shutdown_timeout.pl | 15 +-
43 files changed, 870 insertions(+), 1044 deletions(-)
diff --git a/contrib/amcheck/t/001_verify_heapam.pl b/contrib/amcheck/t/001_verify_heapam.pl
index e3fee19ae5d..9ea72179fcd 100644
--- a/contrib/amcheck/t/001_verify_heapam.pl
+++ b/contrib/amcheck/t/001_verify_heapam.pl
@@ -5,6 +5,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -18,7 +19,9 @@ $node = PostgreSQL::Test::Cluster->new('test');
$node->init(no_data_checksums => 1);
$node->append_conf('postgresql.conf', 'autovacuum=off');
$node->start;
-$node->safe_psql('postgres', q(CREATE EXTENSION amcheck));
+my $session = PostgreSQL::Test::Session->new(node => $node);
+
+$session->do(q(CREATE EXTENSION amcheck));
#
# Check a table with data loaded but no corruption, freezing, etc.
@@ -49,7 +52,7 @@ detects_heap_corruption(
# Check a corrupt table with all-frozen data
#
fresh_test_table('test');
-$node->safe_psql('postgres', q(VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) test));
+$session->do(q(VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) test));
detects_no_corruption("verify_heapam('test')",
"all-frozen not corrupted table");
corrupt_first_page('test');
@@ -81,7 +84,7 @@ sub relation_filepath
my ($relname) = @_;
my $pgdata = $node->data_dir;
- my $rel = $node->safe_psql('postgres',
+ my $rel = $session->query_oneval(
qq(SELECT pg_relation_filepath('$relname')));
die "path not found for relation $relname" unless defined $rel;
return "$pgdata/$rel";
@@ -92,8 +95,8 @@ sub fresh_test_table
{
my ($relname) = @_;
- return $node->safe_psql(
- 'postgres', qq(
+ return $session->do(
+ qq(
DROP TABLE IF EXISTS $relname CASCADE;
CREATE TABLE $relname (a integer, b text);
ALTER TABLE $relname SET (autovacuum_enabled=false);
@@ -117,8 +120,8 @@ sub fresh_test_sequence
{
my ($seqname) = @_;
- return $node->safe_psql(
- 'postgres', qq(
+ return $session->do(
+ qq(
DROP SEQUENCE IF EXISTS $seqname CASCADE;
CREATE SEQUENCE $seqname
INCREMENT BY 13
@@ -134,8 +137,8 @@ sub advance_test_sequence
{
my ($seqname) = @_;
- return $node->safe_psql(
- 'postgres', qq(
+ return $session->query_oneval(
+ qq(
SELECT nextval('$seqname');
));
}
@@ -145,10 +148,7 @@ sub set_test_sequence
{
my ($seqname) = @_;
- return $node->safe_psql(
- 'postgres', qq(
- SELECT setval('$seqname', 102);
- ));
+ return $session->query_oneval(qq(SELECT setval('$seqname', 102)));
}
# Call SQL functions to reset the sequence
@@ -156,8 +156,8 @@ sub reset_test_sequence
{
my ($seqname) = @_;
- return $node->safe_psql(
- 'postgres', qq(
+ return $session->do(
+ qq(
ALTER SEQUENCE $seqname RESTART WITH 51
));
}
@@ -169,6 +169,7 @@ sub corrupt_first_page
my ($relname) = @_;
my $relpath = relation_filepath($relname);
+ $session->close;
$node->stop;
my $fh;
@@ -191,6 +192,7 @@ sub corrupt_first_page
or BAIL_OUT("close failed: $!");
$node->start;
+ $session->reconnect;
}
sub detects_heap_corruption
@@ -216,7 +218,7 @@ sub detects_corruption
my ($function, $testname, @re) = @_;
- my $result = $node->safe_psql('postgres', qq(SELECT * FROM $function));
+ my $result = $session->query_tuples(qq(SELECT * FROM $function));
like($result, $_, $testname) for (@re);
}
@@ -226,7 +228,7 @@ sub detects_no_corruption
my ($function, $testname) = @_;
- my $result = $node->safe_psql('postgres', qq(SELECT * FROM $function));
+ my $result = $session->query_tuples(qq(SELECT * FROM $function));
is($result, '', $testname);
}
diff --git a/contrib/amcheck/t/002_cic.pl b/contrib/amcheck/t/002_cic.pl
index 629d00c1d05..f2c7bc8653f 100644
--- a/contrib/amcheck/t/002_cic.pl
+++ b/contrib/amcheck/t/002_cic.pl
@@ -6,6 +6,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -72,8 +73,8 @@ $node->safe_psql('postgres',
q(INSERT INTO quebec SELECT i FROM generate_series(1, 2) s(i);));
# start background transaction
-my $in_progress_h = $node->background_psql('postgres');
-$in_progress_h->query_safe(q(BEGIN; SELECT pg_current_xact_id();));
+my $in_progress_h = PostgreSQL::Test::Session->new(node => $node);
+$in_progress_h->do(q(BEGIN; SELECT pg_current_xact_id();));
# delete one row from table, while background transaction is in progress
$node->safe_psql('postgres', q(DELETE FROM quebec WHERE i = 1;));
@@ -86,7 +87,7 @@ my $result = $node->psql('postgres',
q(SELECT bt_index_parent_check('oscar', heapallindexed => true)));
is($result, '0', 'bt_index_parent_check for CIC after removed row');
-$in_progress_h->quit;
+$in_progress_h->close;
$node->stop;
done_testing();
diff --git a/contrib/amcheck/t/003_cic_2pc.pl b/contrib/amcheck/t/003_cic_2pc.pl
index f28eeac17ef..d3a1643bf33 100644
--- a/contrib/amcheck/t/003_cic_2pc.pl
+++ b/contrib/amcheck/t/003_cic_2pc.pl
@@ -7,6 +7,7 @@ use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Session;
use Test::More;
@@ -36,29 +37,42 @@ $node->safe_psql('postgres', q(CREATE TABLE tbl(i int, j jsonb)));
# statements.
#
-my $main_h = $node->background_psql('postgres');
+my $main_h = PostgreSQL::Test::Session->new(node=>$node);
-$main_h->query_safe(
+$main_h->do_async(
q(
BEGIN;
INSERT INTO tbl VALUES(0, '[[14,2,3]]');
));
-my $cic_h = $node->background_psql('postgres');
+my $cic_h = PostgreSQL::Test::Session->new(node=>$node);
-$cic_h->query_until(
- qr/start/, q(
-\echo start
-CREATE INDEX CONCURRENTLY idx ON tbl(i);
-CREATE INDEX CONCURRENTLY ginidx ON tbl USING gin(j);
+$cic_h->setnonblocking(1);
+
+$cic_h->enterPipelineMode();
+
+$cic_h->do_pipeline(
+ q(
+CREATE INDEX CONCURRENTLY idx ON tbl(i)
+));
+
+$cic_h->pipelineSync();
+
+$cic_h->do_pipeline(
+ q(
+CREATE INDEX CONCURRENTLY ginidx ON tbl USING gin(j)
));
-$main_h->query_safe(
+$cic_h->pipelineSync();
+
+$main_h->wait_for_completion;
+$main_h->do_async(
q(
PREPARE TRANSACTION 'a';
));
-$main_h->query_safe(
+$main_h->wait_for_completion;
+$main_h->do_async(
q(
BEGIN;
INSERT INTO tbl VALUES(0, '[[14,2,3]]');
@@ -66,7 +80,8 @@ INSERT INTO tbl VALUES(0, '[[14,2,3]]');
$node->safe_psql('postgres', q(COMMIT PREPARED 'a';));
-$main_h->query_safe(
+$main_h->wait_for_completion;
+$main_h->do_async(
q(
PREPARE TRANSACTION 'b';
BEGIN;
@@ -75,14 +90,17 @@ INSERT INTO tbl VALUES(0, '"mary had a little lamb"');
$node->safe_psql('postgres', q(COMMIT PREPARED 'b';));
-$main_h->query_safe(
- q(
-PREPARE TRANSACTION 'c';
-COMMIT PREPARED 'c';
-));
+$main_h->wait_for_completion;
+$main_h->do(
+ q(PREPARE TRANSACTION 'c';),
+ q(COMMIT PREPARED 'c';));
-$main_h->quit;
-$cic_h->quit;
+$main_h->close;
+
+# called twice out of an abundance of caution about pipeline mode
+$cic_h->wait_for_completion;
+$cic_h->wait_for_completion;
+$cic_h->close;
$result = $node->psql('postgres', q(SELECT bt_index_check('idx',true)));
is($result, '0', 'bt_index_check after overlapping 2PC');
@@ -106,10 +124,9 @@ PREPARE TRANSACTION 'persists_forever';
));
$node->restart;
-my $reindex_h = $node->background_psql('postgres');
-$reindex_h->query_until(
- qr/start/, q(
-\echo start
+my $reindex_h = PostgreSQL::Test::Session->new(node => $node);
+$reindex_h->do_async(
+ q(
DROP INDEX CONCURRENTLY idx;
CREATE INDEX CONCURRENTLY idx ON tbl(i);
DROP INDEX CONCURRENTLY ginidx;
@@ -117,7 +134,8 @@ CREATE INDEX CONCURRENTLY ginidx ON tbl USING gin(j);
));
$node->safe_psql('postgres', "COMMIT PREPARED 'spans_restart'");
-$reindex_h->quit;
+$reindex_h->wait_for_completion;
+$reindex_h->close;
$result = $node->psql('postgres', q(SELECT bt_index_check('idx',true)));
is($result, '0', 'bt_index_check after 2PC and restart');
$result = $node->psql('postgres', q(SELECT gin_index_check('ginidx')));
diff --git a/contrib/bloom/t/001_wal.pl b/contrib/bloom/t/001_wal.pl
index 683b1876055..f86c49d6357 100644
--- a/contrib/bloom/t/001_wal.pl
+++ b/contrib/bloom/t/001_wal.pl
@@ -5,11 +5,14 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
my $node_primary;
my $node_standby;
+my $session_primary;
+my $session_standby;
# Run few queries on both primary and standby and check their results match.
sub test_index_replay
@@ -21,20 +24,18 @@ sub test_index_replay
# Wait for standby to catch up
$node_primary->wait_for_catchup($node_standby);
- my $queries = qq(SET enable_seqscan=off;
-SET enable_bitmapscan=on;
-SET enable_indexscan=on;
-SELECT * FROM tst WHERE i = 0;
-SELECT * FROM tst WHERE i = 3;
-SELECT * FROM tst WHERE t = 'b';
-SELECT * FROM tst WHERE t = 'f';
-SELECT * FROM tst WHERE i = 3 AND t = 'c';
-SELECT * FROM tst WHERE i = 7 AND t = 'e';
-);
+ my @queries = (
+ "SELECT * FROM tst WHERE i = 0",
+ "SELECT * FROM tst WHERE i = 3",
+ "SELECT * FROM tst WHERE t = 'b'",
+ "SELECT * FROM tst WHERE t = 'f'",
+ "SELECT * FROM tst WHERE i = 3 AND t = 'c'",
+ "SELECT * FROM tst WHERE i = 7 AND t = 'e'",
+ );
# Run test queries and compare their result
- my $primary_result = $node_primary->safe_psql("postgres", $queries);
- my $standby_result = $node_standby->safe_psql("postgres", $queries);
+ my $primary_result = $session_primary->query_tuples(@queries);
+ my $standby_result = $session_standby->query_tuples(@queries);
is($primary_result, $standby_result, "$test_name: query result matches");
return;
@@ -55,13 +56,24 @@ $node_standby->init_from_backup($node_primary, $backup_name,
has_streaming => 1);
$node_standby->start;
+# Create and initialize the sessions
+$session_primary = PostgreSQL::Test::Session->new(node => $node_primary);
+$session_standby = PostgreSQL::Test::Session->new(node => $node_standby);
+my $initset = q[
+ SET enable_seqscan=off;
+ SET enable_bitmapscan=on;
+ SET enable_indexscan=on;
+];
+$session_primary->do($initset);
+$session_standby->do($initset);
+
# Create some bloom index on primary
-$node_primary->safe_psql("postgres", "CREATE EXTENSION bloom;");
-$node_primary->safe_psql("postgres", "CREATE TABLE tst (i int4, t text);");
-$node_primary->safe_psql("postgres",
+$session_primary->do("CREATE EXTENSION bloom;");
+$session_primary->do("CREATE TABLE tst (i int4, t text);");
+$session_primary->do(
"INSERT INTO tst SELECT i%10, substr(encode(sha256(i::text::bytea), 'hex'), 1, 1) FROM generate_series(1,10000) i;"
);
-$node_primary->safe_psql("postgres",
+$session_primary->do(
"CREATE INDEX bloomidx ON tst USING bloom (i, t) WITH (col1 = 3);");
# Test that queries give same result
diff --git a/contrib/pg_visibility/t/001_concurrent_transaction.pl b/contrib/pg_visibility/t/001_concurrent_transaction.pl
index 3aa556892a6..cf6e3b45182 100644
--- a/contrib/pg_visibility/t/001_concurrent_transaction.pl
+++ b/contrib/pg_visibility/t/001_concurrent_transaction.pl
@@ -6,6 +6,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -24,10 +25,10 @@ $standby->start;
# Setup another database
$node->safe_psql("postgres", "CREATE DATABASE other_database;\n");
-my $bsession = $node->background_psql('other_database');
+my $bsession = PostgreSQL::Test::Session->new(node => $node, dbname => 'other_database');
# Run a concurrent transaction
-$bsession->query_safe(
+$bsession->do(
qq[
BEGIN;
SELECT txid_current();
@@ -55,8 +56,8 @@ $result = $standby->safe_psql("postgres",
ok($result eq "", "pg_check_visible() detects no errors");
# Shutdown
-$bsession->query_safe("COMMIT;");
-$bsession->quit;
+$bsession->do("COMMIT;");
+$bsession->close;
$node->stop;
$standby->stop;
diff --git a/contrib/test_decoding/t/001_repl_stats.pl b/contrib/test_decoding/t/001_repl_stats.pl
index 6814c792e2b..79b9182014f 100644
--- a/contrib/test_decoding/t/001_repl_stats.pl
+++ b/contrib/test_decoding/t/001_repl_stats.pl
@@ -7,6 +7,7 @@ use strict;
use warnings FATAL => 'all';
use File::Path qw(rmtree);
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -129,11 +130,11 @@ $node->safe_psql('postgres',
);
# Look at slot data, with a persistent connection.
-my $bpgsql = $node->background_psql('postgres', on_error_stop => 1);
+my $bgsession = PostgreSQL::Test::Session->new(node=>$node);
# Launch query and look at slot data, incrementing the refcount of the
# stats entry.
-$bpgsql->query_safe(
+$bgsession->do_async(
"SELECT pg_logical_slot_peek_binary_changes('$slot_name_restart', NULL, NULL)"
);
@@ -150,7 +151,8 @@ $node->safe_psql('postgres',
# Look again at the slot data. The local stats reference should be refreshed
# to the reinitialized entry.
-$bpgsql->query_safe(
+$bgsession->wait_for_completion;
+$bgsession->do_async(
"SELECT pg_logical_slot_peek_binary_changes('$slot_name_restart', NULL, NULL)"
);
# Drop again the slot, the entry is not dropped yet as the previous session
@@ -176,6 +178,7 @@ command_like(
my $stats_file = "$datadir/pg_stat/pgstat.stat";
ok(-f "$stats_file", "stats file must exist after shutdown");
-$bpgsql->quit;
+$bgsession->wait_for_completion;
+$bgsession->close;
done_testing();
diff --git a/src/bin/pg_amcheck/t/004_verify_heapam.pl b/src/bin/pg_amcheck/t/004_verify_heapam.pl
index 95f1f34c90d..9afacbcaaea 100644
--- a/src/bin/pg_amcheck/t/004_verify_heapam.pl
+++ b/src/bin/pg_amcheck/t/004_verify_heapam.pl
@@ -5,6 +5,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -190,16 +191,17 @@ $node->append_conf('postgresql.conf', 'max_prepared_transactions=10');
$node->start;
my $port = $node->port;
my $pgdata = $node->data_dir;
-$node->safe_psql('postgres', "CREATE EXTENSION amcheck");
-$node->safe_psql('postgres', "CREATE EXTENSION pageinspect");
+my $session = PostgreSQL::Test::Session->new(node => $node);
+$session->do("CREATE EXTENSION amcheck");
+$session->do("CREATE EXTENSION pageinspect");
# Get a non-zero datfrozenxid
-$node->safe_psql('postgres', qq(VACUUM FREEZE));
+$session->do(qq(VACUUM FREEZE));
# Create the test table with precisely the schema that our corruption function
# expects.
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
CREATE TABLE public.test (a BIGINT, b TEXT, c TEXT);
ALTER TABLE public.test SET (autovacuum_enabled=false);
ALTER TABLE public.test ALTER COLUMN c SET STORAGE EXTERNAL;
@@ -209,14 +211,15 @@ $node->safe_psql(
# We want (0 < datfrozenxid < test.relfrozenxid). To achieve this, we freeze
# an otherwise unused table, public.junk, prior to inserting data and freezing
# public.test
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
CREATE TABLE public.junk AS SELECT 'junk'::TEXT AS junk_column;
ALTER TABLE public.junk SET (autovacuum_enabled=false);
- VACUUM FREEZE public.junk
- ));
+ ),
+ 'VACUUM FREEZE public.junk'
+);
-my $rel = $node->safe_psql('postgres',
+my $rel = $session->query_oneval(
qq(SELECT pg_relation_filepath('public.test')));
my $relpath = "$pgdata/$rel";
@@ -229,23 +232,24 @@ my $ROWCOUNT_BASIC = 16;
# First insert data needed for tests unrelated to update chain validation.
# Then freeze the page. These tuples are at offset numbers 1 to 16.
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
INSERT INTO public.test (a, b, c)
SELECT
x'DEADF9F9DEADF9F9'::bigint,
'abcdefg',
repeat('w', 10000)
FROM generate_series(1, $ROWCOUNT_BASIC);
- VACUUM FREEZE public.test;)
+ ),
+ 'VACUUM FREEZE public.test'
);
# Create some simple HOT update chains for line pointer validation. After
# the page is HOT pruned, we'll have two redirects line pointers each pointing
# to a tuple. We'll then change the second redirect to point to the same
# tuple as the first one and verify that we can detect corruption.
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
INSERT INTO public.test (a, b, c)
VALUES ( x'DEADF9F9DEADF9F9'::bigint, 'abcdefg',
generate_series(1,2)); -- offset numbers 17 and 18
@@ -254,8 +258,8 @@ $node->safe_psql(
));
# Create some more HOT update chains.
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
INSERT INTO public.test (a, b, c)
VALUES ( x'DEADF9F9DEADF9F9'::bigint, 'abcdefg',
generate_series(3,6)); -- offset numbers 21 through 24
@@ -264,25 +268,30 @@ $node->safe_psql(
));
# Negative test case of HOT-pruning with aborted tuple.
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
BEGIN;
UPDATE public.test SET c = 'a' WHERE c = '5'; -- offset number 27
ABORT;
- VACUUM FREEZE public.test;
- ));
+ ),
+ 'VACUUM FREEZE public.test;',
+ );
# Next update on any tuple will be stored at the same place of tuple inserted
# by aborted transaction. This should not cause the table to appear corrupt.
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
+ BEGIN;
UPDATE public.test SET c = 'a' WHERE c = '6'; -- offset number 27 again
- VACUUM FREEZE public.test;
- ));
+ COMMIT;
+ ),
+ 'VACUUM FREEZE public.test;',
+ );
# Data for HOT chain validation, so not calling VACUUM FREEZE.
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
+ BEGIN;
INSERT INTO public.test (a, b, c)
VALUES ( x'DEADF9F9DEADF9F9'::bigint, 'abcdefg',
generate_series(7,15)); -- offset numbers 28 to 36
@@ -293,11 +302,12 @@ $node->safe_psql(
UPDATE public.test SET c = 'a' WHERE c = '13'; -- offset number 41
UPDATE public.test SET c = 'a' WHERE c = '14'; -- offset number 42
UPDATE public.test SET c = 'a' WHERE c = '15'; -- offset number 43
+ COMMIT;
));
# Need one aborted transaction to test corruption in HOT chains.
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
BEGIN;
UPDATE public.test SET c = 'a' WHERE c = '9'; -- offset number 44
ABORT;
@@ -306,19 +316,19 @@ $node->safe_psql(
# Need one in-progress transaction to test few corruption in HOT chains.
# We are creating PREPARE TRANSACTION here as these will not be aborted
# even if we stop the node.
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
BEGIN;
PREPARE TRANSACTION 'in_progress_tx';
));
-my $in_progress_xid = $node->safe_psql(
- 'postgres', qq(
+my $in_progress_xid = $session->query_oneval(
+ qq(
SELECT transaction FROM pg_prepared_xacts;
));
-my $relfrozenxid = $node->safe_psql('postgres',
+my $relfrozenxid = $session->query_oneval(
q(select relfrozenxid from pg_class where relname = 'test'));
-my $datfrozenxid = $node->safe_psql('postgres',
+my $datfrozenxid = $session->query_oneval(
q(select datfrozenxid from pg_database where datname = 'postgres'));
# Sanity check that our 'test' table has a relfrozenxid newer than the
@@ -326,6 +336,7 @@ my $datfrozenxid = $node->safe_psql('postgres',
# first normal xid. We rely on these invariants in some of our tests.
if ($datfrozenxid <= 3 || $datfrozenxid >= $relfrozenxid)
{
+ $session->close;
$node->clean_node;
plan skip_all =>
"Xid thresholds not as expected: got datfrozenxid = $datfrozenxid, relfrozenxid = $relfrozenxid";
@@ -334,17 +345,21 @@ if ($datfrozenxid <= 3 || $datfrozenxid >= $relfrozenxid)
# Find where each of the tuples is located on the page. If a particular
# line pointer is a redirect rather than a tuple, we record the offset as -1.
-my @lp_off = split '\n', $node->safe_psql(
- 'postgres', qq(
+my $lp_off_res = $session->query(
+ qq(
SELECT CASE WHEN lp_flags = 2 THEN -1 ELSE lp_off END
FROM heap_page_items(get_raw_page('test', 'main', 0))
)
-);
+ );
+my @lp_off;
+push(@lp_off, $_->[0]) foreach @{$lp_off_res->{rows}};
+
scalar @lp_off == $ROWCOUNT or BAIL_OUT("row offset counts mismatch");
# Sanity check that our 'test' table on disk layout matches expectations. If
# this is not so, we will have to skip the test until somebody updates the test
# to work on this platform.
+$session->close;
$node->stop;
my $file;
open($file, '+<', $relpath)
@@ -751,17 +766,19 @@ for (my $tupidx = 0; $tupidx < $ROWCOUNT; $tupidx++)
close($file)
or BAIL_OUT("close failed: $!");
$node->start;
+$session->reconnect;
# Run pg_amcheck against the corrupt table with epoch=0, comparing actual
# corruption messages against the expected messages
$node->command_checks_all(
[ 'pg_amcheck', '--no-dependent-indexes', '--port' => $port, 'postgres' ],
2, [@expected], [], 'Expected corruption message output');
-$node->safe_psql(
- 'postgres', qq(
+$session->do(
+ qq(
COMMIT PREPARED 'in_progress_tx';
));
+$session->close;
$node->teardown_node;
$node->clean_node;
diff --git a/src/bin/pg_upgrade/t/007_multixact_conversion.pl b/src/bin/pg_upgrade/t/007_multixact_conversion.pl
index 867a0623153..67e8f9334a4 100644
--- a/src/bin/pg_upgrade/t/007_multixact_conversion.pl
+++ b/src/bin/pg_upgrade/t/007_multixact_conversion.pl
@@ -15,6 +15,7 @@ use warnings FATAL => 'all';
use Math::BigInt;
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -38,10 +39,8 @@ my $tempdir = PostgreSQL::Test::Utils::tempdir;
# versions.
#
# The first argument is the cluster to connect to, the second argument
-# is a cluster using the new version. We need the 'psql' binary from
-# the new version, the new cluster is otherwise unused. (We need to
-# use the new 'psql' because some of the more advanced background psql
-# perl module features depend on a fairly recent psql version.)
+# is a cluster using the new version. We need the libpq library from
+# the new version (for Session), the new cluster is otherwise unused.
sub mxact_workload
{
my $node = shift; # Cluster to connect to
@@ -79,18 +78,15 @@ sub mxact_workload
# in each connection.
for (0 .. $nclients)
{
- # Use the psql binary from the new installation. The
- # BackgroundPsql functionality doesn't work with older psql
- # versions.
- my $conn = $binnode->background_psql(
- '',
+ # Use the libpq/Session infrastructure from the new installation.
+ my $conn = PostgreSQL::Test::Session->new(
connstr => $node->connstr('postgres'),
- timeout => $connection_timeout_secs);
+ libdir => $binnode->config_data('--libdir'));
- $conn->query_safe("SET log_statement=none", verbose => $verbose)
+ $conn->do("SET log_statement=none")
unless $verbose;
- $conn->query_safe("SET enable_seqscan=off", verbose => $verbose);
- $conn->query_safe("BEGIN", verbose => $verbose);
+ $conn->do("SET enable_seqscan=off");
+ $conn->do("BEGIN");
push(@connections, $conn);
}
@@ -108,9 +104,9 @@ sub mxact_workload
my $conn = $connections[ $i % $nclients ];
my $sql = ($i % $abort_every == 0) ? "ABORT" : "COMMIT";
- $conn->query_safe($sql, verbose => $verbose);
+ $conn->do($sql);
- $conn->query_safe("BEGIN", verbose => $verbose);
+ $conn->do("BEGIN");
if ($i % $update_every == 0)
{
$sql = qq[
@@ -126,12 +122,12 @@ sub mxact_workload
) as x
];
}
- $conn->query_safe($sql, verbose => $verbose);
+ $conn->do($sql);
}
for my $conn (@connections)
{
- $conn->quit();
+ $conn->close;
}
$node->stop;
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 69ed4919b16..ac2aa3ce23e 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -13,6 +13,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
if (!$use_unix_sockets)
@@ -185,36 +186,18 @@ my $res = $node->safe_psql(
WHERE rolname = 'scram_role_iter'");
is($res, 'SCRAM-SHA-256$1024:', 'scram_iterations in server side ROLE');
-# If we don't have IO::Pty, forget it, because IPC::Run depends on that
-# to support pty connections. Also skip if IPC::Run isn't at least 0.98
-# as earlier version cause the session to time out.
-SKIP:
-{
- skip "IO::Pty and IPC::Run >= 0.98 required", 1
- unless eval { require IO::Pty; IPC::Run->VERSION('0.98'); };
+# set password using PQchangePassword
+my $session = PostgreSQL::Test::Session->new (node => $node);
- # Alter the password on the created role using \password in psql to ensure
- # that clientside password changes use the scram_iterations value when
- # calculating SCRAM secrets.
- my $session = $node->interactive_psql('postgres');
-
- $session->set_query_timer_restart();
- $session->query("SET password_encryption='scram-sha-256';");
- $session->query("SET scram_iterations=42;");
- $session->query_until(qr/Enter new password/,
- "\\password scram_role_iter\n");
- $session->query_until(qr/Enter it again/, "pass\n");
- $session->query_until(qr/postgres=# /, "pass\n");
- $session->quit;
-
- $res = $node->safe_psql(
- 'postgres',
+$session->do("SET password_encryption='scram-sha-256';",
+ "SET scram_iterations=42;");
+$res = $session->set_password("scram_role_iter","pass");
+is($res->{status}, 1, "set password ok");
+$res = $session->query_oneval(
"SELECT substr(rolpassword,1,17)
FROM pg_authid
WHERE rolname = 'scram_role_iter'");
- is($res, 'SCRAM-SHA-256$42:',
- 'scram_iterations in psql \password command');
-}
+is($res, 'SCRAM-SHA-256$42:', 'scram_iterations correct');
# Create a database to test regular expression.
$node->safe_psql('postgres', "CREATE database regex_testdb;");
diff --git a/src/test/authentication/t/007_pre_auth.pl b/src/test/authentication/t/007_pre_auth.pl
index 04063f4721d..17de92bc41a 100644
--- a/src/test/authentication/t/007_pre_auth.pl
+++ b/src/test/authentication/t/007_pre_auth.pl
@@ -7,6 +7,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Time::HiRes qw(usleep);
use Test::More;
@@ -36,24 +37,27 @@ if (!$node->check_extension('injection_points'))
$node->safe_psql('postgres', 'CREATE EXTENSION injection_points');
# Connect to the server and inject a waitpoint.
-my $psql = $node->background_psql('postgres');
-$psql->query_safe("SELECT injection_points_attach('init-pre-auth', 'wait')");
+my $session = PostgreSQL::Test::Session->new(node => $node);
+$session->do("SELECT injection_points_attach('init-pre-auth', 'wait')");
# From this point on, all new connections will hang during startup, just before
-# authentication. Use the $psql connection handle for server interaction.
-my $conn = $node->background_psql('postgres', wait => 0);
+# authentication. Use the $session connection handle for server interaction.
+my $conn = PostgreSQL::Test::Session->new(node => $node, wait => 0);
# Wait for the connection to show up in pg_stat_activity, with the wait_event
-# of the injection point.
+# of the injection point. We need to poll the async connection to drive it forward.
my $pid;
while (1)
{
- $pid = $psql->query(
+ # Drive the async connection forward - it won't progress without polling
+ $conn->poll_connect();
+
+ $pid = $session->query_oneval(
qq{SELECT pid FROM pg_stat_activity
WHERE backend_type = 'client backend'
AND state = 'starting'
- AND wait_event = 'init-pre-auth';});
- last if $pid ne "";
+ AND wait_event = 'init-pre-auth';}, 1);
+ last if defined $pid && $pid ne "";
usleep(100_000);
}
@@ -62,24 +66,24 @@ note "backend $pid is authenticating";
ok(1, 'authenticating connections are recorded in pg_stat_activity');
# Detach the waitpoint and wait for the connection to complete.
-$psql->query_safe("SELECT injection_points_wakeup('init-pre-auth');");
+$session->do("SELECT injection_points_wakeup('init-pre-auth')");
$conn->wait_connect();
# Make sure the pgstat entry is updated eventually.
while (1)
{
my $state =
- $psql->query("SELECT state FROM pg_stat_activity WHERE pid = $pid;");
- last if $state eq "idle";
+ $session->query_oneval("SELECT state FROM pg_stat_activity WHERE pid = $pid");
+ last if defined $state && $state eq "idle";
- note "state for backend $pid is '$state'; waiting for 'idle'...";
+ note "state for backend $pid is '" . ($state // 'undef') . "'; waiting for 'idle'...";
usleep(100_000);
}
ok(1, 'authenticated connections reach idle state in pg_stat_activity');
-$psql->query_safe("SELECT injection_points_detach('init-pre-auth');");
-$psql->quit();
-$conn->quit();
+$session->do("SELECT injection_points_detach('init-pre-auth')");
+$session->close();
+$conn->close();
done_testing();
diff --git a/src/test/modules/oauth_validator/t/001_server.pl b/src/test/modules/oauth_validator/t/001_server.pl
index 1619fbffd45..3d4eb1bfe30 100644
--- a/src/test/modules/oauth_validator/t/001_server.pl
+++ b/src/test/modules/oauth_validator/t/001_server.pl
@@ -12,6 +12,7 @@ use warnings FATAL => 'all';
use JSON::PP qw(encode_json);
use MIME::Base64 qw(encode_base64);
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -57,7 +58,7 @@ $node->safe_psql('postgres', 'CREATE USER testalt;');
$node->safe_psql('postgres', 'CREATE USER testparam;');
# Save a background connection for later configuration changes.
-my $bgconn = $node->background_psql('postgres');
+my $bgconn = PostgreSQL::Test::Session->new(node => $node);
my $webserver = OAuth::Server->new();
$webserver->run();
@@ -124,11 +125,11 @@ $log_start =
$node->wait_for_log(qr/reloading configuration files/, $log_start);
# Check pg_hba_file_rules() support.
-my $contents = $bgconn->query_safe(
+my $contents = $bgconn->query(
qq(SELECT rule_number, auth_method, options
FROM pg_hba_file_rules
ORDER BY rule_number;));
-is( $contents,
+is( $contents->{psqlout},
qq{1|oauth|\{issuer=$issuer,"scope=openid postgres",validator=validator\}
2|oauth|\{issuer=$issuer/.well-known/oauth-authorization-server/alternate,"scope=openid postgres alt",validator=validator\}
3|oauth|\{issuer=$issuer/param,"scope=openid postgres",validator=validator\}},
@@ -551,7 +552,7 @@ $common_connstr =
"dbname=postgres oauth_issuer=$issuer/.well-known/openid-configuration oauth_scope='' oauth_client_id=f02c6361-0635";
# Misbehaving validators must fail shut.
-$bgconn->query_safe("ALTER SYSTEM SET oauth_validator.authn_id TO ''");
+$bgconn->do("ALTER SYSTEM SET oauth_validator.authn_id TO ''");
$node->reload;
$log_start =
$node->wait_for_log(qr/reloading configuration files/, $log_start);
@@ -562,15 +563,15 @@ $node->connect_fails(
expected_stderr => qr/OAuth bearer authentication failed/,
log_like => [
qr/connection authenticated: identity=""/,
- qr/FATAL:\s+OAuth bearer authentication failed/,
+ qr/FATAL: ( [A-Z0-9]+:)? OAuth bearer authentication failed/,
qr/DETAIL:\s+Validator provided no identity/,
]);
# Even if a validator authenticates the user, if the token isn't considered
# valid, the connection fails.
-$bgconn->query_safe(
+$bgconn->do(
"ALTER SYSTEM SET oauth_validator.authn_id TO 'test\@example.org'");
-$bgconn->query_safe(
+$bgconn->do(
"ALTER SYSTEM SET oauth_validator.authorize_tokens TO false");
$node->reload;
$log_start =
@@ -582,7 +583,7 @@ $node->connect_fails(
expected_stderr => qr/OAuth bearer authentication failed/,
log_like => [
qr/connection authenticated: identity="test\@example\.org"/,
- qr/FATAL:\s+OAuth bearer authentication failed/,
+ qr/FATAL: ( [A-Z0-9]+:)? OAuth bearer authentication failed/,
qr/DETAIL:\s+Validator failed to authorize the provided token/,
]);
@@ -664,8 +665,8 @@ local all testparam oauth issuer="$issuer" scope="" delegate_ident_mapping=1
});
# To start, have the validator use the role names as authn IDs.
-$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authn_id");
-$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authorize_tokens");
+$bgconn->do("ALTER SYSTEM RESET oauth_validator.authn_id");
+$bgconn->do("ALTER SYSTEM RESET oauth_validator.authorize_tokens");
$node->reload;
$log_start =
@@ -682,7 +683,7 @@ $node->connect_fails(
expected_stderr => qr/OAuth bearer authentication failed/);
# Have the validator identify the end user as [email protected].
-$bgconn->query_safe(
+$bgconn->do(
"ALTER SYSTEM SET oauth_validator.authn_id TO 'user\@example.com'");
$node->reload;
$log_start =
@@ -706,7 +707,7 @@ $node->connect_ok(
expected_stderr =>
qr@Visit https://example\.com/ and enter the code: postgresuser@);
-$bgconn->query_safe("ALTER SYSTEM RESET oauth_validator.authn_id");
+$bgconn->do("ALTER SYSTEM RESET oauth_validator.authn_id");
$node->reload;
$log_start =
$node->wait_for_log(qr/reloading configuration files/, $log_start);
@@ -829,7 +830,7 @@ $user = "testalt";
$node->connect_fails(
"user=$user dbname=postgres oauth_issuer=$issuer/.well-known/oauth-authorization-server/alternate oauth_client_id=f02c6361-0636",
"fail_validator is used for $user",
- expected_stderr => qr/FATAL:\s+fail_validator: sentinel error/);
+ expected_stderr => qr/FATAL: ( [A-Z0-9]+:)? fail_validator: sentinel error/);
#
# Test ABI compatibility magic marker
@@ -849,7 +850,7 @@ $node->connect_fails(
"user=test dbname=postgres oauth_issuer=$issuer/.well-known/oauth-authorization-server/alternate oauth_client_id=f02c6361-0636",
"magic_validator is used for $user",
expected_stderr =>
- qr/FATAL:\s+OAuth validator module "magic_validator": magic number mismatch/
+ qr/FATAL: ( [A-Z0-9]+:)? OAuth validator module "magic_validator": magic number mismatch/
);
$node->stop;
diff --git a/src/test/modules/test_aio/t/001_aio.pl b/src/test/modules/test_aio/t/001_aio.pl
index 63cadd64c15..bf946801601 100644
--- a/src/test/modules/test_aio/t/001_aio.pl
+++ b/src/test/modules/test_aio/t/001_aio.pl
@@ -4,6 +4,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -62,19 +63,19 @@ sub psql_like
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
my $io_method = shift;
- my $psql = shift;
+ my $session = shift;
my $name = shift;
my $sql = shift;
my $expected_stdout = shift;
my $expected_stderr = shift;
- my ($cmdret, $output);
- ($output, $cmdret) = $psql->query($sql);
+ my $res = $session->query($sql);
+ my $output = $res->{psqlout};
like($output, $expected_stdout, "$io_method: $name: expected stdout");
- like($psql->{stderr}, $expected_stderr,
+ like($session->get_stderr(), $expected_stderr,
"$io_method: $name: expected stderr");
- $psql->{stderr} = '';
+ $session->clear_stderr();
return $output;
}
@@ -87,17 +88,15 @@ sub query_wait_block
local $Test::Builder::Level = $Test::Builder::Level + 1;
my $io_method = shift;
my $node = shift;
- my $psql = shift;
+ my $session = shift;
my $name = shift;
my $sql = shift;
my $waitfor = shift;
my $wait_current_session = shift;
- my $pid = $psql->query_safe('SELECT pg_backend_pid()');
+ my $pid = $session->backend_pid();
- $psql->{stdin} .= qq($sql;\n);
- $psql->{run}->pump_nb();
- note "issued sql: $sql;\n";
+ $session->do_async($sql);
ok(1, "$io_method: $name: issued sql");
my $waitquery;
@@ -163,7 +162,7 @@ sub test_handle
my $io_method = shift;
my $node = shift;
- my $psql = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql = PostgreSQL::Test::Session->new(node => $node);
# leak warning: implicit xact
psql_like(
@@ -269,7 +268,7 @@ sub test_batchmode
my $io_method = shift;
my $node = shift;
- my $psql = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql = PostgreSQL::Test::Session->new(node => $node);
# In a build with RELCACHE_FORCE_RELEASE and CATCACHE_FORCE_RELEASE, just
# using SELECT batch_start() causes spurious test failures, because the
@@ -329,7 +328,7 @@ sub test_io_error
my $node = shift;
my ($ret, $output);
- my $psql = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql = PostgreSQL::Test::Session->new(node => $node);
$psql->query_safe(
qq(
@@ -381,8 +380,8 @@ sub test_startwait_io
my $node = shift;
my ($ret, $output);
- my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
- my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql_a = PostgreSQL::Test::Session->new(node => $node);
+ my $psql_b = PostgreSQL::Test::Session->new(node => $node);
### Verify behavior for normal tables
@@ -441,7 +440,7 @@ sub test_startwait_io
# Because the IO was terminated, but not marked as valid, second session should get the right to start io
- pump_until($psql_b->{run}, $psql_b->{timeout}, \$psql_b->{stdout}, qr/t/);
+ $psql_b->wait_for_async_pattern(qr/t/);
ok(1, "$io_method: blocking start buffer io, can start io");
# terminate the IO again
@@ -479,7 +478,7 @@ sub test_startwait_io
qr/^$/);
# Because the IO was terminated, and marked as valid, second session should complete but not need io
- pump_until($psql_b->{run}, $psql_b->{timeout}, \$psql_b->{stdout}, qr/f/);
+ $psql_b->wait_for_async_pattern(qr/f/);
ok(1, "$io_method: blocking start buffer io, no need to start io");
# buffer is valid now, make it invalid again
@@ -558,8 +557,8 @@ sub test_complete_foreign
my $node = shift;
my ($ret, $output);
- my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
- my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql_a = PostgreSQL::Test::Session->new(node => $node);
+ my $psql_b = PostgreSQL::Test::Session->new(node => $node);
# Issue IO without waiting for completion, then sleep
$psql_a->query_safe(
@@ -628,7 +627,7 @@ sub test_close_fd
my $node = shift;
my ($ret, $output);
- my $psql = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql = PostgreSQL::Test::Session->new(node => $node);
psql_like(
$io_method,
@@ -678,7 +677,7 @@ sub test_inject
my $node = shift;
my ($ret, $output);
- my $psql = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql = PostgreSQL::Test::Session->new(node => $node);
# injected what we'd expect
$psql->query_safe(qq(SELECT inj_io_short_read_attach(8192);));
@@ -812,7 +811,7 @@ sub test_inject_worker
my $node = shift;
my ($ret, $output);
- my $psql = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql = PostgreSQL::Test::Session->new(node => $node);
# trigger a failure to reopen, should error out, but should recover
$psql->query_safe(
@@ -849,7 +848,7 @@ sub test_invalidate
my $io_method = shift;
my $node = shift;
- my $psql = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql = PostgreSQL::Test::Session->new(node => $node);
foreach my $persistency (qw(normal unlogged temporary))
{
@@ -907,8 +906,8 @@ sub test_zero
my $io_method = shift;
my $node = shift;
- my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
- my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql_a = PostgreSQL::Test::Session->new(node => $node);
+ my $psql_b = PostgreSQL::Test::Session->new(node => $node);
foreach my $persistency (qw(normal temporary))
{
@@ -933,7 +932,7 @@ SELECT modify_rel_block('tbl_zero', 0, corrupt_header=>true);
qq(
SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>false)),
qr/^$/,
- qr/^psql:<stdin>:\d+: ERROR: invalid page in block 0 of relation "base\/.*\/.*$/
+ qr/^(?:psql:<stdin>:\d+: )?ERROR: invalid page in block 0 of relation "base\/.*\/.*$/
);
# Check that page validity errors are zeroed
@@ -944,7 +943,7 @@ SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>false)),
qq(
SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>true)),
qr/^$/,
- qr/^psql:<stdin>:\d+: WARNING: invalid page in block 0 of relation "base\/.*\/.*"; zeroing out page$/
+ qr/^(?:psql:<stdin>:\d+: )?WARNING: invalid page in block 0 of relation "base\/.*\/.*"; zeroing out page$/
);
# And that once the corruption is fixed, we can read again
@@ -952,7 +951,7 @@ SELECT read_rel_block_ll('tbl_zero', 0, zero_on_error=>true)),
qq(
SELECT modify_rel_block('tbl_zero', 0, zero=>true);
));
- $psql_a->{stderr} = '';
+ $psql_a->clear_stderr();
psql_like(
$io_method,
@@ -975,7 +974,7 @@ SELECT modify_rel_block('tbl_zero', 3, corrupt_header=>true);
"$persistency: test zeroing of invalid block 3",
qq(SELECT read_rel_block_ll('tbl_zero', 3, zero_on_error=>true);),
qr/^$/,
- qr/^psql:<stdin>:\d+: WARNING: invalid page in block 3 of relation "base\/.*\/.*"; zeroing out page$/
+ qr/^(?:psql:<stdin>:\d+: )?WARNING: invalid page in block 3 of relation "base\/.*\/.*"; zeroing out page$/
);
@@ -992,7 +991,7 @@ SELECT modify_rel_block('tbl_zero', 3, corrupt_header=>true);
"$persistency: test reading of invalid block 2,3 in larger read",
qq(SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>false)),
qr/^$/,
- qr/^psql:<stdin>:\d+: ERROR: 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL: Block 2 held the first invalid page\.\nHINT:[^\n]+$/
+ qr/^(?:psql:<stdin>:\d+: )?ERROR: 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL: Block 2 held the first invalid page\.\nHINT:[^\n]+$/
);
# Then test zeroing via ZERO_ON_ERROR flag
@@ -1002,7 +1001,7 @@ SELECT modify_rel_block('tbl_zero', 3, corrupt_header=>true);
"$persistency: test zeroing of invalid block 2,3 in larger read, ZERO_ON_ERROR",
qq(SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>true)),
qr/^$/,
- qr/^psql:<stdin>:\d+: WARNING: zeroing out 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL: Block 2 held the first zeroed page\.\nHINT:[^\n]+$/
+ qr/^(?:psql:<stdin>:\d+: )?WARNING: zeroing out 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL: Block 2 held the first zeroed page\.\nHINT:[^\n]+$/
);
# Then test zeroing via zero_damaged_pages
@@ -1017,7 +1016,7 @@ SELECT read_rel_block_ll('tbl_zero', 1, nblocks=>4, zero_on_error=>false)
COMMIT;
),
qr/^$/,
- qr/^psql:<stdin>:\d+: WARNING: zeroing out 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL: Block 2 held the first zeroed page\.\nHINT:[^\n]+$/
+ qr/^(?:psql:<stdin>:\d+: )?WARNING: zeroing out 2 invalid pages among blocks 1..4 of relation "base\/.*\/.*\nDETAIL: Block 2 held the first zeroed page\.\nHINT:[^\n]+$/
);
$psql_a->query_safe(qq(COMMIT));
@@ -1030,7 +1029,7 @@ SELECT invalidate_rel_block('tbl_zero', g.i)
FROM generate_series(0, 15) g(i);
SELECT modify_rel_block('tbl_zero', 3, zero=>true);
));
- $psql_a->{stderr} = '';
+ $psql_a->clear_stderr();
psql_like(
$io_method,
@@ -1039,7 +1038,7 @@ SELECT modify_rel_block('tbl_zero', 3, zero=>true);
qq(
SELECT count(*) FROM tbl_zero),
qr/^$/,
- qr/^psql:<stdin>:\d+: ERROR: invalid page in block 2 of relation "base\/.*\/.*$/
+ qr/^(?:psql:<stdin>:\d+: )?ERROR: invalid page in block 2 of relation "base\/.*\/.*$/
);
# Verify that bufmgr.c IO zeroes out pages with page validity errors
@@ -1054,7 +1053,7 @@ SELECT count(*) FROM tbl_zero;
COMMIT;
),
qr/^\d+$/,
- qr/^psql:<stdin>:\d+: WARNING: invalid page in block 2 of relation "base\/.*\/.*$/
+ qr/^(?:psql:<stdin>:\d+: )?WARNING: invalid page in block 2 of relation "base\/.*\/.*$/
);
# Check that warnings/errors about page validity in an IO started by
@@ -1093,7 +1092,7 @@ DROP TABLE tbl_zero;
));
}
- $psql_a->{stderr} = '';
+ $psql_a->clear_stderr();
$psql_a->quit();
$psql_b->quit();
@@ -1105,19 +1104,17 @@ sub test_checksum
my $io_method = shift;
my $node = shift;
- my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql_a = PostgreSQL::Test::Session->new(node => $node);
- $psql_a->query_safe(
- qq(
-CREATE TABLE tbl_normal(id int) WITH (AUTOVACUUM_ENABLED = false);
-INSERT INTO tbl_normal SELECT generate_series(1, 5000);
-SELECT modify_rel_block('tbl_normal', 3, corrupt_checksum=>true);
-
-CREATE TEMPORARY TABLE tbl_temp(id int) WITH (AUTOVACUUM_ENABLED = false);
-INSERT INTO tbl_temp SELECT generate_series(1, 5000);
-SELECT modify_rel_block('tbl_temp', 3, corrupt_checksum=>true);
-SELECT modify_rel_block('tbl_temp', 4, corrupt_checksum=>true);
-));
+ # Split multi-statement query into separate calls to match psql behavior
+ # where errors in one statement don't prevent subsequent statements
+ $psql_a->query_safe(qq(CREATE TABLE tbl_normal(id int) WITH (AUTOVACUUM_ENABLED = false)));
+ $psql_a->query_safe(qq(INSERT INTO tbl_normal SELECT generate_series(1, 5000)));
+ $psql_a->query_safe(qq(SELECT modify_rel_block('tbl_normal', 3, corrupt_checksum=>true)));
+ $psql_a->query_safe(qq(CREATE TEMPORARY TABLE tbl_temp(id int) WITH (AUTOVACUUM_ENABLED = false)));
+ $psql_a->query_safe(qq(INSERT INTO tbl_temp SELECT generate_series(1, 5000)));
+ $psql_a->query_safe(qq(SELECT modify_rel_block('tbl_temp', 3, corrupt_checksum=>true)));
+ $psql_a->query_safe(qq(SELECT modify_rel_block('tbl_temp', 4, corrupt_checksum=>true)));
# To be able to test checksum failures on shared rels we need a shared rel
# with invalid pages - which is a bit scary. pg_shseclabel seems like a
@@ -1140,7 +1137,7 @@ SELECT modify_rel_block('pg_shseclabel', 3, corrupt_checksum=>true);
qq(
SELECT read_rel_block_ll('tbl_normal', 3, nblocks=>1, zero_on_error=>false);),
qr/^$/,
- qr/^psql:<stdin>:\d+: ERROR: invalid page in block 3 of relation "base\/\d+\/\d+"$/
+ qr/^(?:psql:<stdin>:\d+: )?ERROR: invalid page in block 3 of relation "base\/\d+\/\d+"$/
);
my ($cs_count_after, $cs_ts_after) =
@@ -1162,7 +1159,7 @@ SELECT read_rel_block_ll('tbl_normal', 3, nblocks=>1, zero_on_error=>false);),
qq(
SELECT read_rel_block_ll('tbl_temp', 4, nblocks=>2, zero_on_error=>false);),
qr/^$/,
- qr/^psql:<stdin>:\d+: ERROR: invalid page in block 4 of relation "base\/\d+\/t\d+_\d+"$/
+ qr/^(?:psql:<stdin>:\d+: )?ERROR: invalid page in block 4 of relation "base\/\d+\/t\d+_\d+"$/
);
($cs_count_after, $cs_ts_after) = checksum_failures($psql_a, 'postgres');
@@ -1183,7 +1180,7 @@ SELECT read_rel_block_ll('tbl_temp', 4, nblocks=>2, zero_on_error=>false);),
qq(
SELECT read_rel_block_ll('pg_shseclabel', 2, nblocks=>2, zero_on_error=>false);),
qr/^$/,
- qr/^psql:<stdin>:\d+: ERROR: 2 invalid pages among blocks 2..3 of relation "global\/\d+"\nDETAIL: Block 2 held the first invalid page\.\nHINT:[^\n]+$/
+ qr/^(?:psql:<stdin>:\d+: )?ERROR: 2 invalid pages among blocks 2..3 of relation "global\/\d+"\nDETAIL: Block 2 held the first invalid page\.\nHINT:[^\n]+$/
);
($cs_count_after, $cs_ts_after) = checksum_failures($psql_a);
@@ -1201,7 +1198,7 @@ SELECT read_rel_block_ll('pg_shseclabel', 2, nblocks=>2, zero_on_error=>false);)
SELECT modify_rel_block('pg_shseclabel', 1, zero=>true);
DROP TABLE tbl_normal;
));
- $psql_a->{stderr} = '';
+ $psql_a->clear_stderr();
$psql_a->quit();
}
@@ -1214,7 +1211,7 @@ sub test_checksum_createdb
my $io_method = shift;
my $node = shift;
- my $psql = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql = PostgreSQL::Test::Session->new(node => $node);
$node->safe_psql('postgres',
'CREATE DATABASE regression_createdb_source');
@@ -1248,7 +1245,7 @@ STRATEGY wal_log;
"create database w/ wal strategy, invalid source",
$createdb_sql,
qr/^$/,
- qr/psql:<stdin>:\d+: ERROR: invalid page in block 1 of relation "base\/\d+\/\d+"$/
+ qr/^(?:psql:<stdin>:\d+: )?ERROR: invalid page in block 1 of relation "base\/\d+\/\d+"$/
);
my ($cs_count_after, $cs_ts_after) =
checksum_failures($psql, 'regression_createdb_source');
@@ -1277,7 +1274,7 @@ sub test_ignore_checksum
my $io_method = shift;
my $node = shift;
- my $psql = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql = PostgreSQL::Test::Session->new(node => $node);
# Test setup
$psql->query_safe(
@@ -1342,7 +1339,7 @@ SELECT modify_rel_block('tbl_cs_fail', 4, corrupt_header=>true);
qq(
SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>false);),
qr/^$/,
- qr/^psql:<stdin>:\d+: WARNING: ignoring checksum failure in block 3/
+ qr/^(?:psql:<stdin>:\d+: )?WARNING: ignoring checksum failure in block 3/
);
# Check that the log contains a LOG message about the failure
@@ -1357,7 +1354,7 @@ SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>false);),
qq(
SELECT read_rel_block_ll('tbl_cs_fail', 2, nblocks=>3, zero_on_error=>false);),
qr/^$/,
- qr/^psql:<stdin>:\d+: ERROR: invalid page in block 4 of relation "base\/\d+\/\d+"$/
+ qr/^(?:psql:<stdin>:\d+: )?ERROR: invalid page in block 4 of relation "base\/\d+\/\d+"$/
);
# Test multi-block read with different problems in different blocks
@@ -1369,7 +1366,7 @@ SELECT modify_rel_block('tbl_cs_fail', 3, corrupt_checksum=>true, corrupt_header
SELECT modify_rel_block('tbl_cs_fail', 4, corrupt_header=>true);
SELECT modify_rel_block('tbl_cs_fail', 5, corrupt_header=>true);
));
- $psql->{stderr} = '';
+ $psql->clear_stderr();
$log_location = -s $node->logfile;
psql_like(
@@ -1379,7 +1376,7 @@ SELECT modify_rel_block('tbl_cs_fail', 5, corrupt_header=>true);
qq(
SELECT read_rel_block_ll('tbl_cs_fail', 1, nblocks=>5, zero_on_error=>true);),
qr/^$/,
- qr/^psql:<stdin>:\d+: WARNING: zeroing 3 page\(s\) and ignoring 2 checksum failure\(s\) among blocks 1..5 of relation "/
+ qr/^(?:psql:<stdin>:\d+: )?WARNING: zeroing 3 page\(s\) and ignoring 2 checksum failure\(s\) among blocks 1..5 of relation "/
);
@@ -1412,7 +1409,7 @@ SELECT read_rel_block_ll('tbl_cs_fail', 1, nblocks=>5, zero_on_error=>true);),
qq(
SELECT modify_rel_block('tbl_cs_fail', 3, corrupt_checksum=>true, corrupt_header=>true);
));
- $psql->{stderr} = '';
+ $psql->clear_stderr();
psql_like(
$io_method,
@@ -1421,7 +1418,7 @@ SELECT modify_rel_block('tbl_cs_fail', 3, corrupt_checksum=>true, corrupt_header
qq(
SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>false);),
qr/^$/,
- qr/^psql:<stdin>:\d+: ERROR: invalid page in block 3 of relation "/);
+ qr/^(?:psql:<stdin>:\d+: )?ERROR: invalid page in block 3 of relation "/);
psql_like(
$io_method,
@@ -1430,7 +1427,7 @@ SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>false);),
qq(
SELECT read_rel_block_ll('tbl_cs_fail', 3, nblocks=>1, zero_on_error=>true);),
qr/^$/,
- qr/^psql:<stdin>:\d+: WARNING: invalid page in block 3 of relation "base\/.*"; zeroing out page/
+ qr/^(?:psql:<stdin>:\d+: )?WARNING: invalid page in block 3 of relation "base\/.*"; zeroing out page/
);
@@ -1446,8 +1443,8 @@ sub test_read_buffers
my ($ret, $output);
my $table;
- my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
- my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql_a = PostgreSQL::Test::Session->new(node => $node);
+ my $psql_b = PostgreSQL::Test::Session->new(node => $node);
$psql_a->query_safe(
qq(
@@ -1631,7 +1628,7 @@ INSERT INTO tmp_ok SELECT generate_series(1, 5000);
$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
my $buf_id =
- $psql_b->query_safe(qq|SELECT buffer_create_toy('$table', 3)|);
+ $psql_b->query_oneval(qq|SELECT buffer_create_toy('$table', 3)|);
$psql_b->query_safe(
qq|SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true)|
);
@@ -1648,16 +1645,16 @@ INSERT INTO tmp_ok SELECT generate_series(1, 5000);
qq|SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>false, io_error=>false, release_aio=>false)|
);
# Because no IO wref was assigned, block 3 should not report foreign IO
- pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout},
- qr/0\|1\|t\|f\|2\n2\|3\|t\|f\|3/);
- ok(1,
+ like(
+ $psql_a->wait_for_async_pattern(qr/0\|1\|t\|f\|2\n2\|3\|t\|f\|3/),
+ qr/0\|1\|t\|f\|2\n2\|3\|t\|f\|3/,
"$io_method: $persistency: IO was split due to concurrent failed IO"
);
# Same as before, except the concurrent IO succeeds this time
$psql_a->query_safe(qq|SELECT evict_rel('$table')|);
$buf_id =
- $psql_b->query_safe(qq|SELECT buffer_create_toy('$table', 3)|);
+ $psql_b->query_oneval(qq|SELECT buffer_create_toy('$table', 3)|);
$psql_b->query_safe(
qq|SELECT buffer_call_start_io($buf_id, for_input=>true, wait=>true)|
);
@@ -1674,9 +1671,10 @@ INSERT INTO tmp_ok SELECT generate_series(1, 5000);
qq|SELECT buffer_call_terminate_io($buf_id, for_input=>true, succeed=>true, io_error=>false, release_aio=>false)|
);
# Because no IO wref was assigned, block 3 should not report foreign IO
- pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout},
- qr/0\|1\|t\|f\|2\n2\|3\|f\|f\|1\n3\|4\|t\|f\|2/);
- ok(1,
+ like(
+ $psql_a->wait_for_async_pattern(
+ qr/0\|1\|t\|f\|2\n2\|3\|f\|f\|1\n3\|4\|t\|f\|2/),
+ qr/0\|1\|t\|f\|2\n2\|3\|f\|f\|1\n3\|4\|t\|f\|2/,
"$io_method: $persistency: IO was split due to concurrent successful IO"
);
}
@@ -1692,9 +1690,9 @@ sub test_read_buffers_inject
my $io_method = shift;
my $node = shift;
- my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
- my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
- my $psql_c = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql_a = PostgreSQL::Test::Session->new(node => $node);
+ my $psql_b = PostgreSQL::Test::Session->new(node => $node);
+ my $psql_c = PostgreSQL::Test::Session->new(node => $node);
my $expected;
@@ -1763,12 +1761,13 @@ sub test_read_buffers_inject
# return for something with misses in sync mode.
$expected = qr/0\|1\|t\|f\|4/;
}
- pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout},
- $expected);
- ok(1,
+ like($psql_a->wait_for_async_pattern($expected), $expected,
"$io_method: $persistency: read 1-3, blocked on in-progress 1, see expected result"
);
- $psql_a->{stdout} = '';
+
+ # B's low-level read has completed now that C released it; drain its
+ # result before B is reused below.
+ $psql_b->wait_for_completion;
###
@@ -1829,12 +1828,12 @@ read_buffers('$table', 0, 4)|,
# return for something with misses in sync mode.
$expected = qr/0\|0\|t\|f\|4/;
}
- pump_until($psql_a->{run}, $psql_a->{timeout}, \$psql_a->{stdout},
- $expected);
- ok(1,
+ like($psql_a->wait_for_async_pattern($expected), $expected,
"$io_method: $persistency: read 0-3, blocked on in-progress 2+3, see expected result"
);
- $psql_a->{stdout} = '';
+
+ # Drain B's now-completed low-level read before closing.
+ $psql_b->wait_for_completion;
$psql_a->quit();
diff --git a/src/test/modules/test_aio/t/004_read_stream.pl b/src/test/modules/test_aio/t/004_read_stream.pl
index 32311c07ac0..782b62c452f 100644
--- a/src/test/modules/test_aio/t/004_read_stream.pl
+++ b/src/test/modules/test_aio/t/004_read_stream.pl
@@ -5,6 +5,7 @@ use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Session;
use Test::More;
use FindBin;
@@ -60,7 +61,7 @@ sub test_repeated_blocks
my $io_method = shift;
my $node = shift;
- my $psql = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql = PostgreSQL::Test::Session->new(node => $node);
# Preventing larger reads makes testing easier
$psql->query_safe(qq/SET io_combine_limit = 1/);
@@ -111,7 +112,7 @@ sub test_repeated_blocks
ARRAY[0, 2, 2, 4, 4]);/);
ok(1, "$io_method: temp stream hitting the same block repeatedly");
- $psql->quit();
+ $psql->close();
}
@@ -120,10 +121,10 @@ sub test_inject_foreign
my $io_method = shift;
my $node = shift;
- my $psql_a = $node->background_psql('postgres', on_error_stop => 0);
- my $psql_b = $node->background_psql('postgres', on_error_stop => 0);
+ my $psql_a = PostgreSQL::Test::Session->new(node => $node);
+ my $psql_b = PostgreSQL::Test::Session->new(node => $node);
- my $pid_a = $psql_a->query_safe(qq/SELECT pg_backend_pid();/);
+ my $pid_a = $psql_a->query_oneval(qq/SELECT pg_backend_pid();/);
###
@@ -136,9 +137,8 @@ sub test_inject_foreign
qq/SELECT inj_io_completion_wait(pid=>pg_backend_pid(),
relfilenode=>pg_relation_filenode('largeish'));/);
- $psql_b->{stdin} .= qq/SELECT read_rel_block_ll('largeish',
- blockno=>5, nblocks=>1);\n/;
- $psql_b->{run}->pump_nb();
+ $psql_b->do_async(qq/SELECT read_rel_block_ll('largeish',
+ blockno=>5, nblocks=>1);/);
$node->poll_query_until(
'postgres', qq/SELECT wait_event FROM pg_stat_activity
@@ -147,9 +147,8 @@ sub test_inject_foreign
# Block 5 is undergoing IO in session b, so session a will move on to start
# a new IO for block 7.
- $psql_a->{stdin} .= qq/SELECT array_agg(blocknum) FROM
- read_stream_for_blocks('largeish', ARRAY[0, 2, 5, 7]);\n/;
- $psql_a->{run}->pump_nb();
+ $psql_a->do_async(qq/SELECT array_agg(blocknum) FROM
+ read_stream_for_blocks('largeish', ARRAY[0, 2, 5, 7]);/);
$node->poll_query_until('postgres',
qq(SELECT wait_event FROM pg_stat_activity WHERE pid = $pid_a),
@@ -157,10 +156,10 @@ sub test_inject_foreign
$node->safe_psql('postgres', qq/SELECT inj_io_completion_continue()/);
- pump_until(
- $psql_a->{run}, $psql_a->{timeout},
- \$psql_a->{stdout}, qr/\{0,2,5,7\}/);
- $psql_a->{stdout} = '';
+ $psql_a->wait_for_async_pattern(qr/\{0,2,5,7\}/);
+
+ # Drain session b's now-completed low-level read before reusing it.
+ $psql_b->wait_for_completion;
ok(1,
qq/$io_method: read stream encounters succeeding IO by another backend/
@@ -181,9 +180,8 @@ sub test_inject_foreign
pid=>pg_backend_pid(),
relfilenode=>pg_relation_filenode('largeish'));/);
- $psql_b->{stdin} .= qq/SELECT read_rel_block_ll('largeish',
- blockno=>5, nblocks=>1);\n/;
- $psql_b->{run}->pump_nb();
+ $psql_b->do_async(qq/SELECT read_rel_block_ll('largeish',
+ blockno=>5, nblocks=>1);/);
$node->poll_query_until(
'postgres',
@@ -191,9 +189,8 @@ sub test_inject_foreign
WHERE wait_event = 'completion_wait';/,
'completion_wait');
- $psql_a->{stdin} .= qq/SELECT array_agg(blocknum) FROM
- read_stream_for_blocks('largeish', ARRAY[0, 2, 5, 7]);\n/;
- $psql_a->{run}->pump_nb();
+ $psql_a->do_async(qq/SELECT array_agg(blocknum) FROM
+ read_stream_for_blocks('largeish', ARRAY[0, 2, 5, 7]);/);
$node->poll_query_until('postgres',
qq(SELECT wait_event FROM pg_stat_activity WHERE pid = $pid_a),
@@ -201,15 +198,13 @@ sub test_inject_foreign
$node->safe_psql('postgres', qq/SELECT inj_io_completion_continue()/);
- pump_until(
- $psql_a->{run}, $psql_a->{timeout},
- \$psql_a->{stdout}, qr/\{0,2,5,7\}/);
- $psql_a->{stdout} = '';
+ $psql_a->wait_for_async_pattern(qr/\{0,2,5,7\}/);
- pump_until($psql_b->{run}, $psql_b->{timeout}, \$psql_b->{stderr},
- qr/ERROR.*could not read blocks 5\.\.5/);
- ok(1, "$io_method: injected error occurred");
- $psql_b->{stderr} = '';
+ # Session b's low-level read hits the injected error.
+ my $res_b = $psql_b->get_async_result();
+ like($res_b->{error_message}, qr/ERROR.*could not read blocks 5\.\.5/,
+ "$io_method: injected error occurred");
+ $psql_b->clear_stderr();
$psql_b->query_safe(qq/SELECT inj_io_short_read_detach();/);
ok(1,
@@ -226,9 +221,8 @@ sub test_inject_foreign
qq/SELECT inj_io_completion_wait(pid=>pg_backend_pid(),
relfilenode=>pg_relation_filenode('largeish'));/);
- $psql_b->{stdin} .= qq/SELECT read_rel_block_ll('largeish',
- blockno=>2, nblocks=>3);\n/;
- $psql_b->{run}->pump_nb();
+ $psql_b->do_async(qq/SELECT read_rel_block_ll('largeish',
+ blockno=>2, nblocks=>3);/);
$node->poll_query_until(
'postgres',
@@ -237,9 +231,8 @@ sub test_inject_foreign
'completion_wait');
# Blocks 2 and 4 are undergoing IO initiated by session b
- $psql_a->{stdin} .= qq/SELECT array_agg(blocknum) FROM
- read_stream_for_blocks('largeish', ARRAY[0, 2, 4]);\n/;
- $psql_a->{run}->pump_nb();
+ $psql_a->do_async(qq/SELECT array_agg(blocknum) FROM
+ read_stream_for_blocks('largeish', ARRAY[0, 2, 4]);/);
$node->poll_query_until('postgres',
qq(SELECT wait_event FROM pg_stat_activity WHERE pid = $pid_a),
@@ -247,15 +240,15 @@ sub test_inject_foreign
$node->safe_psql('postgres', qq/SELECT inj_io_completion_continue()/);
- pump_until(
- $psql_a->{run}, $psql_a->{timeout},
- \$psql_a->{stdout}, qr/\{0,2,4\}/);
- $psql_a->{stdout} = '';
+ $psql_a->wait_for_async_pattern(qr/\{0,2,4\}/);
+
+ # Drain session b's now-completed low-level read.
+ $psql_b->wait_for_completion;
ok(1, qq/$io_method: read stream encounters two buffer read in one IO/);
- $psql_a->quit();
- $psql_b->quit();
+ $psql_a->close();
+ $psql_b->close();
}
diff --git a/src/test/modules/test_checksums/t/002_restarts.pl b/src/test/modules/test_checksums/t/002_restarts.pl
index 1aa2c0c65e5..cd9a7314c8a 100644
--- a/src/test/modules/test_checksums/t/002_restarts.pl
+++ b/src/test/modules/test_checksums/t/002_restarts.pl
@@ -8,6 +8,7 @@ use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Session;
use Test::More;
use FindBin;
@@ -44,8 +45,8 @@ SKIP:
#
# This is a similar test to the synthetic variant in 005_injection.pl
# which fakes this scenario.
- my $bsession = $node->background_psql('postgres');
- $bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+ my $bsession = PostgreSQL::Test::Session->new(node => $node);
+ $bsession->do('CREATE TEMPORARY TABLE tt (a integer);');
# In another session, make sure we can see the blocking temp table but
# start processing anyways and check that we are blocked with a proper
@@ -85,7 +86,7 @@ SKIP:
# session first since the brief period between closing and stopping might
# be enough for checksums to get enabled.
$node->stop;
- $bsession->quit;
+ $bsession->close;
$node->start;
# Ensure the checksums aren't enabled across the restart. This leaves the
diff --git a/src/test/modules/test_checksums/t/003_standby_restarts.pl b/src/test/modules/test_checksums/t/003_standby_restarts.pl
index bb35ed0b325..67d4e76e7b5 100644
--- a/src/test/modules/test_checksums/t/003_standby_restarts.pl
+++ b/src/test/modules/test_checksums/t/003_standby_restarts.pl
@@ -7,6 +7,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Session;
use Test::More;
use FindBin;
@@ -255,11 +256,11 @@ $node_primary->wait_for_catchup($node_standby, 'replay');
# Open a background psql connection on the primary and inject a barrier to
# block progress on to keep the state from advancing past inprogress-on
-my $node_primary_bpsql = $node_primary->background_psql('postgres');
-$node_primary_bpsql->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+my $node_primary_bpsql = PostgreSQL::Test::Session->new(node => $node_primary);
+$node_primary_bpsql->do('CREATE TEMPORARY TABLE tt (a integer);');
# Also open a background psql connection to the standby to make sure we have
# an active backend during promotion.
-my $node_standby_bpsql = $node_standby->background_psql('postgres');
+my $node_standby_bpsql = PostgreSQL::Test::Session->new(node => $node_standby);
# Start to enable checksums and wait until both primary and standby have moved
# to the inprogress-on state. Processing will block here as the temporary rel
@@ -281,7 +282,11 @@ $result = $node_standby_bpsql->query_safe("SHOW data_checksums;");
is($result, 'off',
'ensure checksums are set to off after promotion during inprogress-on');
-$node_standby_bpsql->quit;
+# The primary's session was kept open only to hold the blocking temp table;
+# close it explicitly (its backend is already gone after the crash) so it is
+# not left to be torn down at global destruction.
+$node_primary_bpsql->close;
+$node_standby_bpsql->close;
$node_standby->stop;
done_testing();
diff --git a/src/test/modules/test_checksums/t/004_offline.pl b/src/test/modules/test_checksums/t/004_offline.pl
index 73c279e75e0..be6b6b6e4c6 100644
--- a/src/test/modules/test_checksums/t/004_offline.pl
+++ b/src/test/modules/test_checksums/t/004_offline.pl
@@ -8,6 +8,7 @@ use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Session;
use Test::More;
use FindBin;
@@ -53,8 +54,8 @@ test_checksum_state($node, 'off');
# can accomplish this by setting up an interactive psql process which keeps the
# temporary table created as we enable checksums in another psql process.
-my $bsession = $node->background_psql('postgres');
-$bsession->query_safe('CREATE TEMPORARY TABLE tt (a integer);');
+my $bsession = PostgreSQL::Test::Session->new(node => $node);
+$bsession->do('CREATE TEMPORARY TABLE tt (a integer);');
# In another session, make sure we can see the blocking temp table but start
# processing anyways and check that we are blocked with a proper wait event.
@@ -70,7 +71,7 @@ enable_data_checksums($node, wait => 'inprogress-on');
# Stop the cluster before exiting the background session since otherwise
# checksums might have time to get enabled before shutting down the cluster.
$node->stop('fast');
-$bsession->quit;
+$bsession->close;
$node->checksum_enable_offline;
$node->start;
diff --git a/src/test/modules/test_misc/t/005_timeouts.pl b/src/test/modules/test_misc/t/005_timeouts.pl
index c16b7dbf5e6..d64a787a9ab 100644
--- a/src/test/modules/test_misc/t/005_timeouts.pl
+++ b/src/test/modules/test_misc/t/005_timeouts.pl
@@ -6,6 +6,7 @@ use warnings FATAL => 'all';
use locale;
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Time::HiRes qw(usleep);
use Test::More;
@@ -42,24 +43,16 @@ $node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
$node->safe_psql('postgres',
"SELECT injection_points_attach('transaction-timeout', 'wait');");
-my $psql_session = $node->background_psql('postgres');
+my $psql_session = PostgreSQL::Test::Session->new(node => $node);
-# The following query will generate a stream of SELECT 1 queries. This is done
-# so to exercise transaction timeout in the presence of short queries.
-# Note: the interval value is parsed with locale-aware strtod()
-$psql_session->query_until(
- qr/starting_bg_psql/,
- sprintf(
- q(\echo starting_bg_psql
- SET transaction_timeout to '10ms';
- BEGIN;
- SELECT 1 \watch %g
- \q
-), 0.001));
+$psql_session->do("SET transaction_timeout to '10ms';");
+
+$psql_session->do_async("BEGIN; DO ' begin loop PERFORM pg_sleep(0.001); end loop; end ';");
# Wait until the backend enters the timeout injection point. Will get an error
# here if anything goes wrong.
$node->wait_for_event('client backend', 'transaction-timeout');
+pass("got transaction timeout event");
my $log_offset = -s $node->logfile;
@@ -70,11 +63,9 @@ $node->safe_psql('postgres',
# Check that the timeout was logged.
$node->wait_for_log('terminating connection due to transaction timeout',
$log_offset);
+pass("got transaction timeout log");
-# If we send \q with $psql_session->quit the command can be sent to the session
-# already closed. So \q is in initial script, here we only finish IPC::Run.
-$psql_session->{run}->finish;
-
+$psql_session->close;
#
# 2. Test of the idle in transaction timeout
@@ -85,10 +76,8 @@ $node->safe_psql('postgres',
);
# We begin a transaction and the hand on the line
-$psql_session = $node->background_psql('postgres');
-$psql_session->query_until(
- qr/starting_bg_psql/, q(
- \echo starting_bg_psql
+$psql_session->reconnect;
+$psql_session->do(q(
SET idle_in_transaction_session_timeout to '10ms';
BEGIN;
));
@@ -96,6 +85,7 @@ $psql_session->query_until(
# Wait until the backend enters the timeout injection point.
$node->wait_for_event('client backend',
'idle-in-transaction-session-timeout');
+pass("got idle in transaction timeout event");
$log_offset = -s $node->logfile;
@@ -106,8 +96,9 @@ $node->safe_psql('postgres',
# Check that the timeout was logged.
$node->wait_for_log(
'terminating connection due to idle-in-transaction timeout', $log_offset);
+pass("got idle in transaction timeout log");
-ok($psql_session->quit);
+$psql_session->close;
#
@@ -117,15 +108,14 @@ $node->safe_psql('postgres',
"SELECT injection_points_attach('idle-session-timeout', 'wait');");
# We just initialize the GUC and wait. No transaction is required.
-$psql_session = $node->background_psql('postgres');
-$psql_session->query_until(
- qr/starting_bg_psql/, q(
- \echo starting_bg_psql
+$psql_session->reconnect;
+$psql_session->do(q(
SET idle_session_timeout to '10ms';
));
# Wait until the backend enters the timeout injection point.
$node->wait_for_event('client backend', 'idle-session-timeout');
+pass("got idle session timeout event");
$log_offset = -s $node->logfile;
@@ -136,7 +126,8 @@ $node->safe_psql('postgres',
# Check that the timeout was logged.
$node->wait_for_log('terminating connection due to idle-session timeout',
$log_offset);
+pass("got idle sesion tiemout log");
-ok($psql_session->quit);
+$psql_session->close;
done_testing();
diff --git a/src/test/modules/test_misc/t/007_catcache_inval.pl b/src/test/modules/test_misc/t/007_catcache_inval.pl
index 424556261c9..e3b3fd8bae9 100644
--- a/src/test/modules/test_misc/t/007_catcache_inval.pl
+++ b/src/test/modules/test_misc/t/007_catcache_inval.pl
@@ -46,12 +46,12 @@ $node->safe_psql(
CREATE FUNCTION foofunc(dummy integer) RETURNS integer AS \$\$ SELECT 1; /* $longtext */ \$\$ LANGUAGE SQL
]);
-my $psql_session = $node->background_psql('postgres');
-my $psql_session2 = $node->background_psql('postgres');
+my $psql_session = PostgreSQL::Test::Session->new(node => $node);
+my $psql_session2 = PostgreSQL::Test::Session->new(node => $node);
# Set injection point in the session, to pause while populating the
# catcache list
-$psql_session->query_safe(
+$psql_session->do(
qq[
SELECT injection_points_set_local();
SELECT injection_points_attach('catcache-list-miss-systable-scan-started', 'wait');
@@ -59,10 +59,9 @@ $psql_session->query_safe(
# This pauses on the injection point while populating catcache list
# for functions with name "foofunc"
-$psql_session->query_until(
- qr/starting_bg_psql/, q(
- \echo starting_bg_psql
- SELECT foofunc(1);
+$psql_session->do_async(
+ q(
+ SELECT foofunc(1);
));
# While the first session is building the catcache list, create a new
@@ -83,16 +82,19 @@ $node->safe_psql(
# trying to exercise here.)
#
# The "SELECT foofunc(1)" query will now finish.
-$psql_session2->query_safe(
+$psql_session2->do(
qq[
SELECT injection_points_wakeup('catcache-list-miss-systable-scan-started');
SELECT injection_points_detach('catcache-list-miss-systable-scan-started');
]);
# Test that the new function is visible to the session.
-$psql_session->query_safe("SELECT foofunc();");
+$psql_session->wait_for_completion;
+my $res = $psql_session->query("SELECT foofunc();");
-ok($psql_session->quit);
-ok($psql_session2->quit);
+is($res->{status}, 2, "got TUPLES_OK");
+
+$psql_session->close;
+$psql_session2->close;
done_testing();
diff --git a/src/test/modules/test_misc/t/010_index_concurrently_upsert.pl b/src/test/modules/test_misc/t/010_index_concurrently_upsert.pl
index 50a0e7db8f7..a7359cb0624 100644
--- a/src/test/modules/test_misc/t/010_index_concurrently_upsert.pl
+++ b/src/test/modules/test_misc/t/010_index_concurrently_upsert.pl
@@ -13,6 +13,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
use Time::HiRes qw(usleep);
@@ -51,47 +52,38 @@ ALTER TABLE test.tblexpr SET (parallel_workers=0);
############################################################################
note('Test: REINDEX CONCURRENTLY + UPSERT (wakeup at set-dead phase)');
-# Create sessions with on_error_stop => 0 so psql doesn't exit on SQL errors.
-# This allows us to collect stderr and detect errors after the test completes.
-my $s1 = $node->background_psql('postgres', on_error_stop => 0);
-my $s2 = $node->background_psql('postgres', on_error_stop => 0);
-my $s3 = $node->background_psql('postgres', on_error_stop => 0);
+# Create sessions for concurrent operations
+my $s1 = PostgreSQL::Test::Session->new(node => $node);
+my $s2 = PostgreSQL::Test::Session->new(node => $node);
+my $s3 = PostgreSQL::Test::Session->new(node => $node);
# Setup injection points for each session
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait');
]);
# s3 starts REINDEX (will block on reindex-relation-concurrently-before-set-dead)
-$s3->query_until(
- qr/starting_reindex/, q[
-\echo starting_reindex
-REINDEX INDEX CONCURRENTLY test.tblpk_pkey;
-]);
+$s3->do_async(q[REINDEX INDEX CONCURRENTLY test.tblpk_pkey;]);
# Wait for s3 to hit injection point
ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead');
# s1 starts UPSERT (will block on check-exclusion-or-unique-constraint-no-conflict)
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
# Wait for s1 to hit injection point
ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict');
@@ -101,11 +93,7 @@ wakeup_injection_point($node,
'reindex-relation-concurrently-before-set-dead');
# s2 starts UPSERT (will block on exec-insert-before-insert-speculative)
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
# Wait for s2 to hit injection point
ok_injection_point($node, 'exec-insert-before-insert-speculative');
@@ -125,51 +113,39 @@ $node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk');
############################################################################
note('Test: REINDEX CONCURRENTLY + UPSERT (wakeup at swap phase)');
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('reindex-relation-concurrently-before-swap', 'wait');
]);
-$s3->query_until(
- qr/starting_reindex/, q[
-\echo starting_reindex
-REINDEX INDEX CONCURRENTLY test.tblpk_pkey;
-]);
+$s3->do_async(q[REINDEX INDEX CONCURRENTLY test.tblpk_pkey;]);
ok_injection_point($node, 'reindex-relation-concurrently-before-swap');
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict');
wakeup_injection_point($node, 'reindex-relation-concurrently-before-swap');
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'exec-insert-before-insert-speculative');
@@ -184,50 +160,38 @@ $node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk');
############################################################################
note('Test: REINDEX CONCURRENTLY + UPSERT (s1 wakes before reindex)');
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait');
]);
-$s3->query_until(
- qr/starting_reindex/, q[
-\echo starting_reindex
-REINDEX INDEX CONCURRENTLY test.tblpk_pkey;
-]);
+$s3->do_async(q[REINDEX INDEX CONCURRENTLY test.tblpk_pkey;]);
ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead');
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict');
# Start s2 BEFORE waking reindex (key difference from permutation 1)
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'exec-insert-before-insert-speculative');
@@ -245,52 +209,40 @@ $node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk');
############################################################################
note('Test: REINDEX + UPSERT ON CONSTRAINT (set-dead phase)');
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait');
]);
-$s3->query_until(
- qr/starting_reindex/, q[
-\echo starting_reindex
-REINDEX INDEX CONCURRENTLY test.tblpk_pkey;
-]);
+$s3->do_async(q[REINDEX INDEX CONCURRENTLY test.tblpk_pkey;]);
ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead');
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict');
wakeup_injection_point($node,
'reindex-relation-concurrently-before-set-dead');
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'exec-insert-before-insert-speculative');
@@ -305,51 +257,39 @@ $node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk');
############################################################################
note('Test: REINDEX + UPSERT ON CONSTRAINT (swap phase)');
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('reindex-relation-concurrently-before-swap', 'wait');
]);
-$s3->query_until(
- qr/starting_reindex/, q[
-\echo starting_reindex
-REINDEX INDEX CONCURRENTLY test.tblpk_pkey;
-]);
+$s3->do_async(q[REINDEX INDEX CONCURRENTLY test.tblpk_pkey;]);
ok_injection_point($node, 'reindex-relation-concurrently-before-swap');
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict');
wakeup_injection_point($node, 'reindex-relation-concurrently-before-swap');
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'exec-insert-before-insert-speculative');
@@ -364,50 +304,38 @@ $node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk');
############################################################################
note('Test: REINDEX + UPSERT ON CONSTRAINT (s1 wakes before reindex)');
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait');
]);
-$s3->query_until(
- qr/starting_reindex/, q[
-\echo starting_reindex
-REINDEX INDEX CONCURRENTLY test.tblpk_pkey;
-]);
+$s3->do_async(q[REINDEX INDEX CONCURRENTLY test.tblpk_pkey;]);
ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead');
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict');
# Start s2 BEFORE waking reindex
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblpk VALUES (13, now()) ON CONFLICT ON CONSTRAINT tblpk_pkey DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'exec-insert-before-insert-speculative');
@@ -425,52 +353,40 @@ $node->safe_psql('postgres', 'TRUNCATE TABLE test.tblpk');
############################################################################
note('Test: REINDEX on partitioned table (set-dead phase)');
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait');
]);
-$s3->query_until(
- qr/starting_reindex/, q[
-\echo starting_reindex
-REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey;
-]);
+$s3->do_async(q[REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey;]);
ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead');
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict');
wakeup_injection_point($node,
'reindex-relation-concurrently-before-set-dead');
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'exec-insert-before-insert-speculative');
@@ -485,51 +401,39 @@ $node->safe_psql('postgres', 'TRUNCATE TABLE test.tblparted');
############################################################################
note('Test: REINDEX on partitioned table (swap phase)');
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('reindex-relation-concurrently-before-swap', 'wait');
]);
-$s3->query_until(
- qr/starting_reindex/, q[
-\echo starting_reindex
-REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey;
-]);
+$s3->do_async(q[REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey;]);
ok_injection_point($node, 'reindex-relation-concurrently-before-swap');
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict');
wakeup_injection_point($node, 'reindex-relation-concurrently-before-swap');
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'exec-insert-before-insert-speculative');
@@ -544,50 +448,38 @@ $node->safe_psql('postgres', 'TRUNCATE TABLE test.tblparted');
############################################################################
note('Test: REINDEX on partitioned table (s1 wakes before reindex)');
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('reindex-relation-concurrently-before-set-dead', 'wait');
]);
-$s3->query_until(
- qr/starting_reindex/, q[
-\echo starting_reindex
-REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey;
-]);
+$s3->do_async(q[REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey;]);
ok_injection_point($node, 'reindex-relation-concurrently-before-set-dead');
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'check-exclusion-or-unique-constraint-no-conflict');
# Start s2 BEFORE waking reindex
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'exec-insert-before-insert-speculative');
@@ -607,35 +499,27 @@ note(
'Test: REINDEX on partitioned table, cache inval between two get_partition_ancestors'
);
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-init-partition-after-get-partition-ancestors', 'wait');
]);
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('reindex-relation-concurrently-before-swap', 'wait');
]);
-$s2->query_until(
- qr/starting_reindex/, q[
-\echo starting_reindex
-REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey;
-]);
+$s2->do_async(q[REINDEX INDEX CONCURRENTLY test.tbl_partition_pkey;]);
ok_injection_point($node, 'reindex-relation-concurrently-before-swap');
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblparted VALUES (13, now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node,
'exec-init-partition-after-get-partition-ancestors');
@@ -654,26 +538,24 @@ note('Test: CREATE INDEX CONCURRENTLY + UPSERT');
# Uses invalidate-catalog-snapshot-end to test catalog invalidation
# during UPSERT
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-my $s1_pid = $s1->query_safe('SELECT pg_backend_pid()');
+# Get the session's backend PID before attaching injection points
+my $s1_pid = $s1->query_oneval('SELECT pg_backend_pid()');
# s1 attaches BOTH injection points - the unique constraint check AND catalog snapshot
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s1->query_until(
- qr/attaching_injection_point/, q[
-\echo attaching_injection_point
-SELECT injection_points_attach('invalidate-catalog-snapshot-end', 'wait');
-]);
-
# In cases of cache clobbering, s1 may hit the injection point during attach.
+# Start attach asynchronously so we can check if it blocks.
+$s1->do_async(q[SELECT injection_points_attach('invalidate-catalog-snapshot-end', 'wait');]);
+
# Wait for that session to become idle (attach completed), or wake it up if
# it becomes stuck on injection point.
if (!wait_for_idle($node, $s1_pid))
@@ -687,34 +569,28 @@ if (!wait_for_idle($node, $s1_pid))
SELECT injection_points_wakeup('invalidate-catalog-snapshot-end');
]);
}
+# Wait for async command to complete
+$s1->wait_for_completion;
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('define-index-before-set-valid', 'wait');
]);
# s3: Start CREATE INDEX CONCURRENTLY (blocks on define-index-before-set-valid)
-$s3->query_until(
- qr/starting_create_index/, q[
-\echo starting_create_index
-CREATE UNIQUE INDEX CONCURRENTLY tbl_pkey_duplicate ON test.tblpk(i);
-]);
+$s3->do_async(q[CREATE UNIQUE INDEX CONCURRENTLY tbl_pkey_duplicate ON test.tblpk(i);]);
ok_injection_point($node, 'define-index-before-set-valid');
# s1: Start UPSERT (blocks on invalidate-catalog-snapshot-end)
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'invalidate-catalog-snapshot-end');
@@ -722,11 +598,7 @@ ok_injection_point($node, 'invalidate-catalog-snapshot-end');
wakeup_injection_point($node, 'define-index-before-set-valid');
# s2: Start UPSERT (blocks on exec-insert-before-insert-speculative)
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblpk VALUES (13,now()) ON CONFLICT (i) DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'exec-insert-before-insert-speculative');
@@ -747,24 +619,20 @@ $node->safe_psql('postgres', 'TRUNCATE TABLE test.tblparted');
note('Test: CREATE INDEX CONCURRENTLY on partial index + UPSERT');
# Uses invalidate-catalog-snapshot-end to test catalog invalidation during UPSERT
-$s1 = $node->background_psql('postgres', on_error_stop => 0);
-$s2 = $node->background_psql('postgres', on_error_stop => 0);
-$s3 = $node->background_psql('postgres', on_error_stop => 0);
+$s1 = PostgreSQL::Test::Session->new(node => $node);
+$s2 = PostgreSQL::Test::Session->new(node => $node);
+$s3 = PostgreSQL::Test::Session->new(node => $node);
-$s1_pid = $s1->query_safe('SELECT pg_backend_pid()');
+$s1_pid = $s1->query_oneval('SELECT pg_backend_pid()');
# s1 attaches BOTH injection points - the unique constraint check AND catalog snapshot
-$s1->query_safe(
+$s1->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('check-exclusion-or-unique-constraint-no-conflict', 'wait');
]);
-$s1->query_until(
- qr/attaching_injection_point/, q[
-\echo attaching_injection_point
-SELECT injection_points_attach('invalidate-catalog-snapshot-end', 'wait');
-]);
+$s1->do(q[SELECT injection_points_attach('invalidate-catalog-snapshot-end', 'wait');]);
# In cases of cache clobbering, s1 may hit the injection point during attach.
# Wait for that session to become idle (attach completed), or wake it up if
@@ -781,33 +649,25 @@ if (!wait_for_idle($node, $s1_pid))
]);
}
-$s2->query_safe(
+$s2->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('exec-insert-before-insert-speculative', 'wait');
]);
-$s3->query_safe(
+$s3->do(
q[
SELECT injection_points_set_local();
SELECT injection_points_attach('define-index-before-set-valid', 'wait');
]);
# s3: Start CREATE INDEX CONCURRENTLY (blocks on define-index-before-set-valid)
-$s3->query_until(
- qr/starting_create_index/, q[
-\echo starting_create_index
-CREATE UNIQUE INDEX CONCURRENTLY tbl_pkey_special_duplicate ON test.tblexpr(abs(i)) WHERE i < 10000;
-]);
+$s3->do_async(q[CREATE UNIQUE INDEX CONCURRENTLY tbl_pkey_special_duplicate ON test.tblexpr(abs(i)) WHERE i < 10000;]);
ok_injection_point($node, 'define-index-before-set-valid');
# s1: Start UPSERT (blocks on invalidate-catalog-snapshot-end)
-$s1->query_until(
- qr/starting_upsert_s1/, q[
-\echo starting_upsert_s1
-INSERT INTO test.tblexpr VALUES(13,now()) ON CONFLICT (abs(i)) WHERE i < 100 DO UPDATE SET updated_at = now();
-]);
+$s1->do_async(q[INSERT INTO test.tblexpr VALUES(13,now()) ON CONFLICT (abs(i)) WHERE i < 100 DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'invalidate-catalog-snapshot-end');
@@ -815,11 +675,7 @@ ok_injection_point($node, 'invalidate-catalog-snapshot-end');
wakeup_injection_point($node, 'define-index-before-set-valid');
# s2: Start UPSERT (blocks on exec-insert-before-insert-speculative)
-$s2->query_until(
- qr/starting_upsert_s2/, q[
-\echo starting_upsert_s2
-INSERT INTO test.tblexpr VALUES(13,now()) ON CONFLICT (abs(i)) WHERE i < 100 DO UPDATE SET updated_at = now();
-]);
+$s2->do_async(q[INSERT INTO test.tblexpr VALUES(13,now()) ON CONFLICT (abs(i)) WHERE i < 100 DO UPDATE SET updated_at = now();]);
ok_injection_point($node, 'exec-insert-before-insert-speculative');
wakeup_injection_point($node, 'invalidate-catalog-snapshot-end');
@@ -920,33 +776,23 @@ SELECT injection_points_wakeup('$point_name');
]);
}
-# Wait for any pending query to complete, capture stderr, and close the session.
-# Returns the stderr output (excluding internal markers).
+# Wait for any pending query to complete and close the session.
+# Returns empty string on success, error message on failure.
sub safe_quit
{
my ($session) = @_;
- # Send a marker and wait for it to ensure any pending query completes
- my $banner = "safe_quit_marker";
- my $banner_match = qr/(^|\n)$banner\r?\n/;
+ # Wait for any async queries to complete
+ $session->wait_for_completion;
- $session->{stdin} .= "\\echo $banner\n\\warn $banner\n";
-
- pump_until(
- $session->{run}, $session->{timeout},
- \$session->{stdout}, $banner_match);
- pump_until(
- $session->{run}, $session->{timeout},
- \$session->{stderr}, $banner_match);
-
- # Capture stderr (excluding the banner)
- my $stderr = $session->{stderr};
- $stderr =~ s/$banner_match//;
+ # Check connection status
+ my $status = $session->conn_status;
# Close the session
- $session->quit;
+ $session->close;
- return $stderr;
+ # Return empty string if connection was OK, otherwise return error
+ return ($status == PostgreSQL::PqFFI::CONNECTION_OK()) ? '' : 'connection error';
}
# Helper function: verify that the given sessions exit cleanly.
diff --git a/src/test/modules/test_misc/t/011_lock_stats.pl b/src/test/modules/test_misc/t/011_lock_stats.pl
index 45d7d26f70c..00c1ab00df2 100644
--- a/src/test/modules/test_misc/t/011_lock_stats.pl
+++ b/src/test/modules/test_misc/t/011_lock_stats.pl
@@ -19,6 +19,7 @@ use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Session;
use Test::More;
plan skip_all => 'Injection points not supported by this build'
@@ -32,15 +33,12 @@ my $node;
# Setup the 2 sessions
sub setup_sessions
{
- $s1 = $node->background_psql('postgres');
- $s2 = $node->background_psql('postgres');
+ $s1 = PostgreSQL::Test::Session->new(node => $node);
+ $s2 = PostgreSQL::Test::Session->new(node => $node);
# Setup injection points for the waiting session
- $s2->query_until(
- qr/attaching_injection_point/, q[
- \echo attaching_injection_point
- SELECT injection_points_attach('deadlock-timeout-fired', 'wait');
- ]);
+ $s2->do(
+ q[SELECT injection_points_attach('deadlock-timeout-fired', 'wait');]);
}
# Fetch waits and wait_time from pg_stat_lock for a given lock type
@@ -98,7 +96,7 @@ setup_sessions();
my $log_offset = -s $node->logfile;
-$s1->query_safe(
+$s1->do(
q[
SELECT pg_stat_reset_shared('lock');
BEGIN;
@@ -106,17 +104,13 @@ LOCK TABLE test_stat_tab;
]);
# s2 setup
-$s2->query_safe(
+$s2->do(
q[
BEGIN;
SELECT pg_stat_force_next_flush();
]);
# s2 blocks on LOCK.
-$s2->query_until(
- qr/lock_s2/, q[
-\echo lock_s2
-LOCK TABLE test_stat_tab;
-]);
+$s2->do_async(q(LOCK TABLE test_stat_tab;));
wait_and_detach($node, 'deadlock-timeout-fired');
@@ -136,8 +130,9 @@ $node->safe_psql(
$node->wait_for_log(qr/logging memory contexts/, $log_offset);
# deadlock_timeout fired, now commit in s1 and s2
-$s1->query_safe(q(COMMIT));
-$s2->query_safe(q(COMMIT));
+$s1->do(q(COMMIT));
+$s2->wait_for_completion;
+$s2->do(q(COMMIT));
# check that pg_stat_lock has been updated
wait_for_pg_stat_lock($node, 'relation');
@@ -158,8 +153,8 @@ is( scalar @still_waiting,
);
# close sessions
-$s1->quit;
-$s2->quit;
+$s1->close;
+$s2->close;
####### transaction lock
@@ -167,27 +162,31 @@ setup_sessions();
$log_offset = -s $node->logfile;
-$s1->query_safe(
+# The INSERT must autocommit before the explicit transaction is opened, so
+# that session s2 can see rows k1/k2/k3 and block on s1's row lock. Send it
+# separately from the BEGIN block: a single multi-statement query containing
+# BEGIN would run the INSERT inside the still-open transaction, leaving the
+# rows invisible to s2 (so its UPDATE would match nothing and never wait).
+$s1->do(
q[
SELECT pg_stat_reset_shared('lock');
INSERT INTO test_stat_tab(key, value) VALUES('k1', 1), ('k2', 1), ('k3', 1);
+]);
+$s1->do(
+ q[
BEGIN;
UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
]);
# s2 setup
-$s2->query_safe(
+$s2->do(
q[
SET log_lock_waits = on;
BEGIN;
SELECT pg_stat_force_next_flush();
]);
# s2 blocks here on UPDATE
-$s2->query_until(
- qr/lock_s2/, q[
-\echo lock_s2
-UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
-]);
+$s2->do_async(q(UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';));
wait_and_detach($node, 'deadlock-timeout-fired');
@@ -196,8 +195,9 @@ $node->wait_for_log(qr/still waiting for ShareLock on transaction/,
$log_offset);
# deadlock_timeout fired, now commit in s1 and s2
-$s1->query_safe(q(COMMIT));
-$s2->query_safe(q(COMMIT));
+$s1->do(q(COMMIT));
+$s2->wait_for_completion;
+$s2->do(q(COMMIT));
# check that pg_stat_lock has been updated
wait_for_pg_stat_lock($node, 'transactionid');
@@ -208,8 +208,8 @@ ok(1, "Lock stats ok for transactionid");
$node->wait_for_log(qr/acquired ShareLock on transaction/, $log_offset);
# Close sessions
-$s1->quit;
-$s2->quit;
+$s1->close;
+$s2->close;
####### advisory lock
@@ -217,25 +217,21 @@ setup_sessions();
$log_offset = -s $node->logfile;
-$s1->query_safe(
+$s1->do(
q[
SELECT pg_stat_reset_shared('lock');
SELECT pg_advisory_lock(1);
]);
# s2 setup
-$s2->query_safe(
+$s2->do(
q[
SET log_lock_waits = on;
BEGIN;
SELECT pg_stat_force_next_flush();
]);
# s2 blocks on the advisory lock.
-$s2->query_until(
- qr/lock_s2/, q[
-\echo lock_s2
-SELECT pg_advisory_lock(1);
-]);
+$s2->do_async(q(SELECT pg_advisory_lock(1);));
wait_and_detach($node, 'deadlock-timeout-fired');
@@ -244,8 +240,9 @@ $node->wait_for_log(qr/still waiting for ExclusiveLock on advisory lock/,
$log_offset);
# deadlock_timeout fired, now unlock and commit s2
-$s1->query_safe(q(SELECT pg_advisory_unlock(1)));
-$s2->query_safe(
+$s1->do(q(SELECT pg_advisory_unlock(1)));
+$s2->wait_for_completion;
+$s2->do(
q[
SELECT pg_advisory_unlock(1);
COMMIT;
@@ -260,8 +257,8 @@ ok(1, "Lock stats ok for advisory");
$node->wait_for_log(qr/acquired ExclusiveLock on advisory lock/, $log_offset);
# Close sessions
-$s1->quit;
-$s2->quit;
+$s1->close;
+$s2->close;
####### Ensure log_lock_waits has no impact
@@ -269,7 +266,7 @@ setup_sessions();
$log_offset = -s $node->logfile;
-$s1->query_safe(
+$s1->do(
q[
SELECT pg_stat_reset_shared('lock');
BEGIN;
@@ -277,24 +274,21 @@ LOCK TABLE test_stat_tab;
]);
# s2 setup
-$s2->query_safe(
+$s2->do(
q[
SET log_lock_waits = off;
BEGIN;
SELECT pg_stat_force_next_flush();
]);
# s2 blocks on LOCK.
-$s2->query_until(
- qr/lock_s2/, q[
-\echo lock_s2
-LOCK TABLE test_stat_tab;
-]);
+$s2->do_async(q(LOCK TABLE test_stat_tab;));
wait_and_detach($node, 'deadlock-timeout-fired');
# deadlock_timeout fired, now commit in s1 and s2
-$s1->query_safe(q(COMMIT));
-$s2->query_safe(q(COMMIT));
+$s1->do(q(COMMIT));
+$s2->wait_for_completion;
+$s2->do(q(COMMIT));
# check that pg_stat_lock has been updated
wait_for_pg_stat_lock($node, 'relation');
@@ -310,8 +304,8 @@ ok( !$node->log_contains(
);
# close sessions
-$s1->quit;
-$s2->quit;
+$s1->close;
+$s2->close;
# cleanup
$node->safe_psql('postgres', q[DROP TABLE test_stat_tab;]);
diff --git a/src/test/modules/test_misc/t/013_temp_obj_multisession.pl b/src/test/modules/test_misc/t/013_temp_obj_multisession.pl
index 5f3cc7d2fc5..0b15b0bde08 100644
--- a/src/test/modules/test_misc/t/013_temp_obj_multisession.pl
+++ b/src/test/modules/test_misc/t/013_temp_obj_multisession.pl
@@ -20,21 +20,21 @@ use strict;
use warnings;
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
-use PostgreSQL::Test::BackgroundPsql;
+use PostgreSQL::Test::Session;
use Test::More;
my $node = PostgreSQL::Test::Cluster->new('temp_lock');
$node->init;
$node->start;
-# Owner session. Created via background_psql so it stays alive while
-# the second session probes its temp objects.
-my $psql1 = $node->background_psql('postgres');
+# Owner session. Created as a persistent libpq session so it stays alive
+# while the second session probes its temp objects.
+my $psql1 = PostgreSQL::Test::Session->new(node => $node);
# Initially create the table without an index, so read paths go straight
# through the read-stream / buffer-manager entry points without being
# masked by an index scan that would hit ReadBuffer_common from nbtree.
-$psql1->query_safe(q(CREATE TEMP TABLE foo AS SELECT 42 AS val;));
+$psql1->do(q(CREATE TEMP TABLE foo AS SELECT 42 AS val;));
# Resolve the owner's temp schema so the probing session can refer to
# the table by a fully-qualified name.
@@ -130,7 +130,7 @@ like($stderr,
# Now create an index to exercise the index-scan path. nbtree calls
# ReadBuffer (which is ReadBufferExtended -> ReadBuffer_common), so
# this exercises a different chain of buffer-manager entry points.
-$psql1->query_safe(q(CREATE INDEX ON foo(val);));
+$psql1->do(q(CREATE INDEX ON foo(val);));
$node->psql(
'postgres',
@@ -156,7 +156,7 @@ like($stderr, qr/cannot alter temporary tables of other sessions/,
# operations -- they don't read the underlying table -- which
# documents the boundary between catalog and data access for temp
# objects.
-$psql1->query_safe(
+$psql1->do(
q[CREATE FUNCTION pg_temp.foo_id(r foo) RETURNS int LANGUAGE SQL ]
. q[AS 'SELECT r.val';]);
@@ -187,7 +187,7 @@ is($stderr, '', 'DROP TABLE is allowed');
# into the creator's pg_temp namespace with an auto-dependency on
# the borrowed type, so it disappears together with the session that
# created it.
-$psql1->query_safe(q(CREATE TEMP TABLE foo2 AS SELECT 42 AS val;));
+$psql1->do(q(CREATE TEMP TABLE foo2 AS SELECT 42 AS val;));
$node->psql(
'postgres',
@@ -216,8 +216,8 @@ my $foo2_oid = $node->safe_psql('postgres',
# Cross-session LOCK TABLE scenario. Ensure that LockRelationOid is working
# properly for other temp tables since this mechanism is also used by
# autovacuum during orphaned tables cleanup.
-my $psql2 = $node->background_psql('postgres');
-$psql2->query_safe(
+my $psql2 = PostgreSQL::Test::Session->new(node => $node);
+$psql2->do(
qq{
BEGIN;
LOCK TABLE $tempschema.foo2 IN ACCESS SHARE MODE;
@@ -233,15 +233,15 @@ $psql2->query_safe(
# owner will try to acquire deletion lock all its temp objects via
# findDependentObjects.
my $log_offset = -s $node->logfile;
-$psql1->quit;
+$psql1->close;
# Check whether session-exit cleanup is blocked.
$node->wait_for_log(qr/waiting for AccessExclusiveLock on relation $foo2_oid/,
$log_offset);
# Release lock on foo2 and allow session-exit cleanup to finish.
-$psql2->query_safe(q(COMMIT;));
-$psql2->quit;
+$psql2->do(q(COMMIT;));
+$psql2->close;
# After releasing the lock, the owner can finally acquire
# AccessExclusiveLock on foo2 and finish session-exit cleanup. Verify
diff --git a/src/test/modules/test_slru/t/001_multixact.pl b/src/test/modules/test_slru/t/001_multixact.pl
index f6f45895ebd..2d86434d7bc 100644
--- a/src/test/modules/test_slru/t/001_multixact.pl
+++ b/src/test/modules/test_slru/t/001_multixact.pl
@@ -6,6 +6,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -30,8 +31,8 @@ $node->safe_psql('postgres', q(CREATE EXTENSION test_slru));
# lost.
# Create the first multixact
-my $bg_psql = $node->background_psql('postgres');
-my $multi1 = $bg_psql->query_safe(q(SELECT test_create_multixact();));
+my $bg_session = PostgreSQL::Test::Session->new(node => $node);
+my $multi1 = $bg_session->query_oneval(q(SELECT test_create_multixact();));
# Assign the middle multixact. Use an injection point to prevent it
# from being fully recorded.
@@ -39,11 +40,9 @@ $node->safe_psql('postgres',
q{SELECT injection_points_attach('multixact-create-from-members','wait');}
);
-$bg_psql->query_until(
- qr/assigning lost multi/, q(
-\echo assigning lost multi
- SELECT test_create_multixact();
-));
+# Start the second multixact creation asynchronously - it will block at
+# the injection point
+$bg_session->do_async(q(SELECT test_create_multixact();));
$node->wait_for_event('client backend', 'multixact-create-from-members');
$node->safe_psql('postgres',
@@ -52,10 +51,10 @@ $node->safe_psql('postgres',
# Create the third multixid
my $multi2 = $node->safe_psql('postgres', q{SELECT test_create_multixact();});
-# All set and done, it's time for hard restart
+# All set and done, it's time for hard restart. The background session
+# will be terminated by the crash.
$node->stop('immediate');
$node->start;
-$bg_psql->{run}->finish;
# Verify that the recorded multixids are readable
is( $node->safe_psql('postgres', qq{SELECT test_read_multixact('$multi1');}),
diff --git a/src/test/modules/xid_wraparound/t/001_emergency_vacuum.pl b/src/test/modules/xid_wraparound/t/001_emergency_vacuum.pl
index 213f9052ed2..304076735b4 100644
--- a/src/test/modules/xid_wraparound/t/001_emergency_vacuum.pl
+++ b/src/test/modules/xid_wraparound/t/001_emergency_vacuum.pl
@@ -4,6 +4,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -47,17 +48,10 @@ CREATE TABLE small_trunc(id serial primary key, data text, filler text default r
INSERT INTO small_trunc(data) SELECT generate_series(1,15000);
]);
-# Bump the query timeout to avoid false negatives on slow test systems.
-my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
-
# Start a background session, which holds a transaction open, preventing
# autovacuum from advancing relfrozenxid and datfrozenxid.
-my $background_psql = $node->background_psql(
- 'postgres',
- on_error_stop => 0,
- timeout => $psql_timeout_secs);
-$background_psql->set_query_timer_restart();
-$background_psql->query_safe(
+my $background_session = PostgreSQL::Test::Session->new(node => $node);
+$background_session->do(
qq[
BEGIN;
DELETE FROM large WHERE id % 2 = 0;
@@ -89,8 +83,8 @@ my $log_offset = -s $node->logfile;
# Finish the old transaction, to allow vacuum freezing to advance
# relfrozenxid and datfrozenxid again.
-$background_psql->query_safe(qq[COMMIT]);
-$background_psql->quit;
+$background_session->do(qq[COMMIT;]);
+$background_session->close;
# Wait until autovacuum processed all tables and advanced the
# system-wide oldest-XID.
diff --git a/src/test/modules/xid_wraparound/t/002_limits.pl b/src/test/modules/xid_wraparound/t/002_limits.pl
index 86632a8d510..0ef67d0f4b8 100644
--- a/src/test/modules/xid_wraparound/t/002_limits.pl
+++ b/src/test/modules/xid_wraparound/t/002_limits.pl
@@ -10,6 +10,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
use Time::HiRes qw(usleep);
@@ -29,6 +30,8 @@ $node->append_conf(
'postgresql.conf', qq[
autovacuum_naptime = 1s
log_autovacuum_min_duration = 0
+log_connections = on
+log_statement = 'all'
]);
$node->start;
$node->safe_psql('postgres', 'CREATE EXTENSION xid_wraparound');
@@ -41,16 +44,10 @@ CREATE TABLE wraparoundtest(t text) WITH (autovacuum_enabled = off);
INSERT INTO wraparoundtest VALUES ('start');
]);
-# Bump the query timeout to avoid false negatives on slow test systems.
-my $psql_timeout_secs = 4 * $PostgreSQL::Test::Utils::timeout_default;
-
# Start a background session, which holds a transaction open, preventing
# autovacuum from advancing relfrozenxid and datfrozenxid.
-my $background_psql = $node->background_psql(
- 'postgres',
- on_error_stop => 0,
- timeout => $psql_timeout_secs);
-$background_psql->query_safe(
+my $background_session = PostgreSQL::Test::Session->new(node => $node);
+$background_session->do(
qq[
BEGIN;
INSERT INTO wraparoundtest VALUES ('oldxact');
@@ -108,8 +105,8 @@ like(
# Finish the old transaction, to allow vacuum freezing to advance
# relfrozenxid and datfrozenxid again.
-$background_psql->query_safe(qq[COMMIT]);
-$background_psql->quit;
+$background_session->do(qq[COMMIT;]);
+$background_session->close;
# VACUUM, to freeze the tables and advance datfrozenxid.
#
@@ -122,8 +119,8 @@ $node->safe_psql('postgres', 'VACUUM');
# the system-wide oldest-XID.
$ret =
$node->poll_query_until('postgres',
- qq[INSERT INTO wraparoundtest VALUES ('after VACUUM')],
- 'INSERT 0 1');
+ qq[INSERT INTO wraparoundtest VALUES ('after VACUUM') RETURNING true],
+ );
# Check the table contents
$ret = $node->safe_psql('postgres', qq[SELECT * from wraparoundtest]);
diff --git a/src/test/modules/xid_wraparound/t/004_notify_freeze.pl b/src/test/modules/xid_wraparound/t/004_notify_freeze.pl
index d0a1f1fe2fc..f2fa4e5a6b9 100644
--- a/src/test/modules/xid_wraparound/t/004_notify_freeze.pl
+++ b/src/test/modules/xid_wraparound/t/004_notify_freeze.pl
@@ -7,6 +7,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use Test::More;
my $node = PostgreSQL::Test::Cluster->new('node');
@@ -24,9 +25,9 @@ $node->safe_psql('postgres',
'ALTER DATABASE template0 WITH ALLOW_CONNECTIONS true');
# Start Session 1 and leave it idle in transaction
-my $psql_session1 = $node->background_psql('postgres');
-$psql_session1->query_safe('listen s;');
-$psql_session1->query_safe('begin;');
+my $session1 = PostgreSQL::Test::Session->new(node => $node);
+$session1->do('LISTEN s');
+$session1->do('BEGIN');
# Send some notifys from other sessions
for my $i (1 .. 10)
@@ -54,18 +55,20 @@ my $datafronzenxid_freeze = $node->safe_psql('postgres',
"select min(datfrozenxid::text::bigint) from pg_database");
ok($datafronzenxid_freeze > $datafronzenxid, 'datfrozenxid advanced');
-# On Session 1, commit and ensure that the all the notifications are
+# On Session 1, commit and ensure that all the notifications are
# received. This depends on correctly freezing the XIDs in the pending
# notification entries.
-my $res = $psql_session1->query_safe('commit;');
-my $notifications_count = 0;
-foreach my $i (split('\n', $res))
+$session1->do('COMMIT');
+
+my $notifications = $session1->get_all_notifications();
+is(scalar(@$notifications), 10, 'received all committed notifications');
+
+my $expected_payload = 1;
+foreach my $notify (@$notifications)
{
- $notifications_count++;
- like($i,
- qr/Asynchronous notification "s" with payload "$notifications_count" received/
- );
+ is($notify->{channel}, 's', "notification $expected_payload has correct channel");
+ is($notify->{payload}, $expected_payload, "notification $expected_payload has correct payload");
+ $expected_payload++;
}
-is($notifications_count, 10, 'received all committed notifications');
done_testing();
diff --git a/src/test/postmaster/t/002_connection_limits.pl b/src/test/postmaster/t/002_connection_limits.pl
index 8c67c4a86c7..1b4a97d16f4 100644
--- a/src/test/postmaster/t/002_connection_limits.pl
+++ b/src/test/postmaster/t/002_connection_limits.pl
@@ -7,6 +7,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -33,19 +34,20 @@ GRANT pg_use_reserved_connections TO regress_reserved;
CREATE USER regress_superuser LOGIN SUPERUSER;
});
+my $node_connstr = $node->connstr('postgres');
+my $libdir = $node->config_data('--libdir');
+
# With the limits we set in postgresql.conf, we can establish:
# - 3 connections for any user with no special privileges
# - 2 more connections for users belonging to "pg_use_reserved_connections"
# - 1 more connection for superuser
-sub background_psql_as_user
+sub session_as_user
{
my $user = shift;
+ my $connstr = "$node_connstr user=$user";
- return $node->background_psql(
- 'postgres',
- on_error_die => 1,
- extra_params => [ '--username' => $user ]);
+ return PostgreSQL::Test::Session->new( connstr => $connstr, libdir => $libdir);
}
# Like connect_fails(), except that we also wait for the failed backend to
@@ -82,9 +84,9 @@ $node->restart;
my @sessions = ();
my @raw_connections = ();
-push(@sessions, background_psql_as_user('regress_regular'));
-push(@sessions, background_psql_as_user('regress_regular'));
-push(@sessions, background_psql_as_user('regress_regular'));
+push(@sessions, session_as_user('regress_regular'));
+push(@sessions, session_as_user('regress_regular'));
+push(@sessions, session_as_user('regress_regular'));
connect_fails_wait(
$node,
"dbname=postgres user=regress_regular",
@@ -93,8 +95,8 @@ connect_fails_wait(
qr/FATAL: remaining connection slots are reserved for roles with privileges of the "pg_use_reserved_connections" role/
);
-push(@sessions, background_psql_as_user('regress_reserved'));
-push(@sessions, background_psql_as_user('regress_reserved'));
+push(@sessions, session_as_user('regress_reserved'));
+push(@sessions, session_as_user('regress_reserved'));
connect_fails_wait(
$node,
"dbname=postgres user=regress_reserved",
@@ -103,7 +105,7 @@ connect_fails_wait(
qr/FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute/
);
-push(@sessions, background_psql_as_user('regress_superuser'));
+push(@sessions, session_as_user('regress_superuser'));
connect_fails_wait(
$node,
"dbname=postgres user=regress_superuser",
@@ -150,7 +152,7 @@ SKIP:
# Clean up
foreach my $session (@sessions)
{
- $session->quit;
+ $session->close;
}
foreach my $socket (@raw_connections)
{
diff --git a/src/test/recovery/t/009_twophase.pl b/src/test/recovery/t/009_twophase.pl
index aa73d3e106c..9fe037bb0a1 100644
--- a/src/test/recovery/t/009_twophase.pl
+++ b/src/test/recovery/t/009_twophase.pl
@@ -6,6 +6,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -331,11 +332,10 @@ $cur_primary->stop;
$cur_standby->restart;
# Acquire a snapshot in standby, before we commit the prepared transaction
-my $standby_session =
- $cur_standby->background_psql('postgres', on_error_die => 1);
-$standby_session->query_safe("BEGIN ISOLATION LEVEL REPEATABLE READ");
+my $standby_session = PostgreSQL::Test::Session->new(node => $cur_standby);
+$standby_session->do("BEGIN ISOLATION LEVEL REPEATABLE READ");
$psql_out =
- $standby_session->query_safe("SELECT count(*) FROM t_009_tbl_standby_mvcc");
+ $standby_session->query_oneval("SELECT count(*) FROM t_009_tbl_standby_mvcc");
is($psql_out, '0',
"Prepared transaction not visible in standby before commit");
@@ -349,17 +349,17 @@ COMMIT PREPARED 'xact_009_standby_mvcc';
# Still not visible to the old snapshot
$psql_out =
- $standby_session->query_safe("SELECT count(*) FROM t_009_tbl_standby_mvcc");
+ $standby_session->query_oneval("SELECT count(*) FROM t_009_tbl_standby_mvcc");
is($psql_out, '0',
"Committed prepared transaction not visible to old snapshot in standby");
# Is visible to a new snapshot
-$standby_session->query_safe("COMMIT");
+$standby_session->do("COMMIT");
$psql_out =
- $standby_session->query_safe("SELECT count(*) FROM t_009_tbl_standby_mvcc");
+ $standby_session->query_oneval("SELECT count(*) FROM t_009_tbl_standby_mvcc");
is($psql_out, '2',
"Committed prepared transaction is visible to new snapshot in standby");
-$standby_session->quit;
+$standby_session->close;
###############################################################################
# Check for a lock conflict between prepared transaction with DDL inside and
diff --git a/src/test/recovery/t/013_crash_restart.pl b/src/test/recovery/t/013_crash_restart.pl
index 0fde920713e..b62b0000edf 100644
--- a/src/test/recovery/t/013_crash_restart.pl
+++ b/src/test/recovery/t/013_crash_restart.pl
@@ -149,7 +149,7 @@ ok( pump_until(
$monitor->finish;
# Wait till server restarts
-is($node->poll_query_until('postgres', undef, ''),
+is($node->poll_until_connection('postgres'),
"1", "reconnected after SIGQUIT");
@@ -238,7 +238,7 @@ ok( pump_until(
$monitor->finish;
# Wait till server restarts
-is($node->poll_query_until('postgres', undef, ''),
+is($node->poll_until_connection('postgres'),
"1", "reconnected after SIGKILL");
# Make sure the committed rows survived, in-progress ones not
diff --git a/src/test/recovery/t/022_crash_temp_files.pl b/src/test/recovery/t/022_crash_temp_files.pl
index 5de9b0fb0eb..6c55fcf40b1 100644
--- a/src/test/recovery/t/022_crash_temp_files.pl
+++ b/src/test/recovery/t/022_crash_temp_files.pl
@@ -146,7 +146,7 @@ ok( pump_until(
$killme2->finish;
# Wait till server finishes restarting
-$node->poll_query_until('postgres', undef, '');
+$node->poll_until_connection('postgres');
# Check for temporary files
is( $node->safe_psql(
@@ -253,7 +253,7 @@ ok( pump_until(
$killme2->finish;
# Wait till server finishes restarting
-$node->poll_query_until('postgres', undef, '');
+$node->poll_until_connection('postgres');
# Check for temporary files -- should be there
is( $node->safe_psql(
diff --git a/src/test/recovery/t/031_recovery_conflict.pl b/src/test/recovery/t/031_recovery_conflict.pl
index 7a740f69806..74349749170 100644
--- a/src/test/recovery/t/031_recovery_conflict.pl
+++ b/src/test/recovery/t/031_recovery_conflict.pl
@@ -7,6 +7,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -67,8 +68,7 @@ $node_primary->wait_for_replay_catchup($node_standby);
# a longrunning psql that we can use to trigger conflicts
-my $psql_standby =
- $node_standby->background_psql($test_db, on_error_stop => 0);
+my $psql_standby = PostgreSQL::Test::Session->new(node => $node_standby, dbname => $test_db);
my $expected_conflicts = 0;
@@ -96,7 +96,7 @@ my $cursor1 = "test_recovery_conflict_cursor";
# DECLARE and use a cursor on standby, causing buffer with the only block of
# the relation to be pinned on the standby
-my $res = $psql_standby->query_safe(
+my $res = $psql_standby->query_oneval(
qq[
BEGIN;
DECLARE $cursor1 CURSOR FOR SELECT b FROM $table1;
@@ -119,7 +119,7 @@ $node_primary->safe_psql($test_db, qq[VACUUM FREEZE $table1;]);
$node_primary->wait_for_replay_catchup($node_standby);
check_conflict_log("User was holding shared buffer pin for too long");
-$psql_standby->reconnect_and_clear();
+$psql_standby->reconnect();
check_conflict_stat("bufferpin");
@@ -132,7 +132,7 @@ $node_primary->safe_psql($test_db,
$node_primary->wait_for_replay_catchup($node_standby);
# DECLARE and FETCH from cursor on the standby
-$res = $psql_standby->query_safe(
+$res = $psql_standby->query_oneval(
qq[
BEGIN;
DECLARE $cursor1 CURSOR FOR SELECT b FROM $table1;
@@ -152,7 +152,7 @@ $node_primary->wait_for_replay_catchup($node_standby);
check_conflict_log(
"User query might have needed to see row versions that must be removed");
-$psql_standby->reconnect_and_clear();
+$psql_standby->reconnect();
check_conflict_stat("snapshot");
@@ -161,7 +161,7 @@ $sect = "lock conflict";
$expected_conflicts++;
# acquire lock to conflict with
-$res = $psql_standby->query_safe(
+$res = $psql_standby->query_oneval(
qq[
BEGIN;
LOCK TABLE $table1 IN ACCESS SHARE MODE;
@@ -175,7 +175,7 @@ $node_primary->safe_psql($test_db, qq[DROP TABLE $table1;]);
$node_primary->wait_for_replay_catchup($node_standby);
check_conflict_log("User was holding a relation lock for too long");
-$psql_standby->reconnect_and_clear();
+$psql_standby->reconnect();
check_conflict_stat("lock");
@@ -186,7 +186,7 @@ $expected_conflicts++;
# DECLARE a cursor for a query which, with sufficiently low work_mem, will
# spill tuples into temp files in the temporary tablespace created during
# setup.
-$res = $psql_standby->query_safe(
+$res = $psql_standby->query_oneval(
qq[
BEGIN;
SET work_mem = '64kB';
@@ -205,7 +205,7 @@ $node_primary->wait_for_replay_catchup($node_standby);
check_conflict_log(
"User was or might have been using tablespace that must be dropped");
-$psql_standby->reconnect_and_clear();
+$psql_standby->reconnect();
check_conflict_stat("tablespace");
@@ -220,8 +220,9 @@ $node_standby->adjust_conf(
'postgresql.conf',
'max_standby_streaming_delay',
"${PostgreSQL::Test::Utils::timeout_default}s");
+$psql_standby->close;
$node_standby->restart();
-$psql_standby->reconnect_and_clear();
+$psql_standby->reconnect();
# Generate a few dead rows, to later be cleaned up by vacuum. Then acquire a
# lock on another relation in a prepared xact, so it's held continuously by
@@ -244,12 +245,15 @@ SELECT txid_current();
$node_primary->wait_for_replay_catchup($node_standby);
-$res = $psql_standby->query_until(
- qr/^1$/m, qq[
+$res = $psql_standby->query_oneval(
+ qq[
BEGIN;
-- hold pin
DECLARE $cursor1 CURSOR FOR SELECT a FROM $table1;
FETCH FORWARD FROM $cursor1;
+]);
+is ($res, 1, "pin held");
+$psql_standby->do_async(qq[
-- wait for lock held by prepared transaction
SELECT * FROM $table2;
]);
@@ -270,15 +274,16 @@ $node_primary->safe_psql($test_db, qq[VACUUM FREEZE $table1;]);
$node_primary->wait_for_replay_catchup($node_standby);
check_conflict_log("User transaction caused buffer deadlock with recovery.");
-$psql_standby->reconnect_and_clear();
+$psql_standby->reconnect();
check_conflict_stat("deadlock");
# clean up for next tests
$node_primary->safe_psql($test_db, qq[ROLLBACK PREPARED 'lock';]);
$node_standby->adjust_conf('postgresql.conf', 'max_standby_streaming_delay',
- '50ms');
+ '50ms');
+$psql_standby->close;
$node_standby->restart();
-$psql_standby->reconnect_and_clear();
+$psql_standby->reconnect();
# Check that expected number of conflicts show in pg_stat_database. Needs to
@@ -302,7 +307,7 @@ check_conflict_log("User was connected to a database that must be dropped");
# explicitly shut down psql instances gracefully - to avoid hangs or worse on
# windows
-$psql_standby->quit;
+$psql_standby->close;
$node_standby->stop();
$node_primary->stop();
diff --git a/src/test/recovery/t/037_invalid_database.pl b/src/test/recovery/t/037_invalid_database.pl
index a0947108700..b10a79d0ee7 100644
--- a/src/test/recovery/t/037_invalid_database.pl
+++ b/src/test/recovery/t/037_invalid_database.pl
@@ -5,6 +5,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -91,41 +92,38 @@ is($node->psql('postgres', 'DROP DATABASE regression_invalid'),
# dropping the database, making it a suitable point to wait. Since relcache
# init reads pg_tablespace, establish each connection before locking. This
# avoids a connection-time hang with debug_discard_caches.
-my $cancel = $node->background_psql('postgres', on_error_stop => 1);
-my $bgpsql = $node->background_psql('postgres', on_error_stop => 0);
-my $pid = $bgpsql->query('SELECT pg_backend_pid()');
+my $cancel = PostgreSQL::Test::Session->new(node=>$node);
+my $bgpsql = PostgreSQL::Test::Session->new(node=>$node);
+my $pid = $bgpsql->query_oneval('SELECT pg_backend_pid()');
# create the database, prevent drop database via lock held by a 2PC transaction
-$bgpsql->query_safe(
- qq(
- CREATE DATABASE regression_invalid_interrupt;
- BEGIN;
+is (1, $bgpsql->do(
+ qq(
+ CREATE DATABASE regression_invalid_interrupt;),
+ qq(BEGIN;
LOCK pg_tablespace;
- PREPARE TRANSACTION 'lock_tblspc';));
+ PREPARE TRANSACTION 'lock_tblspc';)));
# Try to drop. This will wait due to the still held lock.
-$bgpsql->query_until(qr//, "DROP DATABASE regression_invalid_interrupt;\n");
+$bgpsql->do_async("DROP DATABASE regression_invalid_interrupt;");
# Once the DROP DATABASE is waiting for the lock, interrupt it.
-ok( $cancel->query_safe(
- qq(
+my $cancel_res = $cancel->query(
+ qq[
DO \$\$
BEGIN
WHILE NOT EXISTS(SELECT * FROM pg_locks WHERE NOT granted AND relation = 'pg_tablespace'::regclass AND mode = 'AccessShareLock') LOOP
PERFORM pg_sleep(.1);
END LOOP;
END\$\$;
- SELECT pg_cancel_backend($pid);)),
- "canceling DROP DATABASE");
-$cancel->quit();
+ SELECT pg_cancel_backend($pid)]);
+is (2, $cancel_res->{status}, "canceling DROP DATABASE"); # COMMAND_TUPLES_OK
+$cancel->close();
+$bgpsql->wait_for_completion;
# wait for cancellation to be processed
-ok( pump_until(
- $bgpsql->{run}, $bgpsql->{timeout},
- \$bgpsql->{stderr}, qr/canceling statement due to user request/),
- "cancel processed");
-$bgpsql->{stderr} = '';
+pass("cancel processed");
# Verify that connections to the database aren't allowed. The backend checks
# this before relcache init, so the lock won't interfere.
@@ -134,9 +132,12 @@ is($node->psql('regression_invalid_interrupt', ''),
# To properly drop the database, we need to release the lock previously preventing
# doing so.
-$bgpsql->query_safe(qq(ROLLBACK PREPARED 'lock_tblspc'));
-$bgpsql->query_safe(qq(DROP DATABASE regression_invalid_interrupt));
+ok($bgpsql->do(qq(ROLLBACK PREPARED 'lock_tblspc')),
+ "unblock DROP DATABASE");
-$bgpsql->quit();
+ok($bgpsql->query(qq(DROP DATABASE regression_invalid_interrupt)),
+ "DROP DATABASE invalid_interrupt");
+
+$bgpsql->close();
done_testing();
diff --git a/src/test/recovery/t/040_standby_failover_slots_sync.pl b/src/test/recovery/t/040_standby_failover_slots_sync.pl
index f8922aaa1a2..ef16e0a8740 100644
--- a/src/test/recovery/t/040_standby_failover_slots_sync.pl
+++ b/src/test/recovery/t/040_standby_failover_slots_sync.pl
@@ -4,6 +4,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -768,17 +769,13 @@ $primary->safe_psql('postgres',
"SELECT pg_create_logical_replication_slot('test_slot', 'test_decoding', false, false, true);"
);
-my $back_q = $primary->background_psql(
- 'postgres',
- on_error_stop => 0,
- timeout => $PostgreSQL::Test::Utils::timeout_default);
+my $back_q = PostgreSQL::Test::Session->new(node=>$primary);
# pg_logical_slot_get_changes will be blocked until the standby catches up,
# hence it needs to be executed in a background session.
$offset = -s $primary->logfile;
-$back_q->query_until(
- qr/logical_slot_get_changes/, q(
- \echo logical_slot_get_changes
+$back_q->do_async(
+ q(
SELECT pg_logical_slot_get_changes('test_slot', NULL, NULL);
));
@@ -796,7 +793,8 @@ $primary->reload;
# Since there are no slots in synchronized_standby_slots, the function
# pg_logical_slot_get_changes should now return, and the session can be
# stopped.
-$back_q->quit;
+$back_q->wait_for_completion;
+$back_q->close;
$primary->safe_psql('postgres',
"SELECT pg_drop_replication_slot('test_slot');");
@@ -1059,13 +1057,8 @@ $standby2->reload;
# synchronization until the remote slot catches up.
# The API will not return until this happens, to be able to make
# further calls, call the API in a background process.
-my $h = $standby2->background_psql('postgres', on_error_stop => 0);
-
-$h->query_until(
- qr/start/, q(
- \echo start
- SELECT pg_sync_replication_slots();
- ));
+my $h = PostgreSQL::Test::Session->new(node => $standby2);
+$h->do_async(q(SELECT pg_sync_replication_slots();));
# Confirm that the slot sync is skipped due to the remote slot lagging behind
$standby2->wait_for_log(
@@ -1104,6 +1097,7 @@ $standby2->wait_for_log(
qr/newly created replication slot \"lsub1_slot\" is sync-ready now/,
$log_offset);
-$h->quit;
+$h->wait_for_completion;
+$h->close;
done_testing();
diff --git a/src/test/recovery/t/041_checkpoint_at_promote.pl b/src/test/recovery/t/041_checkpoint_at_promote.pl
index d0783fef9ae..fcc01334589 100644
--- a/src/test/recovery/t/041_checkpoint_at_promote.pl
+++ b/src/test/recovery/t/041_checkpoint_at_promote.pl
@@ -4,6 +4,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Time::HiRes qw(usleep);
use Test::More;
@@ -70,11 +71,9 @@ $node_standby->safe_psql('postgres',
# Execute a restart point on the standby, that we will now be waiting on.
# This needs to be in the background.
my $logstart = -s $node_standby->logfile;
-my $psql_session =
- $node_standby->background_psql('postgres', on_error_stop => 0);
-$psql_session->query_until(
- qr/starting_checkpoint/, q(
- \echo starting_checkpoint
+my $psql_session = PostgreSQL::Test::Session->new(node=> $node_standby);
+$psql_session->do_async(
+ q(
CHECKPOINT;
));
@@ -159,7 +158,7 @@ ok( pump_until(
$killme->finish;
# Wait till server finishes restarting.
-$node_standby->poll_query_until('postgres', undef, '');
+$node_standby->poll_until_connection('postgres');
# After recovery, the server should be able to start.
my $stdout;
diff --git a/src/test/recovery/t/042_low_level_backup.pl b/src/test/recovery/t/042_low_level_backup.pl
index df4ae029fe6..58489482341 100644
--- a/src/test/recovery/t/042_low_level_backup.pl
+++ b/src/test/recovery/t/042_low_level_backup.pl
@@ -10,6 +10,7 @@ use warnings FATAL => 'all';
use File::Copy qw(copy);
use File::Path qw(rmtree);
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -20,11 +21,10 @@ $node_primary->start;
# Start backup.
my $backup_name = 'backup1';
-my $psql = $node_primary->background_psql('postgres');
+my $psql = PostgreSQL::Test::Session->new(node => $node_primary);
-$psql->query_safe("SET client_min_messages TO WARNING");
-$psql->set_query_timer_restart;
-$psql->query_safe("select pg_backup_start('test label')");
+$psql->do("SET client_min_messages TO WARNING");
+$psql->query("select pg_backup_start('test label')");
# Copy files.
my $backup_dir = $node_primary->backup_dir . '/' . $backup_name;
@@ -81,9 +81,9 @@ my $stop_segment_name = $node_primary->safe_psql('postgres',
# Stop backup and get backup_label, the last segment is archived.
my $backup_label =
- $psql->query_safe("select labelfile from pg_backup_stop()");
+ $psql->query_oneval("select labelfile from pg_backup_stop()");
-$psql->quit;
+$psql->close;
# Rather than writing out backup_label, try to recover the backup without
# backup_label to demonstrate that recovery will not work correctly without it,
diff --git a/src/test/recovery/t/046_checkpoint_logical_slot.pl b/src/test/recovery/t/046_checkpoint_logical_slot.pl
index 66761bf56c1..3a46f20fc28 100644
--- a/src/test/recovery/t/046_checkpoint_logical_slot.pl
+++ b/src/test/recovery/t/046_checkpoint_logical_slot.pl
@@ -8,6 +8,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -52,13 +53,10 @@ $node->safe_psql('postgres',
$node->safe_psql('postgres', q{checkpoint});
# Generate some transactions to get RUNNING_XACTS.
-my $xacts = $node->background_psql('postgres');
-$xacts->query_until(
- qr/run_xacts/,
- q(\echo run_xacts
-SELECT 1 \watch 0.1
-\q
-));
+for (my $i = 0; $i < 10; $i++)
+{
+ $node->safe_psql('postgres', 'SELECT 1');
+}
$node->advance_wal(20);
@@ -72,16 +70,11 @@ $node->advance_wal(20);
# removing old WAL segments.
note('starting checkpoint');
-my $checkpoint = $node->background_psql('postgres');
-$checkpoint->query_safe(
+$node->safe_psql('postgres',
q(select injection_points_attach('checkpoint-before-old-wal-removal','wait'))
);
-$checkpoint->query_until(
- qr/starting_checkpoint/,
- q(\echo starting_checkpoint
-checkpoint;
-\q
-));
+my $checkpoint = PostgreSQL::Test::Session->new(node => $node);
+$checkpoint->do_async(q(CHECKPOINT;));
# Wait until the checkpoint stops right before removing WAL segments.
note('waiting for injection_point');
@@ -90,17 +83,21 @@ note('injection_point is reached');
# Try to advance the logical slot, but make it stop when it moves to the next
# WAL segment (this has to happen in the background, too).
-my $logical = $node->background_psql('postgres');
-$logical->query_safe(
+# We need to call pg_logical_slot_get_changes repeatedly until the slot
+# advances to the next segment and hits the injection point.
+my $logical = PostgreSQL::Test::Session->new(node => $node);
+$logical->do(
q{select injection_points_attach('logical-replication-slot-advance-segment','wait');}
);
-$logical->query_until(
- qr/get_changes/,
- q(
-\echo get_changes
-select count(*) from pg_logical_slot_get_changes('slot_logical', null, null) \watch 1
-\q
-));
+$logical->do_async(
+ q{DO $$
+ BEGIN
+ LOOP
+ PERFORM count(*) FROM pg_logical_slot_get_changes('slot_logical', null, null);
+ PERFORM pg_sleep(0.1);
+ END LOOP;
+ END $$;}
+);
# Wait until the slot's restart_lsn points to the next WAL segment.
note('waiting for injection_point');
@@ -138,12 +135,8 @@ eval {
};
is($@, '', "Logical slot still valid");
-# If we send \q with $<psql_session>->quit the command can be sent to the
-# session already closed. So \q is in initial script, here we only finish
-# IPC::Run
-$xacts->{run}->finish;
-$checkpoint->{run}->finish;
-$logical->{run}->finish;
+# Sessions were terminated by the server crash and will be cleaned up
+# automatically when they go out of scope.
# Verify that the synchronized slots won't be invalidated immediately after
# synchronization in the presence of a concurrent checkpoint.
@@ -185,15 +178,11 @@ $primary->wait_for_replay_catchup($standby);
# checkpoint stops right before invalidating replication slots.
note('starting checkpoint');
-$checkpoint = $standby->background_psql('postgres');
-$checkpoint->query_safe(
+$standby->safe_psql('postgres',
q(select injection_points_attach('restartpoint-before-slot-invalidation','wait'))
);
-$checkpoint->query_until(
- qr/starting_checkpoint/,
- q(\echo starting_checkpoint
-checkpoint;
-));
+$checkpoint = PostgreSQL::Test::Session->new(node => $standby);
+$checkpoint->do_async(q(CHECKPOINT;));
# Wait until the checkpoint stops right before invalidating slots
note('waiting for injection_point');
@@ -216,7 +205,8 @@ $standby->safe_psql(
q{select injection_points_wakeup('restartpoint-before-slot-invalidation');
select injection_points_detach('restartpoint-before-slot-invalidation')});
-$checkpoint->quit;
+$checkpoint->wait_for_completion;
+$checkpoint->close;
# Confirm that the slot is not invalidated
is( $standby->safe_psql(
diff --git a/src/test/recovery/t/047_checkpoint_physical_slot.pl b/src/test/recovery/t/047_checkpoint_physical_slot.pl
index 4334145abe1..43193c219ba 100644
--- a/src/test/recovery/t/047_checkpoint_physical_slot.pl
+++ b/src/test/recovery/t/047_checkpoint_physical_slot.pl
@@ -8,6 +8,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -69,16 +70,11 @@ note("restart lsn before checkpoint: $restart_lsn_init");
# removing old WAL segments.
note('starting checkpoint');
-my $checkpoint = $node->background_psql('postgres');
-$checkpoint->query_safe(
+my $checkpoint = PostgreSQL::Test::Session->new(node => $node);
+$checkpoint->do(
q{select injection_points_attach('checkpoint-before-old-wal-removal','wait')}
);
-$checkpoint->query_until(
- qr/starting_checkpoint/,
- q(\echo starting_checkpoint
-checkpoint;
-\q
-));
+$checkpoint->do_async('checkpoint');
# Wait until the checkpoint stops right before removing WAL segments.
note('waiting for injection_point');
@@ -104,7 +100,11 @@ my $restart_lsn_old = $node->safe_psql('postgres',
chomp($restart_lsn_old);
note("restart lsn before stop: $restart_lsn_old");
-# Abruptly stop the server.
+$checkpoint->wait_for_completion();
+$checkpoint->close();
+
+# Abruptly stop the server (1 second should be enough for the checkpoint
+# to finish; it would be better).
$node->stop('immediate');
$node->start;
diff --git a/src/test/recovery/t/048_vacuum_horizon_floor.pl b/src/test/recovery/t/048_vacuum_horizon_floor.pl
index 52acb5561d6..a487be04dc8 100644
--- a/src/test/recovery/t/048_vacuum_horizon_floor.pl
+++ b/src/test/recovery/t/048_vacuum_horizon_floor.pl
@@ -45,12 +45,10 @@ my $orig_conninfo = $node_primary->connstr();
my $table1 = "vac_horizon_floor_table";
# Long-running Primary Session A
-my $psql_primaryA =
- $node_primary->background_psql($test_db, on_error_stop => 1);
+my $session_primaryA = PostgreSQL::Test::Session->new(node => $node_primary, dbname => $test_db);
# Long-running Primary Session B
-my $psql_primaryB =
- $node_primary->background_psql($test_db, on_error_stop => 1);
+my $session_primaryB = PostgreSQL::Test::Session->new(node => $node_primary, dbname => $test_db);
# Our test relies on two rounds of index vacuuming for reasons elaborated
# later. To trigger two rounds of index vacuuming, we must fill up the
@@ -123,7 +121,7 @@ $node_replica->poll_query_until(
# Now insert and update a tuple which will be visible to the vacuum on the
# primary but which will have xmax newer than the oldest xmin on the standby
# that was recently disconnected.
-my $res = $psql_primaryA->query_safe(
+my $res = $session_primaryA->query(
qq[
INSERT INTO $table1 VALUES (99);
UPDATE $table1 SET col1 = 100 WHERE col1 = 99;
@@ -132,7 +130,7 @@ my $res = $psql_primaryA->query_safe(
);
# Make sure the UPDATE finished
-like($res, qr/^after_update$/m, "UPDATE occurred on primary session A");
+like($res->{psqlout}, qr/^after_update$/m, "UPDATE occurred on primary session A");
# Open a cursor on the primary whose pin will keep VACUUM from getting a
# cleanup lock on the first page of the relation. We want VACUUM to be able to
@@ -145,7 +143,7 @@ my $primary_cursor1 = "vac_horizon_floor_cursor1";
# The first value inserted into the table was a 7, so FETCH FORWARD should
# return a 7. That's how we know the cursor has a pin.
# Disable index scans so the cursor pins heap pages and not index pages.
-$res = $psql_primaryB->query_safe(
+$res = $session_primaryB->query(
qq[
BEGIN;
SET enable_bitmapscan = off;
@@ -156,11 +154,11 @@ $res = $psql_primaryB->query_safe(
]
);
-is($res, 7, qq[Cursor query returned $res. Expected value 7.]);
+is($res->{psqlout}, 7, qq[Cursor query returned $res->{psqlout}. Expected value 7.]);
# Get the PID of the session which will run the VACUUM FREEZE so that we can
# use it to filter pg_stat_activity later.
-my $vacuum_pid = $psql_primaryA->query_safe("SELECT pg_backend_pid();");
+my $vacuum_pid = $session_primaryA->query_oneval("SELECT pg_backend_pid();");
# Now start a VACUUM FREEZE on the primary. It will call vacuum_get_cutoffs()
# and establish values of OldestXmin and GlobalVisState which are newer than
@@ -176,14 +174,8 @@ my $vacuum_pid = $psql_primaryA->query_safe("SELECT pg_backend_pid();");
# pages of the heap must be processed in order by a single worker to ensure
# test stability (PARALLEL 0 shouldn't be necessary but guards against the
# possibility of parallel heap vacuuming).
-$psql_primaryA->{stdin} .= qq[
- SET maintenance_io_concurrency = 0;
- VACUUM (VERBOSE, FREEZE, PARALLEL 0) $table1;
- \\echo VACUUM
- ];
-
-# Make sure the VACUUM command makes it to the server.
-$psql_primaryA->{run}->pump_nb();
+$session_primaryA->do('SET maintenance_io_concurrency = 0;');
+$session_primaryA->do_async("VACUUM (VERBOSE, FREEZE, PARALLEL 0) $table1;");
# Make sure that the VACUUM has already called vacuum_get_cutoffs() and is
# just waiting on the lock to start vacuuming. We don't want the standby to
@@ -229,7 +221,7 @@ $node_primary->poll_query_until(
# expect that a round of index vacuuming has happened and that the vacuum is
# now waiting for the cursor to release its pin on the last page of the
# relation.
-$res = $psql_primaryB->query_safe("FETCH $primary_cursor1");
+$res = $session_primaryB->query_oneval("FETCH $primary_cursor1");
is($res, 7,
qq[Cursor query returned $res from second fetch. Expected value 7.]);
@@ -243,13 +235,7 @@ $node_primary->poll_query_until(
], 't');
# Commit the transaction with the open cursor so that the VACUUM can finish.
-$psql_primaryB->query_until(
- qr/^commit$/m,
- qq[
- COMMIT;
- \\echo commit
- ]
-);
+$session_primaryB->do('COMMIT');
# VACUUM proceeds with pruning and does a visibility check on each tuple. In
# older versions of Postgres, pruning found our final dead tuple
@@ -281,8 +267,8 @@ $node_primary->safe_psql($test_db, "INSERT INTO $table1 VALUES (1);");
$node_primary->wait_for_catchup($node_replica, 'replay', $primary_lsn);
## Shut down psqls
-$psql_primaryA->quit;
-$psql_primaryB->quit;
+$session_primaryA->close;
+$session_primaryB->close;
$node_replica->stop();
$node_primary->stop();
diff --git a/src/test/recovery/t/049_wait_for_lsn.pl b/src/test/recovery/t/049_wait_for_lsn.pl
index bc216064714..9911e5bfa04 100644
--- a/src/test/recovery/t/049_wait_for_lsn.pl
+++ b/src/test/recovery/t/049_wait_for_lsn.pl
@@ -5,6 +5,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -221,20 +222,15 @@ ok($output eq "timeout", "WAIT FOR returns correct status after timeout");
my $subxact_lsn = $node_primary->safe_psql('postgres',
"SELECT pg_current_wal_insert_lsn() + 10000000000");
my $subxact_appname = 'wait_for_lsn_subxact_cleanup';
-my $subxact_session =
- $node_primary->background_psql('postgres', on_error_stop => 0);
-$subxact_session->query_until(
- qr/start/, qq[
- SET application_name = '$subxact_appname';
- BEGIN;
- SAVEPOINT wait_cleanup;
- \\echo start
- WAIT FOR LSN '${subxact_lsn}' WITH (MODE 'primary_flush');
- ROLLBACK TO wait_cleanup;
- WAIT FOR LSN '${subxact_lsn}'
- WITH (MODE 'primary_flush', timeout '10ms', no_throw);
- COMMIT;
-]);
+my $subxact_session = PostgreSQL::Test::Session->new(node => $node_primary);
+# Send the setup statements individually so the first WAIT FOR LSN can be
+# issued asynchronously: it blocks (the target LSN is unreachable) and will
+# be canceled below.
+$subxact_session->do("SET application_name = '$subxact_appname'");
+$subxact_session->do("BEGIN");
+$subxact_session->do("SAVEPOINT wait_cleanup");
+$subxact_session->do_async(
+ "WAIT FOR LSN '${subxact_lsn}' WITH (MODE 'primary_flush')");
$node_primary->poll_query_until(
'postgres',
"SELECT count(*) = 1 FROM pg_stat_activity
@@ -248,18 +244,30 @@ my $subxact_cancelled = $node_primary->safe_psql(
AND wait_event = 'WaitForWalFlush'"
);
is($subxact_cancelled, 't', "canceled WAIT FOR LSN in subtransaction");
-$subxact_session->quit;
-chomp($subxact_session->{stdout});
+
+# The cancel interrupts the blocking WAIT FOR LSN, leaving the transaction
+# in an aborted state.
+my $subxact_cancel_res = $subxact_session->get_async_result();
like(
- $subxact_session->{stderr},
+ $subxact_cancel_res->{error_message},
qr/canceling statement due to user request/,
"query cancel interrupted WAIT FOR LSN in subtransaction");
-is($subxact_session->{stdout},
+
+# Roll back to the savepoint so a second WAIT FOR LSN can register again in
+# the same backend; with no_throw it returns 'timeout' rather than erroring.
+$subxact_session->do("ROLLBACK TO wait_cleanup");
+my $subxact_timeout = $subxact_session->query_oneval(
+ "WAIT FOR LSN '${subxact_lsn}'
+ WITH (MODE 'primary_flush', timeout '10ms', no_throw)");
+is($subxact_timeout,
"timeout", "second WAIT FOR LSN timed out after savepoint rollback");
-unlike(
- $subxact_session->{stderr},
- qr/server closed the connection unexpectedly/,
+
+# The backend survived the cancel without disconnecting: the connection is
+# still usable.
+is($subxact_session->query_oneval('SELECT 1'), '1',
"WAIT FOR LSN after savepoint rollback did not disconnect");
+$subxact_session->do("COMMIT");
+$subxact_session->close;
# 5. Check mode validation: standby modes error on primary, primary mode errors
# on standby, and primary_flush works on primary. Also check that WAIT FOR
@@ -467,10 +475,8 @@ for (my $i = 0; $i < 5; $i++)
my $lsn =
$node_primary->safe_psql('postgres',
"SELECT pg_current_wal_insert_lsn()");
- $psql_sessions[$i] = $node_standby->background_psql('postgres');
- $psql_sessions[$i]->query_until(
- qr/start/, qq[
- \\echo start
+ $psql_sessions[$i] = PostgreSQL::Test::Session->new(node => $node_standby);
+ $psql_sessions[$i]->do_async(qq[
WAIT FOR LSN '${lsn}';
SELECT log_count(${i});
]);
@@ -481,7 +487,8 @@ $node_standby->safe_psql('postgres', "SELECT pg_wal_replay_resume();");
for (my $i = 0; $i < 5; $i++)
{
$node_standby->wait_for_log("count ${i}", $log_offset);
- $psql_sessions[$i]->quit;
+ $psql_sessions[$i]->wait_for_completion;
+ $psql_sessions[$i]->close;
}
ok(1, 'multiple standby_replay waiters reported consistent data');
@@ -505,10 +512,8 @@ for (my $i = 0; $i < 5; $i++)
my @write_sessions;
for (my $i = 0; $i < 5; $i++)
{
- $write_sessions[$i] = $node_standby->background_psql('postgres');
- $write_sessions[$i]->query_until(
- qr/start/, qq[
- \\echo start
+ $write_sessions[$i] = PostgreSQL::Test::Session->new(node => $node_standby);
+ $write_sessions[$i]->do_async(qq[
WAIT FOR LSN '$write_lsns[$i]' WITH (MODE 'standby_write', timeout '1d');
SELECT log_wait_done('write_done', $i);
]);
@@ -527,7 +532,8 @@ resume_walreceiver($node_standby);
for (my $i = 0; $i < 5; $i++)
{
$node_standby->wait_for_log("write_done $i", $write_log_offset);
- $write_sessions[$i]->quit;
+ $write_sessions[$i]->wait_for_completion;
+ $write_sessions[$i]->close;
}
# Verify on standby that WAL was written up to the target LSN
@@ -557,10 +563,8 @@ for (my $i = 0; $i < 5; $i++)
my @flush_sessions;
for (my $i = 0; $i < 5; $i++)
{
- $flush_sessions[$i] = $node_standby->background_psql('postgres');
- $flush_sessions[$i]->query_until(
- qr/start/, qq[
- \\echo start
+ $flush_sessions[$i] = PostgreSQL::Test::Session->new(node => $node_standby);
+ $flush_sessions[$i]->do_async(qq[
WAIT FOR LSN '$flush_lsns[$i]' WITH (MODE 'standby_flush', timeout '1d');
SELECT log_wait_done('flush_done', $i);
]);
@@ -579,7 +583,8 @@ resume_walreceiver($node_standby);
for (my $i = 0; $i < 5; $i++)
{
$node_standby->wait_for_log("flush_done $i", $flush_log_offset);
- $flush_sessions[$i]->quit;
+ $flush_sessions[$i]->wait_for_completion;
+ $flush_sessions[$i]->close;
}
# Verify on standby that WAL was flushed up to the target LSN
@@ -615,10 +620,8 @@ my @mixed_sessions;
my @mixed_modes = ('standby_replay', 'standby_write', 'standby_flush');
for (my $i = 0; $i < 6; $i++)
{
- $mixed_sessions[$i] = $node_standby->background_psql('postgres');
- $mixed_sessions[$i]->query_until(
- qr/start/, qq[
- \\echo start
+ $mixed_sessions[$i] = PostgreSQL::Test::Session->new(node => $node_standby);
+ $mixed_sessions[$i]->do_async(qq[
WAIT FOR LSN '${mixed_target_lsn}' WITH (MODE '$mixed_modes[$i % 3]', timeout '1d');
SELECT log_wait_done('mixed_done', $i);
]);
@@ -642,7 +645,8 @@ resume_walreceiver($node_standby);
for (my $i = 0; $i < 6; $i++)
{
$node_standby->wait_for_log("mixed_done $i", $mixed_log_offset);
- $mixed_sessions[$i]->quit;
+ $mixed_sessions[$i]->wait_for_completion;
+ $mixed_sessions[$i]->close;
}
# Verify all modes reached the target LSN
@@ -675,10 +679,8 @@ my $primary_flush_log_offset = -s $node_primary->logfile;
my @primary_flush_sessions;
for (my $i = 0; $i < 5; $i++)
{
- $primary_flush_sessions[$i] = $node_primary->background_psql('postgres');
- $primary_flush_sessions[$i]->query_until(
- qr/start/, qq[
- \\echo start
+ $primary_flush_sessions[$i] = PostgreSQL::Test::Session->new(node => $node_primary);
+ $primary_flush_sessions[$i]->do_async(qq[
WAIT FOR LSN '$primary_flush_lsns[$i]' WITH (MODE 'primary_flush', timeout '1d');
SELECT log_wait_done('primary_flush_done', $i);
]);
@@ -689,7 +691,8 @@ for (my $i = 0; $i < 5; $i++)
{
$node_primary->wait_for_log("primary_flush_done $i",
$primary_flush_log_offset);
- $primary_flush_sessions[$i]->quit;
+ $primary_flush_sessions[$i]->wait_for_completion;
+ $primary_flush_sessions[$i]->close;
}
# Verify on primary that WAL was flushed up to the target LSN
@@ -717,10 +720,8 @@ my @wait_modes = ('standby_replay', 'standby_write', 'standby_flush');
my @wait_sessions;
for (my $i = 0; $i < 3; $i++)
{
- $wait_sessions[$i] = $node_standby->background_psql('postgres');
- $wait_sessions[$i]->query_until(
- qr/start/, qq[
- \\echo start
+ $wait_sessions[$i] = PostgreSQL::Test::Session->new(node => $node_standby);
+ $wait_sessions[$i]->do_async(qq[
WAIT FOR LSN '${lsn4}' WITH (MODE '$wait_modes[$i]');
]);
}
@@ -758,12 +759,7 @@ ok($output eq "not in recovery",
$node_standby->stop;
$node_primary->stop;
-# If we send \q with $session->quit the command can be sent to the session
-# already closed. So \q is in initial script, here we only finish IPC::Run.
-for (my $i = 0; $i < 3; $i++)
-{
- $wait_sessions[$i]->{run}->finish;
-}
+# Sessions will be cleaned up automatically when they go out of scope.
# 9. Archive-only standby tests: verify standby_write/standby_flush work
# without a walreceiver. These exercises the replay-position floor in
@@ -862,18 +858,14 @@ $arc_primary->poll_query_until('postgres',
# Start background waiters. With replay paused, target > replay, so they
# will sleep on WaitLatch. They can only be woken by the replay-loop
# WaitLSNWakeup calls.
-my $arc_write_session = $arc_standby->background_psql('postgres');
-$arc_write_session->query_until(
- qr/start/, qq[
- \\echo start
+my $arc_write_session = PostgreSQL::Test::Session->new(node => $arc_standby);
+$arc_write_session->do_async(qq[
WAIT FOR LSN '${arc_target_lsn2}'
WITH (MODE 'standby_write', timeout '1d', no_throw);
]);
-my $arc_flush_session = $arc_standby->background_psql('postgres');
-$arc_flush_session->query_until(
- qr/start/, qq[
- \\echo start
+my $arc_flush_session = PostgreSQL::Test::Session->new(node => $arc_standby);
+$arc_flush_session->do_async(qq[
WAIT FOR LSN '${arc_target_lsn2}'
WITH (MODE 'standby_flush', timeout '1d', no_throw);
]);
@@ -887,15 +879,15 @@ $arc_standby->poll_query_until('postgres',
# STANDBY_FLUSH waiters as it replays past arc_target_lsn2.
$arc_standby->safe_psql('postgres', "SELECT pg_wal_replay_resume()");
-$arc_write_session->quit;
-$arc_flush_session->quit;
-chomp($arc_write_session->{stdout});
-chomp($arc_flush_session->{stdout});
+my $arc_write_out = $arc_write_session->get_async_result();
+my $arc_flush_out = $arc_flush_session->get_async_result();
+$arc_write_session->close;
+$arc_flush_session->close;
-is($arc_write_session->{stdout},
+is($arc_write_out->{psqlout},
'success',
"standby_write waiter woken by replay on archive-only standby");
-is($arc_flush_session->{stdout},
+is($arc_flush_out->{psqlout},
'success',
"standby_flush waiter woken by replay on archive-only standby");
@@ -1035,10 +1027,8 @@ check_wait_for_lsn_fencepost($rcv_standby, 'standby_flush', $flush_lsn,
$rcv_primary->safe_psql('postgres',
"INSERT INTO rcv_test VALUES (generate_series(200, 210))");
-my $boundary_session = $rcv_standby->background_psql('postgres');
-$boundary_session->query_until(
- qr/start/, qq[
- \\echo start
+my $boundary_session = PostgreSQL::Test::Session->new(node => $rcv_standby);
+$boundary_session->do_async(qq[
WAIT FOR LSN '${replay_lsn_plus}'
WITH (MODE 'standby_replay', timeout '1d', no_throw);
]);
@@ -1049,9 +1039,9 @@ $rcv_standby->poll_query_until('postgres',
$rcv_standby->safe_psql('postgres', "SELECT pg_wal_replay_resume()");
resume_walreceiver($rcv_standby);
-$boundary_session->quit;
-chomp($boundary_session->{stdout});
-is($boundary_session->{stdout},
+my $boundary_out = $boundary_session->get_async_result();
+$boundary_session->close;
+is($boundary_out->{psqlout},
'success',
"standby_replay: waiter at current + 1 wakes when replay advances");
@@ -1101,10 +1091,8 @@ $tl_standby2->poll_query_until('postgres',
"SELECT pg_get_wal_replay_pause_state() = 'paused'")
or die "Timed out waiting for tl_standby2 replay to pause";
-my $tl_session = $tl_standby2->background_psql('postgres');
-$tl_session->query_until(
- qr/start/, qq[
- \\echo start
+my $tl_session = PostgreSQL::Test::Session->new(node => $tl_standby2);
+$tl_session->do_async(qq[
WAIT FOR LSN '${tl_target}'
WITH (MODE 'standby_replay', timeout '1d', no_throw);
]);
@@ -1126,9 +1114,9 @@ $tl_standby2->poll_query_until('postgres',
"SELECT received_tli > 1 FROM pg_stat_wal_receiver")
or die "tl_standby2 did not follow upstream timeline switch";
-$tl_session->quit;
-chomp($tl_session->{stdout});
-is($tl_session->{stdout}, 'success',
+my $tl_out = $tl_session->get_async_result();
+$tl_session->close;
+is($tl_out->{psqlout}, 'success',
"WAIT FOR LSN survives upstream promotion and timeline switch on cascade standby"
);
diff --git a/src/test/recovery/t/050_redo_segment_missing.pl b/src/test/recovery/t/050_redo_segment_missing.pl
index e07ff0c72fe..b492db44684 100644
--- a/src/test/recovery/t/050_redo_segment_missing.pl
+++ b/src/test/recovery/t/050_redo_segment_missing.pl
@@ -7,6 +7,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -44,15 +45,11 @@ $node->safe_psql('postgres',
$node->safe_psql('postgres',
q{select injection_points_attach('create-checkpoint-run', 'wait')});
-# Start a psql session to run the checkpoint in the background and make
+# Start a session to run the checkpoint in the background and make
# the test wait on the injection point so the checkpoint stops just after
# it starts.
-my $checkpoint = $node->background_psql('postgres');
-$checkpoint->query_until(
- qr/starting_checkpoint/,
- q(\echo starting_checkpoint
-checkpoint;
-));
+my $checkpoint = PostgreSQL::Test::Session->new(node => $node);
+$checkpoint->do_async(q(CHECKPOINT;));
# Wait for the initial point to finish, the checkpointer is still
# outside its critical section. Then release to reach the second
@@ -76,7 +73,8 @@ $node->safe_psql('postgres',
q{select injection_points_wakeup('create-checkpoint-run')});
$node->wait_for_log(qr/checkpoint complete/, $log_offset);
-$checkpoint->quit;
+$checkpoint->wait_for_completion;
+$checkpoint->close;
# Retrieve the WAL file names for the redo record and checkpoint record.
my $redo_lsn = $node->safe_psql('postgres',
diff --git a/src/test/recovery/t/051_effective_wal_level.pl b/src/test/recovery/t/051_effective_wal_level.pl
index c862073c34e..5c68949f80c 100644
--- a/src/test/recovery/t/051_effective_wal_level.pl
+++ b/src/test/recovery/t/051_effective_wal_level.pl
@@ -6,6 +6,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -382,20 +383,15 @@ if ( $ENV{enable_injection_points} eq 'yes'
test_wal_level($primary, "replica|replica",
"effective_wal_level got decreased to 'replica' on primary");
- # Start a psql session to test the case where the activation process is
+ # Start a session to test the case where the activation process is
# interrupted.
- my $psql_create_slot = $primary->background_psql('postgres');
+ my $psql_create_slot = PostgreSQL::Test::Session->new(node => $primary);
- # Start the logical decoding activation process upon creating the logical
- # slot, but it will wait due to the injection point.
- $psql_create_slot->query_until(
- qr/create_slot_canceled/,
- q(\echo create_slot_canceled
-select injection_points_set_local();
-select injection_points_attach('logical-decoding-activation', 'wait');
-select pg_create_logical_replication_slot('slot_canceled', 'pgoutput');
-\q
-));
+ # Set up the injection point in this session (using set_local so it only
+ # affects this session), then start the slot creation which will block.
+ $psql_create_slot->do(q{select injection_points_set_local()});
+ $psql_create_slot->do(q{select injection_points_attach('logical-decoding-activation', 'wait')});
+ $psql_create_slot->do_async(q{select pg_create_logical_replication_slot('slot_canceled', 'pgoutput')});
$primary->wait_for_event('client backend', 'logical-decoding-activation');
note("injection_point 'logical-decoding-activation' is reached");
@@ -412,6 +408,9 @@ select pg_cancel_backend(pid) from pg_stat_activity where query ~ 'slot_canceled
$primary->wait_for_log("aborting logical decoding activation process");
test_wal_level($primary, "replica|replica",
"the activation process aborted");
+
+ # Clean up the session (the async query was cancelled, so we just close)
+ $psql_create_slot->close;
}
$primary->stop;
diff --git a/src/test/subscription/t/015_stream.pl b/src/test/subscription/t/015_stream.pl
index ac96bc3f009..41765faea34 100644
--- a/src/test/subscription/t/015_stream.pl
+++ b/src/test/subscription/t/015_stream.pl
@@ -5,6 +5,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -30,18 +31,17 @@ sub test_streaming
# Interleave a pair of transactions, each exceeding the 64kB limit.
my $offset = 0;
- my $h = $node_publisher->background_psql('postgres', on_error_stop => 0);
+ my $h = PostgreSQL::Test::Session->new(node=>$node_publisher);
# Check the subscriber log from now on.
$offset = -s $node_subscriber->logfile;
- $h->query_safe(
- q{
- BEGIN;
- INSERT INTO test_tab SELECT i, sha256(i::text::bytea) FROM generate_series(3, 5000) s(i);
- UPDATE test_tab SET b = sha256(b) WHERE mod(a,2) = 0;
- DELETE FROM test_tab WHERE mod(a,3) = 0;
- });
+ $h->do(
+ 'BEGIN',
+ 'INSERT INTO test_tab SELECT i, sha256(i::text::bytea) FROM generate_series(3, 5000) s(i)',
+ 'UPDATE test_tab SET b = sha256(b) WHERE mod(a,2) = 0',
+ 'DELETE FROM test_tab WHERE mod(a,3) = 0',
+ );
$node_publisher->safe_psql(
'postgres', q{
@@ -51,9 +51,9 @@ sub test_streaming
COMMIT;
});
- $h->query_safe('COMMIT');
+ $h->do('COMMIT');
# errors make the next test fail, so ignore them here
- $h->quit;
+ $h->close;
$node_publisher->wait_for_catchup($appname);
@@ -211,14 +211,14 @@ $node_subscriber->reload;
$node_subscriber->safe_psql('postgres', q{SELECT 1});
# Interleave a pair of transactions, each exceeding the 64kB limit.
-my $h = $node_publisher->background_psql('postgres', on_error_stop => 0);
+my $h = PostgreSQL::Test::Session->new(node => $node_publisher);
# Confirm if a deadlock between the leader apply worker and the parallel apply
# worker can be detected.
my $offset = -s $node_subscriber->logfile;
-$h->query_safe(
+$h->do(
q{
BEGIN;
INSERT INTO test_tab_2 SELECT i FROM generate_series(1, 5000) s(i);
@@ -232,8 +232,8 @@ $node_subscriber->wait_for_log(
$node_publisher->safe_psql('postgres', "INSERT INTO test_tab_2 values(1)");
-$h->query_safe('COMMIT');
-$h->quit;
+$h->do('COMMIT');
+$h->close;
$node_subscriber->wait_for_log(qr/ERROR: ( [A-Z0-9]+:)? deadlock detected/,
$offset);
@@ -260,7 +260,8 @@ $node_subscriber->safe_psql('postgres',
# Check the subscriber log from now on.
$offset = -s $node_subscriber->logfile;
-$h->query_safe(
+$h->reconnect;
+$h->do(
q{
BEGIN;
INSERT INTO test_tab_2 SELECT i FROM generate_series(1, 5000) s(i);
@@ -275,8 +276,8 @@ $node_subscriber->wait_for_log(
$node_publisher->safe_psql('postgres',
"INSERT INTO test_tab_2 SELECT i FROM generate_series(1, 5000) s(i)");
-$h->query_safe('COMMIT');
-$h->quit;
+$h->do('COMMIT');
+$h->close;
$node_subscriber->wait_for_log(qr/ERROR: ( [A-Z0-9]+:)? deadlock detected/,
$offset);
diff --git a/src/test/subscription/t/035_conflicts.pl b/src/test/subscription/t/035_conflicts.pl
index f23fe6af2a5..8058beeb680 100644
--- a/src/test/subscription/t/035_conflicts.pl
+++ b/src/test/subscription/t/035_conflicts.pl
@@ -4,6 +4,7 @@
use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Session;
use PostgreSQL::Test::Utils;
use Test::More;
@@ -455,17 +456,14 @@ if ($injection_points_supported != 0)
# Start a background session on the publisher node to perform an update and
# pause at the injection point.
- my $pub_session = $node_B->background_psql('postgres');
- $pub_session->query_until(
- qr/starting_bg_psql/,
- q{
- \echo starting_bg_psql
- BEGIN;
- UPDATE tab SET b = 2 WHERE a = 1;
- PREPARE TRANSACTION 'txn_with_later_commit_ts';
- COMMIT PREPARED 'txn_with_later_commit_ts';
- }
+ my $pub_session = PostgreSQL::Test::Session->new(node => $node_B);
+ $pub_session->do(
+ q{BEGIN},
+ q{UPDATE tab SET b = 2 WHERE a = 1},
+ q{PREPARE TRANSACTION 'txn_with_later_commit_ts'}
);
+ # COMMIT PREPARED will block on the injection point
+ $pub_session->do_async(q{COMMIT PREPARED 'txn_with_later_commit_ts'});
# Wait until the backend enters the injection point
$node_B->wait_for_event('client backend',
@@ -516,8 +514,9 @@ if ($injection_points_supported != 0)
SELECT injection_points_detach('commit-after-delay-checkpoint');"
);
- # Close the background session on the publisher node
- ok($pub_session->quit, "close publisher session");
+ # Wait for the async query to complete and close the background session
+ $pub_session->wait_for_completion;
+ $pub_session->close;
# Confirm that the transaction committed
$result = $node_B->safe_psql('postgres', 'SELECT * FROM tab WHERE a = 1');
diff --git a/src/test/subscription/t/038_walsnd_shutdown_timeout.pl b/src/test/subscription/t/038_walsnd_shutdown_timeout.pl
index f4ed5d97852..48caf8e3da4 100644
--- a/src/test/subscription/t/038_walsnd_shutdown_timeout.pl
+++ b/src/test/subscription/t/038_walsnd_shutdown_timeout.pl
@@ -8,6 +8,7 @@ use strict;
use warnings FATAL => 'all';
use PostgreSQL::Test::Cluster;
use PostgreSQL::Test::Utils;
+use PostgreSQL::Test::Session;
use Test::More;
use Time::HiRes qw(usleep);
@@ -45,7 +46,7 @@ $node_subscriber->wait_for_subscription_sync($node_publisher, 'test_sub');
# Start a background session on the subscriber to run a transaction later
# that will block the logical apply worker on a lock.
-my $sub_session = $node_subscriber->background_psql('postgres');
+my $sub_session = PostgreSQL::Test::Session->new(node => $node_subscriber);
# Test that when the logical apply worker is blocked on a lock and replication
# is stalled, shutting down the publisher causes the logical walsender to exit
@@ -53,7 +54,7 @@ my $sub_session = $node_subscriber->background_psql('postgres');
# Cause the logical apply worker to block on a lock by running conflicting
# transactions on the publisher and subscriber.
-$sub_session->query_safe("BEGIN; INSERT INTO test_tab VALUES (0);");
+$sub_session->do("BEGIN; INSERT INTO test_tab VALUES (0);");
$node_publisher->safe_psql('postgres', "INSERT INTO test_tab VALUES (0);");
my $log_offset = -s $node_publisher->logfile;
@@ -65,7 +66,7 @@ ok( $node_publisher->log_contains(
$log_offset),
"walsender exits due to wal_sender_shutdown_timeout");
-$sub_session->query_safe("ABORT;");
+$sub_session->do("ABORT;");
$node_publisher->start;
$node_publisher->wait_for_catchup('test_sub');
@@ -79,7 +80,7 @@ $node_publisher->wait_for_catchup('test_sub');
# Run a transaction on the subscriber that blocks the logical apply worker
# on a lock.
-$sub_session->query_safe("BEGIN; LOCK TABLE test_tab IN EXCLUSIVE MODE;");
+$sub_session->do("BEGIN; LOCK TABLE test_tab IN EXCLUSIVE MODE;");
# Generate enough data to fill the logical walsender's output buffer.
$node_publisher->safe_psql('postgres',
@@ -117,7 +118,7 @@ ok( $node_publisher->log_contains(
"walsender with full output buffer exits due to wal_sender_shutdown_timeout"
);
-$sub_session->query_safe("ABORT;");
+$sub_session->do("ABORT;");
# The next test depends on Perl's `kill`, which apparently is not
# portable to Windows. (It would be nice to use Test::More's `subtest`,
@@ -167,7 +168,7 @@ $node_standby->start;
# Cause the logical apply worker to block on a lock by running conflicting
# transactions on the publisher and subscriber, stalling logical replication.
$node_publisher->wait_for_catchup('test_sub');
-$sub_session->query_safe("BEGIN; LOCK TABLE test_tab IN EXCLUSIVE MODE;");
+$sub_session->do("BEGIN; LOCK TABLE test_tab IN EXCLUSIVE MODE;");
$node_publisher->safe_psql('postgres', "INSERT INTO test_tab VALUES (-1); ");
# Cause the standby's walreceiver to be blocked with SIGSTOP signal,
@@ -193,7 +194,7 @@ ok( $node_publisher->log_contains(
);
kill 'CONT', $receiverpid;
-$sub_session->quit;
+$sub_session->close;
$node_subscriber->stop('fast');
$node_standby->stop('fast');
--
2.43.0
view thread (18+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected]
Subject: Re: BackgroundPsql swallowing errors on windows
In-Reply-To: <[email protected]>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox