diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
index e209ea7163..c6086101f2 100644
--- a/src/test/perl/PostgresNode.pm
+++ b/src/test/perl/PostgresNode.pm
@@ -96,6 +96,7 @@ use File::Spec;
 use File::stat qw(stat);
 use File::Temp ();
 use IPC::Run;
+use PostgresVersion;
 use RecursiveCopy;
 use Socket;
 use Test::More;
@@ -127,6 +128,20 @@ INIT
 	$last_port_assigned = int(rand() * 16384) + 49152;
 }
 
+# Current dev version, for which we have no subclass
+# When a new stable branch is made this and the subclass hierarchy below
+# need to be adjusted.
+my $devtip = 14;
+
+INIT
+{
+	# sanity check to make sure there is a subclass for the last stable branch
+	my $last_child = 'PostgresNodeV_' . ($devtip -1);
+	eval "${last_child}->can('get_new_node') || die('not found');";
+	die "No child package $last_child found" if $@;
+}
+
+
 =pod
 
 =head1 METHODS
@@ -347,9 +362,12 @@ about this node.
 sub info
 {
 	my ($self) = @_;
+	my $varr = $self->{_pg_version};
+	my $vstr = join('.', @$varr) if ref $varr;
 	my $_info = '';
 	open my $fh, '>', \$_info or die;
 	print $fh "Name: " . $self->name . "\n";
+	print $fh "Version: " . $vstr . "\n" if $vstr;
 	print $fh "Data directory: " . $self->data_dir . "\n";
 	print $fh "Backup directory: " . $self->backup_dir . "\n";
 	print $fh "Archive directory: " . $self->archive_dir . "\n";
@@ -438,7 +456,7 @@ sub init
 	mkdir $self->backup_dir;
 	mkdir $self->archive_dir;
 
-	TestLib::system_or_bail('initdb', '-D', $pgdata, '-A', 'trust', '-N',
+	TestLib::system_or_bail('initdb', '-D', $pgdata, ($self->_initdb_flags),
 		@{ $params{extra} });
 	TestLib::system_or_bail($ENV{PG_REGRESS}, '--config-auth', $pgdata,
 		@{ $params{auth_extra} });
@@ -465,31 +483,36 @@ sub init
 
 	if ($params{allows_streaming})
 	{
-		if ($params{allows_streaming} eq "logical")
-		{
-			print $conf "wal_level = logical\n";
-		}
-		else
-		{
-			print $conf "wal_level = replica\n";
-		}
-		print $conf "max_wal_senders = 10\n";
-		print $conf "max_replication_slots = 10\n";
-		print $conf "wal_log_hints = on\n";
-		print $conf "hot_standby = on\n";
-		# conservative settings to ensure we can run multiple postmasters:
-		print $conf "shared_buffers = 1MB\n";
-		print $conf "max_connections = 10\n";
-		# limit disk space consumption, too:
-		print $conf "max_wal_size = 128MB\n";
+		$self->_init_streaming($conf, $params{allows_streaming})
 	}
 	else
 	{
-		print $conf "wal_level = minimal\n";
-		print $conf "max_wal_senders = 0\n";
+		$self->_init_wal_level_minimal($conf);
 	}
 
 	print $conf "port = $port\n";
+
+	$self->_init_network($conf, $use_tcp, $host);
+
+	close $conf;
+
+	chmod($self->group_access ? 0640 : 0600, "$pgdata/postgresql.conf")
+	  or die("unable to set permissions for $pgdata/postgresql.conf");
+
+	$self->set_replication_conf if $params{allows_streaming};
+	$self->enable_archiving     if $params{has_archiving};
+	return;
+}
+
+
+# methods use in init() which can be overridden in older versions
+
+sub _initdb_flags {	return ('-A', 'trust', '-N'); }
+
+sub _init_network
+{
+	my ($self, $conf, $use_tcp, $host) = @_;
+
 	if ($use_tcp)
 	{
 		print $conf "unix_socket_directories = ''\n";
@@ -500,14 +523,36 @@ sub init
 		print $conf "unix_socket_directories = '$host'\n";
 		print $conf "listen_addresses = ''\n";
 	}
-	close $conf;
+}
 
-	chmod($self->group_access ? 0640 : 0600, "$pgdata/postgresql.conf")
-	  or die("unable to set permissions for $pgdata/postgresql.conf");
+sub _init_streaming
+{
+	my ($self, $conf, $allows_streaming) = @_;
 
-	$self->set_replication_conf if $params{allows_streaming};
-	$self->enable_archiving     if $params{has_archiving};
-	return;
+	if ($allows_streaming eq "logical")
+	{
+		print $conf "wal_level = logical\n";
+	}
+	else
+	{
+		print $conf "wal_level = 'replica'\n";
+	}
+	print $conf "max_wal_senders = 10\n";
+	print $conf "max_replication_slots = 10\n";
+	print $conf "wal_log_hints = on\n";
+	print $conf "hot_standby = on\n";
+	# conservative settings to ensure we can run multiple postmasters:
+	print $conf "shared_buffers = 1MB\n";
+	print $conf "max_connections = 10\n";
+	# limit disk space consumption, too:
+	print $conf "max_wal_size = 128MB\n";
+}
+
+sub _init_wal_level_minimal
+{
+	my ($self, $conf) = @_;
+	print $conf "wal_level = minimal\n";
+	print $conf "max_wal_senders = 0\n";
 }
 
 =pod
@@ -539,6 +584,54 @@ sub append_conf
 
 =pod
 
+=item $node->adjust_conf(filename, setting, value, skip_equals)
+
+Modify the names config file with the setting. If the vaue is undefined,
+instead delete the setting.
+
+This will write "$setting = $value\n" in place of the existsing line,
+unless skip_equals is true, in which case it will  write
+"$setting $value\n". If the value needs to be quoted it is up to the
+caller to do that.
+
+=cut
+
+sub adjust_conf
+{
+	my ($self, $filename, $setting, $value, $skip_equals) = @_;
+
+	my $conffile = $self->data_dir . '/' . $filename;
+
+	my $contents = TestLib::slurp_file($conffile);
+	my @lines = split(/\n/,$contents);
+	my @result;
+	my $eq = $skip_equals ? '' : '= ';
+	foreach my $line (@lines)
+	{
+		if ($line !~ /^$setting\W/)
+		{
+			push(@result,$line);
+			next;
+		}
+		print "found setting '$setting'\n";
+		if (defined $value)
+		{
+			print "replaced setting '$setting'\n";
+			push(@result,"$setting $eq$value");
+		}
+	}
+    open my $fh, ">", $conffile
+      or croak "could not write \"$conffile\": $!";
+    print $fh join("\n",@result),"\n";;
+    close $fh;
+
+	chmod($self->group_access() ? 0640 : 0600, $conffile)
+	  or die("unable to set permissions for $conffile");
+
+}
+
+=pod
+
 =item $node->backup(backup_name)
 
 Create a hot backup with B<pg_basebackup> in subdirectory B<backup_name> of
@@ -565,12 +658,14 @@ sub backup
 	TestLib::system_or_bail(
 		'pg_basebackup', '-D', $backup_path, '-h',
 		$self->host,     '-p', $self->port,  '--checkpoint',
-		'fast',          '--no-sync',
+		'fast',          ($self->_backup_sync),
 		@{ $params{backup_options} });
 	print "# Backup finished\n";
 	return;
 }
 
