From fdb5cbab3375d9d2e4da078cf6ee7eaf7de5c8fd Mon Sep 17 00:00:00 2001
From: Kyotaro Horiguchi <horiguchi.kyotaro@lab.ntt.co.jp>
Date: Thu, 20 Jul 2017 14:56:51 +0900
Subject: [PATCH 1/2] Add new TAP test library PsqlSession.pm

PostgreNode::psql makes temporary session to run commands so it is not
usable when more interactive operation on a continued session. This
library offers continuous sessions feature that can execute multiple
sql commands separately.
---
 src/test/perl/PsqlSession.pm | 341 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 341 insertions(+)
 create mode 100644 src/test/perl/PsqlSession.pm

diff --git a/src/test/perl/PsqlSession.pm b/src/test/perl/PsqlSession.pm
new file mode 100644
index 0000000..d69fd14
--- /dev/null
+++ b/src/test/perl/PsqlSession.pm
@@ -0,0 +1,341 @@
+
+=pod
+
+=head1 NAME
+
+PsqlSession - class representing PostgreSQL psql instance
+
+=head1 SYNOPSIS
+
+  use PsqlSession;
+
+  my $session = get_new_session('session1', $server);
+
+  to connect to a PostgreNode as $server, or
+
+  my $session = get_new_session('session1', 'localhost', '5432', 'postgres');
+
+  to specify each options explicitly.
+
+  # Connect to the server
+  $session->open();
+
+  # Execute an SQL query
+  $ret = $session->execsql('SELECT now();');
+
+  Returns a pair of output of stdout, and stderr in array context.
+
+  ($out, $err) = $session->execsql('SELECT now();');
+
+  $session->execsql_multi('SELECT 1;', 'SELECT now();');
+
+  is just a shortcut of writing many execsqls.
+
+  # End the session
+  $session->close();
+
+=head1 DESCRIPTION
+
+PsqlSession contains a set of routines able to work on a psql session,
+allowing to connect, send a command and receive the result and close.
+
+The IPC::Run module is required.
+
+=cut
+
+package PsqlSession;
+
+use strict;
+use warnings;
+
+use Exporter 'import';
+use Test::More;
+use TestLib ();
+use Scalar::Util qw(blessed);
+
+our @EXPORT = qw(
+  get_new_session
+);
+
+
+=pod
+
+=head1 METHODS
+
+=over
+
+=item PsqlSession::new($class, $name, $pghost, $pgport, $dbname)
+
+Create a new PsqlSession instance. Does not connect.
+
+You should generally prefer to use get_new_session() instead since it
+takes care of finding host name, port number or database name.
+
+=cut
+
+sub new
+{
+	my ($class, $name, $pghost, $pgport, $dbname) = @_;
+
+	my $self = {
+		_name => $name,
+		_host => $pghost,
+		_port => $pgport,
+		_dbname => $dbname };
+
+	bless $self, $class;
+
+#	$self->dump_info;
+
+	return $self;
+}
+
+=pod
+
+=item $session->name()
+
+The name assigned to the session at creation time.
+
+=cut
+
+sub name
+{
+	return $_[0]->{_name};
+}
+
+=pod
+
+=item $session->host()
+
+Return the host (like PGHOST) for this instance. May be a UNIX socket path.
+
+=cut
+
+sub host
+{
+	return $_[0]->{_host};
+}
+
+=pod
+
+=item $session->port()
+
+Get the port number connects to. This won't necessarily be a TCP port
+open on the local host since we prefer to use unix sockets if
+possible.
+
+=cut
+
+sub port
+{
+	return $_[0]->{_port};
+}
+
+=pod
+
+=item $session->dbname()
+
+Get the database name this session connects to.
+
+=cut
+
+sub dbname
+{
+	return $_[0]->{_dbname};
+}
+
+=pod
+
+=item $session->errstate()
+
+Get the error state of this session. 0 means no error and 1 means
+error. This value is reset at the starting of every execution of an
+SQL query.
+
+=cut
+
+sub errstate
+{
+	return $_[0]->{_errstate};
+}
+
+=pod
+
+=item $session->open()
+
+Open this session.
+
+=cut
+
+sub open
+{
+	my ($self) = @_;
+
+	# Create anonymous scalar references to be passed to IPC::Run::start
+	my $in = do {\my $anon};
+	my $out = do {\my $anon};
+	my $err = do {\my $anon};
+
+	# create and store the harness
+	$self->{_run} = IPC::Run::start
+		["psql", $self->dbname, "-p" ,$self->port, "-h", $self->host, "-n"],
+		$in, $out, $err, IPC::Run::timeout(10);
+
+	$self->{_in} = $in;
+	$self->{_out} = $out;
+	$self->{_err} = $err;
+	$self->{_errstate} = 0;
+}
+
+=pod
+
+=item $session->clearerr()
+
+Clear error status.
+
+=cut
+
+sub clearerr
+{
+	$_[0]->errstate = 0;
+}
+
+=pod
+
+=item $session->close()
+
+Close this session. This session can be relaunched by open().
+
+=cut
+
+sub close
+{
+	my ($self) = @_;
+
+	if (defined $self->{_run})
+	{
+		IPC::Run::finish($self->{_run});
+		delete $self->{_run};
+	}
+}
+
+=pod
+
+=item $session->execsql($sqlstr)
+
+Execute sql commands in $sqlstr then returns the
+result. Multistatement is allowed.
+
+In scalar context, this function returns a string that contains the
+result of the query. In array context, returns an array consists of
+the output of stdout and stderr.
+
+For implment reasons, this function adds a sentinel query that gives a
+end marker for reading the result at the end of the query. The stderr
+may contain the log for the sentinel query on certain setting, such as
+log_min_duration_statement = 0.
+
+If the stderr contained the string 'ERROR:', this session is marked as
+error. This state can be checked by $session->errstate();
+
+=cut
+
+sub execsql
+{
+	my ($self, $cmd) = @_;
+	my $run = $self->{_run};
+
+	die if (!defined $run);
+	${$self->{_out}} = '';
+	${$self->{_err}} = '';
+
+	# add a query to receive an end marker
+	$cmd =~ s/[;\n]+$//;
+	${$self->{_in}} .= $cmd.";select '###END###' as end;\n";
+
+	# wait for the end marker or an error
+	$self->{_run}->pump until
+		(${$self->{_out}} =~ /###END###\n\(1 row\)\n/ ||
+		 ${$self->{_err}} =~ /ERROR:/);
+
+	# set error state
+	$self->{_errstate} = 1 if (${$self->{_err}} =~ /ERROR:/);
+
+	# remove useless result
+	my $out = ${$self->{_out}};
+	$out =~ s/ *end *\n-+\n *###END### *\n\(1 row\)\n\n//g;
+
+	return wantarray ? ($out, ${$self->{_err}}) : $out;
+}
+
+=pod
+
+=item $session->execsql_multi("query", "query",...)
+
+Run multiple queries. This is just a convenient function using instead
+of a series of execsql(). Returns concatenated output of each
+query. Returns the output of stdout in scalar context and a pair of
+stdout and stderr in array context.
+
+If any query fails, this function returns immediately.
+
+=cut
+
+sub execsql_multi
+{
+	my ($self) = shift(@_);
+	my ($retout, $reterr, $out, $err);
+
+	foreach my $cmd (@_)
+    {
+		($out, $err) = $self->execsql($cmd);
+		$retout .= $out;
+		$reterr .= $err;
+		last if ($self->errstate);
+	}
+
+	return wantarray ? ($retout, $reterr) : $retout;
+}
+
+=pod
+
+=item get_new_sesion($name, $host, $port, $dbname) or
+     get_new_sesion($name, $PostgreNode)
+
+Build a new session object to connect to the specified
+server. Standalone function that's automatically imported.
+
+In the first form, returns a session according to the parameters. In
+the second form, returns a session connects to the node.
+
+=cut
+
+
+sub get_new_session
+{
+	my ($name, $host_or_ref, $pgport, $dbname) = @_;
+	my $pghost = $host_or_ref;
+
+	if (blessed($host_or_ref) && $host_or_ref->isa('PostgresNode'))
+	{
+		$pghost = $host_or_ref->host;
+		$pgport = $host_or_ref->port;
+	}
+
+	# Fill in default values
+	$pghost = 'localhost' if (! defined $pghost);
+	$pgport = 5432 if (! defined $pgport);
+	$dbname = 'postgres' if (! defined $dbname);
+
+	my $session = new PsqlSession($name, $pghost, $pgport, $dbname);
+
+	return $session;
+}
+
+=pod
+
+=back
+
+=cut
+
+1;
-- 
2.9.2