+sub _backup_sync { return ('--no-sync'); }
+
 =item $node->backup_fs_hot(backup_name)
 
 Create a backup with a filesystem level copy in subdirectory B<backup_name> of
@@ -725,6 +820,18 @@ sub init_from_backup
 		qq(
 port = $port
 ));
+	$self->_init_network_append($use_tcp, $host);
+
+	$self->enable_streaming($root_node) if $params{has_streaming};
+	$self->enable_restoring($root_node, $params{standby})
+	  if $params{has_restoring};
+	return;
+}
+
+sub _init_network_append
+{
+	my ($self, $use_tcp, $host) = @_;
+
 	if ($use_tcp)
 	{
 		$self->append_conf('postgresql.conf', "listen_addresses = '$host'");
@@ -734,10 +841,6 @@ port = $port
 		$self->append_conf('postgresql.conf',
 			"unix_socket_directories = '$host'");
 	}
-	$self->enable_streaming($root_node) if $params{has_streaming};
-	$self->enable_restoring($root_node, $params{standby})
-	  if $params{has_restoring};
-	return;
 }
 
 =pod
@@ -797,8 +900,8 @@ sub start
 
 	# Note: We set the cluster_name here, not in postgresql.conf (in
 	# sub init) so that it does not get copied to standbys.
-	$ret = TestLib::system_log('pg_ctl', '-D', $self->data_dir, '-l',
-		$self->logfile, '-o', "--cluster-name=$name", 'start');
+	$ret = TestLib::system_log('pg_ctl', '-w', '-D', $self->data_dir, '-l',
+		$self->logfile, ($self->_cluster_name_opt($name)), 'start');
 
 	if ($ret != 0)
 	{
@@ -812,6 +915,12 @@ sub start
 	return 1;
 }
 
+sub _cluster_name_opt
+{
+	my ($self, $name) =  @_;
+	return ('-o', "--cluster-name=$name");
+}
+
 =pod
 
 =item $node->kill9()
@@ -911,7 +1020,7 @@ sub restart
 
 	print "### Restarting node \"$name\"\n";
 
-	TestLib::system_or_bail('pg_ctl', '-D', $pgdata, '-l', $logfile,
+	TestLib::system_or_bail('pg_ctl', '-w', '-D', $pgdata, '-l', $logfile,
 		'restart');
 
 	$self->_update_pid(1);
@@ -975,13 +1084,15 @@ sub enable_streaming
 
 	print "### Enabling streaming replication for node \"$name\"\n";
 	$self->append_conf(
-		'postgresql.conf', qq(
+		$self->_recovery_file, qq(
 primary_conninfo='$root_connstr'
 ));
 	$self->set_standby_mode();
 	return;
 }
 
+sub _recovery_file { return "postgresql.conf"; }
+
 # Internal routine to enable archive recovery command on a standby node
 sub enable_restoring
 {
@@ -1004,7 +1115,7 @@ sub enable_restoring
 	  : qq{cp "$path/%f" "%p"};
 
 	$self->append_conf(
-		'postgresql.conf', qq(
+		$self->_recovery_file, qq(
 restore_command = '$copy_command'
 ));
 	if ($standby)
@@ -1196,9 +1307,72 @@ sub get_new_node
 	# Add node to list of nodes
 	push(@all_nodes, $node);
 
+	# Get information about the node
+	$node->_read_pg_config;
+
+	# bless the object into the appropriate subclass,
+	# according to the found version
+	if (ref $node->{_pg_version} && $node->{_pg_version} < $devtip )
+	{
+		my $maj = $node->{_pg_version}->[0];
+		my $subclass = __PACKAGE__ . "V_$maj";
+		if ($maj < 10)
+		{
+			$maj = $node->{_pg_version}->[1];
+			$subclass .= "_$maj";
+		}
+		bless $node, $subclass;
+	}
+
 	return $node;
 }
 
+# Private routine to run the pg_config binary found in our environment (or in
+# our install_path, if we have one), and collect all fields that matter to us.
+#
+sub _read_pg_config
+{
+	my ($self) = @_;
+	my $inst = $self->{_install_path};
+	my $pg_config = "pg_config";
+
+	if (defined $inst)
+	{
+		# If the _install_path is invalid, our PATH variables might find an
+		# unrelated pg_config executable elsewhere.  Sanity check the
+		# directory.
+		BAIL_OUT("directory not found: $inst")
+			unless -d $inst;
+
+		# If the directory exists but is not the root of a postgresql
+		# installation, or if the user configured using
+		# --bindir=$SOMEWHERE_ELSE, we're not going to find pg_config, so
+		# complain about that, too.
+		$pg_config = "$inst/bin/pg_config";
+		BAIL_OUT("pg_config not found: $pg_config")
+			unless -e $pg_config;
+		BAIL_OUT("pg_config not executable: $pg_config")
+			unless -x $pg_config;
+
+		# Leave $pg_config install_path qualified, to be sure we get the right
+		# version information, below, or die trying
+	}
+
+	local %ENV = $self->_get_env();
+
+	# We only want the version field
+	open my $fh, "-|", $pg_config, "--version"
+		or
+		BAIL_OUT("$pg_config failed: $!");
+	my $version_line = <$fh>;
+	close $fh or die;
+
+	$self->{_pg_version} = PostgresVersion->new($version_line);
+
+	BAIL_OUT("could not parse pg_config --version output: $version_line")
+		unless defined $self->{_pg_version};
+}
+
 # Private routine to return a copy of the environment with the PATH and
 # (DY)LD_LIBRARY_PATH correctly set when there is an install path set for
 # the node.
@@ -1271,6 +1445,28 @@ sub _get_env
 	return (%inst_env);
 }
 
+# Private routine to get an installation path qualified command.
+#
+# IPC::Run maintains a cache, %cmd_cache, mapping commands to paths.  Tests
+# which use nodes spanning more than one postgres installation path need to
+# avoid confusing which installation's binaries get run.  Setting $ENV{PATH} is
+# insufficient, as IPC::Run does not check to see if the path has changed since
+# caching a command.
+sub installed_command
+{
+	my ($self, $cmd) = @_;
+
+	# Nodes using alternate installation locations use their installation's
+	# bin/ directory explicitly
+	return join('/', $self->{_install_path}, 'bin', $cmd)
+		if defined $self->{_install_path};
+
+	# Nodes implicitly using the default installation location rely on IPC::Run
+	# to find the right binary, which should not cause %cmd_cache confusion,
+	# because no nodes with other installation paths do it that way.
+	return $cmd;
+}
+
 =pod
 
 =item get_free_port()
@@ -1516,6 +1712,14 @@ is set to true if the psql call times out.
 If set, use this as the connection string for the connection to the
 backend.
 
+=item host => B<value>
+
+If this parameter is set, this host is used for the connection attempt.
+
+=item port => B<port>
+
+If this parameter is set, this port is used for the connection attempt.
+
 =item replication => B<value>
 
 If set, add B<replication=value> to the conninfo string.
@@ -1568,7 +1772,23 @@ sub psql
 	}
 	$psql_connstr .= defined $replication ? " replication=$replication" : "";
 
-	my @psql_params = ('psql', '-XAtq', '-d', $psql_connstr, '-f', '-');
+	my @no_password = ('-w') if ($params{no_password});
+
+	my @host = ('-h', $params{host})
+		if defined $params{host};
+	my @port = ('-p', $params{port})
+		if defined $params{port};
+
+	my @psql_params = (
+		$self->installed_command('psql'),
+		'-XAtq',
+		@no_password,
+		@host,
+		@port,
+		'-d',
+		$psql_connstr,
+		'-f',
+		'-');
 
 	# If the caller wants an array and hasn't passed stdout/stderr
 	# references, allocate temporary ones to capture them so we
@@ -1754,7 +1974,7 @@ sub background_psql
 	my $replication = $params{replication};
 
 	my @psql_params = (
-		'psql',
+		$self->installed_command('psql'),
 		'-XAtq',
 		'-d',
 		$self->connstr($dbname)
@@ -1831,7 +2051,11 @@ sub interactive_psql
 
 	local %ENV = $self->_get_env();
 
-	my @psql_params = ('psql', '-XAt', '-d', $self->connstr($dbname));
+	my @psql_params = (
+		$self->installed_command('psql'),
+		'-XAt',
+		'-d',
+		$self->connstr($dbname));
 
 	push @psql_params, @{ $params{extra_params} }
 	  if defined $params{extra_params};
@@ -1888,6 +2112,14 @@ If given, it must be an array reference containing a list of regular
 expressions that must NOT match against the server log.  They will be
 passed to C<Test::More::unlike()>.
 
+=item host => B<value>
+
+If this parameter is set, this host is used for the connection attempt.
+
+=item port => B<port>
+
+If this parameter is set, this port is used for the connection attempt.
+
 =back
 
 =cut
@@ -1924,7 +2156,9 @@ sub connect_ok
 	my ($ret, $stdout, $stderr) = $self->psql(
 		'postgres',
 		$sql,
-		extra_params  => ['-w'],
+		no_password   => 1,
+		host          => $params{host},
+		port          => $params{port},
 		connstr       => "$connstr",
 		on_error_stop => 0);
 
@@ -2041,7 +2275,13 @@ sub poll_query_until
 
 	$expected = 't' unless defined($expected);    # default value
 
-	my $cmd = [ 'psql', '-XAt', '-c', $query, '-d', $self->connstr($dbname) ];
+	my $cmd = [
+		$self->installed_command('psql'),
+		'-XAt',
+		'-c',
+		$query,
+		'-d',
+		$self->connstr($dbname) ];
 	my ($stdout, $stderr);
 	my $max_attempts = 180 * 10;
 	my $attempts     = 0;
@@ -2225,13 +2465,7 @@ mode must be specified.
 sub lsn
 {
 	my ($self, $mode) = @_;
-	my %modes = (
-		'insert'  => 'pg_current_wal_insert_lsn()',
-		'flush'   => 'pg_current_wal_flush_lsn()',
-		'write'   => 'pg_current_wal_lsn()',
-		'receive' => 'pg_last_wal_receive_lsn()',
-		'replay'  => 'pg_last_wal_replay_lsn()');
-
+	my %modes = $self->_lsn_mode_map;
 	$mode = '<undef>' if !defined($mode);
 	croak "unknown mode for 'lsn': '$mode', valid modes are "
 	  . join(', ', keys %modes)
@@ -2249,6 +2483,16 @@ sub lsn
 	}
 }
 
+sub _lsn_mode_map
+{
+	return (
+		'insert'  => 'pg_current_wal_insert_lsn()',
+		'flush'   => 'pg_current_wal_flush_lsn()',
+		'write'   => 'pg_current_wal_lsn()',
+		'receive' => 'pg_last_wal_receive_lsn()',
+		'replay'  => 'pg_last_wal_replay_lsn()');
+}
+
 =pod
 
 =item $node->wait_for_catchup(standby_name, mode, target_lsn)
@@ -2295,8 +2539,10 @@ sub wait_for_catchup
 	}
 	else
 	{
-		$lsn_expr = 'pg_current_wal_lsn()';
+		my %funcmap = $self->_lsn_mode_map;
+		$lsn_expr = $funcmap{write};
 	}
+	my $suffix = $self->_replication_suffix;
 	print "Waiting for replication conn "
 	  . $standby_name . "'s "
 	  . $mode
@@ -2304,13 +2550,16 @@ sub wait_for_catchup
 	  . $lsn_expr . " on "
 	  . $self->name . "\n";
 	my $query =
-	  qq[SELECT $lsn_expr <= ${mode}_lsn AND state = 'streaming' FROM pg_catalog.pg_stat_replication WHERE application_name = '$standby_name';];
+	  qq[SELECT $lsn_expr <= ${mode}$suffix AND state = 'streaming' FROM pg_catalog.pg_stat_replication WHERE application_name in ('$standby_name', 'walreceiver');];
 	$self->poll_query_until('postgres', $query)
 	  or croak "timed out waiting for catchup";
 	print "done\n";
 	return;
 }
 
+sub _current_lsn_func { return "pg_current_wal_lsn"; }
+sub _replication_suffix { return "_lsn"; }
+
 =pod
 
 =item $node->wait_for_slot_catchup(slot_name, mode, target_lsn)
@@ -2461,7 +2710,8 @@ sub pg_recvlogical_upto
 	croak 'endpos must be specified'    unless defined($endpos);
 
 	my @cmd = (
-		'pg_recvlogical', '-S', $slot_name, '--dbname',
+		$self->installed_command('pg_recvlogical'),
+		'-S', $slot_name, '--dbname',
 		$self->connstr($dbname));
 	push @cmd, '--endpos', $endpos;
 	push @cmd, '-f', '-', '--no-loop', '--start';
@@ -2528,4 +2778,454 @@ sub pg_recvlogical_upto
 
 =cut
 
+##########################################################################
+#
+# Subclasses.
+#
+# There should be a subclass for each old version supported. The newest
+# (i.e. the one for the latest stable release) should inherit from the
+# PostgresNode class. Each other subclass should inherit from the subclass
+# repesenting the immediately succeeding stable release.
+#
+# The name must be PostgresNodeV_nn{_nn} where V_nn_{_nn} corresonds to the
+# release number (e.g. V_12 for release 12 or V_9_6 fpr release 9.6.)
+# PostgresNode knows about this naming convention and blesses each node
+# into the appropriate subclass.
+#
+# Each time a new stable release branch is made a subclass should be added
+# that inherits from PostgresNode, and be made the parent of the previous
+# subclass that inherited from PostgresNode.
+#
+# An empty package means that there are no differences that need to be
+# handled between this release and the later release.
+#
+##########################################################################
+
+package PostgresNodeV_13; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNode);
+
+# https://www.postgresql.org/docs/10/release-13.html
+
+##########################################################################
+
+package PostgresNodeV_12; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_13);
+
+# https://www.postgresql.org/docs/12/release-12.html
+
+##########################################################################
+
+package PostgresNodeV_11; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_12);
+
+# https://www.postgresql.org/docs/11/release-11.html
+
+# max_wal_senders + superuser_reserved_connections must be < max_connections
+# uses recovery.conf
+
+sub _recovery_file { return "recovery.conf"; }
+
+sub set_standby_mode
+{
+	my $self = shift;
+	$self->append_conf(
+		"recovery.conf",
+		"standby_mode = on\n");
+}
+
+
+sub init
+{
+	my ($self, @args) = @_;
+	$self->SUPER::init(@args);
+	$self->adjust_conf('postgresql.conf','max_wal_senders','5');
+}
+
+##########################################################################
+
+package PostgresNodeV_10; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_11);
+
+# https://www.postgresql.org/docs/10/release-10.html
+
+##########################################################################
+
+package PostgresNodeV_9_6; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_10);
+
+# https://www.postgresql.org/docs/9.6/release-9-6.html
+
+# no -no-sync option for pg_basebackup
+# replication conf is a bit different too
+# lsn function names are different
+
+sub _backup_sync { return (); }
+
+sub set_replication_conf
+{
+    my ($self) = @_;
+    my $pgdata = $self->data_dir;
+
+    $self->host eq $test_pghost
+      or die "set_replication_conf only works with the default host";
+
+    open my $hba, ">>$pgdata/pg_hba.conf";
+    print $hba "\n# Allow replication (set up by PostgresNode.pm)\n";
+    if (!$TestLib::windows_os)
+    {
+        print $hba "local replication all trust\n";
+    }
+    else
+    {
+        print $hba
+"host replication all $test_localhost/32 sspi include_realm=1 map=regress\n";
+    }
+    close $hba;
+}
+
+sub _lsn_mode_map
+{
+	return (
+		'insert'  => 'pg_current_xlog_insert_location()',
+		'flush'   => 'pg_current_xlog_flush_location()',
+		'write'   => 'pg_current_xlog_location()',
+		'receive' => 'pg_last_xlog_receive_location()',
+		'replay'  => 'pg_last_xlog_replay_location()');
+}
+
+sub _replication_suffix { return "_location"; }
+
+##########################################################################
+
+package PostgresNodeV_9_5; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_9_6);
+
+# https://www.postgresql.org/docs/9.5/release-9-5.html
+
+##########################################################################
+
+package PostgresNodeV_9_4; ## no critic (ProhibitMultiplePackages)
+
+use Test::More;
+use parent -norequire, qw(PostgresNodeV_9_5);
+
+# https://www.postgresql.org/docs/9.4/release-9-4.html
+
+# no log_replication_commands
+# no wal_retrieve_retry_interval
+# no cluster_name
+
+
+sub init
+{
+	my ($self, @args) = @_;
+	$self->SUPER::init(@args);
+	$self->adjust_conf('postgresql.conf','log_replication_commands',undef);
+	$self->adjust_conf('postgresql.conf','wal_retrieve_retry_interval',undef);
+	$self->adjust_conf('postgresql.conf','max_wal_size',undef);
+}
+
+sub _cluster_name_opt { return (); }
+
+##########################################################################
+
+package PostgresNodeV_9_3; ## no critic (ProhibitMultiplePackages)
+
+use Test::More;
+use parent -norequire, qw(PostgresNodeV_9_4);
+
+# https://www.postgresql.org/docs/9.3/release-9-3.html
+
+# no logical replication, so no logical streaming
+
+sub init
+{
+	my ($self, %params) = @_;
+	$self->SUPER::init(%params);
+	$self->adjust_conf('postgresql.conf','max_replication_slots',undef);
+	$self->adjust_conf('postgresql.conf','wal_log_hints',undef);
+	$self->adjust_conf('postgresql.conf','wal_level','hot_standby') if $params{allows_streaming};
+}
+
+sub _init_streaming
+{
+	my ($self, $conf, $allows_streaming) = @_;
+
+	print "9.3 _init_streaming called\n";
+
+	BAIL_OUT("Server Version too old for logical replication")
+	  if ($allows_streaming eq "logical");
+	$self->SUPER::_init_streaming($conf, $allows_streaming);
+}
+
+
+##########################################################################
+
+package PostgresNodeV_9_2; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_9_3);
+
+# https://www.postgresql.org/docs/9.3/release-9-2.html
+
+# no -N flag to initdb
+# socket location is in unix_socket_directory
+
+sub _initdb_flags {	return ('-A', 'trust'); }
+
+sub _init_network
+{
+	my ($self, $conf, $use_tcp, $host) = @_;
+
+	if ($use_tcp)
+	{
+		print $conf "unix_socket_directory = ''\n";
+		print $conf "listen_addresses = '$host'\n";
+	}
+	else
+	{
+		print $conf "unix_socket_directory = '$host'\n";
+		print $conf "listen_addresses = ''\n";
+	}
+}
+
+sub _init_network_append
+{
+	my ($self, $use_tcp, $host) = @_;
+
+	if ($use_tcp)
+	{
+		$self->append_conf('postgresql.conf', "listen_addresses = '$host'");
+	}
+	else
+	{
+		$self->append_conf('postgresql.conf',
+			"unix_socket_directory = '$host'");
+	}
+}
+
+
+##########################################################################
+
+package PostgresNodeV_9_1; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_9_2);
+
+# https://www.postgresql.org/docs/9.3/release-9-1.html
+
+##########################################################################
+
+package PostgresNodeV_9_0; ## no critic (ProhibitMultiplePackages)
+
+use Test::More;
+use parent -norequire, qw(PostgresNodeV_9_1);
+
+# https://www.postgresql.org/docs/9.3/release-9-0.html
+
+# no wal_senders setting
+# no pg_basebackup
+# can't turn off restart after crash
+
+sub init
+{
+	my ($self, @args) = @_;
+	$self->SUPER::init(@args);
+	$self->adjust_conf('postgresql.conf','restart_after_crash',undef);
+	$self->adjust_conf('postgresql.conf','wal_senders',undef);
+}
+
+sub _init_restart_after_crash { return  ""; }
+
+sub backup
+{
+	BAIL_OUT("Server version too old for backup function");
+}
+
+sub init_from_backup
+{
+	BAIL_OUT("Server version too old for init_from_backup function");
+}
+
+sub _init_streaming
+{
+	my ($self, $conf, $allows_streaming) = @_;
+
+	BAIL_OUT("Server Version too old for logical replication")
+	  if ($allows_streaming eq "logical");
+	$self->SUPER::_init_streaming($conf, $allows_streaming);
+}
+
+##########################################################################
+
+package PostgresNodeV_8_4; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_9_0);
+
+# https://www.postgresql.org/docs/9.3/release-8-4.html
+
+# no wal_level setting
+
+sub _init_wal_level_minimal
+{
+	# do nothing
+}
+
+
+sub _init_restart_after_crash { return  ""; }
+
+##########################################################################
+
+package PostgresNodeV_8_3; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_8_4);
+
+# https://www.postgresql.org/docs/9.3/release-8-3.html
+
+# no stats_temp_directory setting
+# no -w flag for psql
+
+sub init
+{
+	my ($self, @args) = @_;
+	$self->SUPER::init(@args);
+	$self->adjust_conf('postgresql.conf','stats_temp_directory',undef);
+}
+
+sub psql
+{
+	my ($self, $dbname, $sql, %params) = @_;
+
+	local $ENV{PGPASSWORD};
+
+	if ($params{no_password})
+	{
+		# since there is no -w flag for psql here, we try to
+		# inhibit a password prompt by setting PGPASSWORD instead
+		$ENV{PGPASSWORD} = 'no_such_password_12345';
+		delete $params{no_password};
+	}
+
+	$self->SUPER::psql($dbname, $sql, %params);
+}
+
+##########################################################################
+
+package PostgresNodeV_8_2; ## no critic (ProhibitMultiplePackages)
+
+use Test::More;
+use parent -norequire, qw(PostgresNodeV_8_3);
+
+
+# https://www.postgresql.org/docs/9.3/release-8-2.html
+
+# no support for connstr with =
+
+sub psql
+{
+	my ($self, $dbname, $sql, %params) = @_;
+
+	my $connstr = $params{connstr};
+
+	BAIL_OUT("Server version too old: complex connstr with = not supported")
+	  if (defined($connstr) && $connstr =~ /=/);
+
+	# Handle the simple common case where there's no explicit connstr
+	$params{host} ||= $self->host;
+	$params{port} ||= $self->port;
+	# Supply this so the superclass doesn't try to construct a connstr
+	$params{connstr} ||= $dbname;
+
+	$self->SUPER::psql($dbname, $sql, %params);
+}
+
+##########################################################################
+
+package PostgresNodeV_8_1; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_8_2);
+
+# https://www.postgresql.org/docs/9.3/release-8-1.html
+
+##########################################################################
+
+package PostgresNodeV_8_0; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_8_1);
+
+# https://www.postgresql.org/docs/9.3/release-8-0.html
+
+##########################################################################
+
+package PostgresNodeV_7_4; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_8_0);
+
+# https://www.postgresql.org/docs/9.3/release-7-4.html
+
+# no '-A trust' for initdb
+# no log_line_prefix
+# no 'log_statement = all' (only 'on')
+# no listen_addresses - use tcpip_socket and virtual_host instead
+# no archiving
+
+sub _initdb_flags {	return (); }
+
+sub init
+{
+	my ($self, @args) = @_;
+	$self->SUPER::init(@args);
+	$self->adjust_conf('postgresql.conf','log_line_prefix',undef);
+	$self->adjust_conf('postgresql.conf','log_statement','on');
+}
+
+sub _init_network
+{
+	my ($self, $conf, $use_tcp, $host) = @_;
+
+	if ($use_tcp)
+	{
+		print $conf "unix_socket_directory = ''\n";
+		print $conf "virtual_host = '$host'\n";
+		print $conf "tcpip_socket = true\n";
+	}
+	else
+	{
+		print $conf "unix_socket_directory = '$host'\n";
+	}
+}
+
+
+##########################################################################
+
+package PostgresNodeV_7_3; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire, qw(PostgresNodeV_7_4);
+
+# https://www.postgresql.org/docs/9.3/release-7-3.html
+
+##########################################################################
+
+package PostgresNodeV_7_2; ## no critic (ProhibitMultiplePackages)
+
+use parent -norequire,  qw(PostgresNodeV_7_3);
+
+# https://www.postgresql.org/docs/9.3/release-7-2.html
+
+# no log_statement
+
+sub init
+{
+	my ($self, @args) = @_;
+	$self->SUPER::init(@args);
+	$self->adjust_conf('postgresql.conf','log_statement',undef);
+}
+
+##########################################################################
+# traditional module 'value'
+
 1;
diff --git a/src/test/perl/PostgresVersion.pm b/src/test/perl/PostgresVersion.pm
new file mode 100644
index 0000000000..d99c991597
--- /dev/null
+++ b/src/test/perl/PostgresVersion.pm
@@ -0,0 +1,61 @@
+
+package PostgresVersion;
+
+use strict;
+use warnings;
+
+use Scalar::Util qw(blessed);
+
+use overload
+  '<=>' => \&_version_cmp,
+  'cmp' => \&_version_cmp;
+
+sub new
+{
+	my $class = shift;
+	my $arg = shift;
+
+	# Accept standard formats, in case caller has handed us the output of a
+	# postgres command line tool
+	$arg = $1
+		if ($arg =~ m/\(?PostgreSQL\)? (\d+(?:\.\d+)*(?:devel)?)/);
+
+	# Split into an array
+	my @result = split(/\./, $arg);
+
+	# Treat development versions as having a minor/micro version one less than
+	# the first released version of that branch.
+	if ($result[$#result] =~ m/^(\d+)devel$/)
+	{
+		pop(@result);
+		push(@result, $1, -1);
+	}
+
+	my $res = [ @result ];
+	bless $res, $class;
+	return $res;
+}
+
+
+# Routine which compares the _pg_version_array obtained for the two
+# arguments and returns -1, 0, or 1, allowing comparison between two
+# PostgresNodes or a PostgresNode and a version string.
+#
+# if the second argument is not a blessed object ew call the constructor
+# to make one.
+#
+sub _version_cmp
+{
+	my ($a, $b) = @_;
+
+	$b = __PACKAGE__->new($b) unless blessed($b);
+
+	for (my $idx = 0; ; $idx++)
+	{
+		return 0 unless (defined $a->[$idx] && defined $b->[$idx]);
+		return $a->[$idx] <=> $b->[$idx]
+			if ($a->[$idx] <=> $b->[$idx]);
+	}
+}
+
+1;
