# -*- perl -*-
# RServ.pm
# Vadim Mikheev, (c) 2001, PostgreSQL Inc.

package eRServer;

require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(ReadConfig CopyTbl);
@EXPORT_OK = qw();

use IO::File;

$debug = 0;
$quiet = 0;

my %Mtables = ();
my %Stables = ();

my $Sinfo = '';			# Sync info
my $Snapshot = '';		# Snapshot (for PTE)
my $ActiveLogTable = 0;	#
my $UseOldLogTable = 1;
my @UStable = ();		# Info about snapshots generated after last succeeded one

my %Childs = ();

sub ReadConfig
{
  open(IN, "grep ^replic @prefix@/etc/replication.cfg |");

  while(<IN>) {
    chomp();
    ( $field, $value ) = split(/=/);
    $replic->{$field} = $value;
  }

  return $replic;
}

sub CopyTbl
{
  local $masteruser = shift;
  local $masterdb = shift;
  local $master = shift;
  local $slave = shift;
  local $table = shift;

  $get_struct = $master->prepare("SELECT a.attname, format_type(a.atttypid, a.atttypmod), a.attnotnull, a.atthasdef, a.attnum
                                    FROM pg_class c, pg_attribute a
                                   WHERE c.relname = ?
                                     AND a.attnum > 0 AND a.attrelid = c.oid
                                  ORDER BY a.attnum;");
  $get_struct->execute($tablename);

  $get_default = $master->prepare("SELECT 'DEFAULT ' || adsrc
                                     FROM pg_class pc, pg_attrdef pa
                                    WHERE relname = ?
                                      AND relfilenode = adrelid
                                      AND adnum = ?;");

  $check_slave_seq = $master->prepare("SELECT * FROM pg_class WHERE relname = ?;");

  if($get_struct->rows > 0) {
    $table_struct = sprintf"CREATE TABLE %s (\n", $tablename;

    while (($attname, $atttype, $attnotnull, $atthasdef, $attnum) = $get_struct->fetchrow_array()) {
      if($attnum != 1) {
        $table_struct .= sprint ",\n";
      }
      if($attnotnull) {
        $notnull = "NOT NULL";
      } else {
        $notnull = "";
      }
      if($atthasdef) {
        $get_default->execute($tablename, $attnum);
        ( $default  ) = $get_default->fetchrow_array();
        if($default =~ /nextval/) {
          ( $junk, $sequence ) = split(/"/, $default);
          $check_slave_seq->execute($sequence);
          if($check_slave_seq->rows == 0) {
            $slave->do("CREATE SEQUENCE $sequence;");
          }
          $trig_seq{$sequence} = 1;
        }
      } else {
        $default = "";
      }
      $table_struct .= sprintf"\t%-16s %s %s %s", $attname, $atttype, $notnull, $default;
    }
    $table_struct .= sprintf"\n);\n";

    $slave->do($table_struct) || die $slave->errstr;

    open(DATA, "pg_dump -t ${table} -da -U ${masteruser} ${masterdb} | grep INSERT |");
    while(<DATA>) {
      $slave->do($_) || die $slave->errstr;
    }

    $stable = $slave->prepare("SELECT pgc.oid, pga.attnum
                                 FROM pg_class pgc, pg_attribute pga
                                WHERE pgc.relname = ?
                                  AND pgc.oid = pga.attrelid
                                  AND pga.attname = ?");
    $stable->execute($table, 'ers_uniq');
    ( $s_oid, $s_attnum ) = $stable->fetchrow_array();
    $stable->finish();

    $slave->do("INSERT INTO _RSERV_SLAVE_TABLES_ (tname, cname, reloid, key) values ('${table}', '_ers_uniq', ${s_oid}, ${s_attnum})");

    foreach $seq_name ( keys %trig_seq ) {
      $stable->execute($seq_name, 'last_value');
      ( $s_oid, $s_attnum ) = $stable->fetchrow_array();
      $slave->do("INSERT INTO _RSERV_SLAVE_TABLES_ (tname, cname, reloid, key) values ('${table}', 'last_value', ${s_oid}, ${s_attnum})");
    }
  }
  $get_default->finish();
}

#
# Parallel snapshot generation
#
sub PrepareSnapshotPTE
{
	my ($conn, $outf, $server, $cinfo, @oconn) = @_;
	my @row;
	my $childs;

	select(STDERR); $| = 1;
	select(STDOUT);

	my $result = $conn->exec("BEGIN");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	$result = $conn->exec("set transaction isolation level serializable");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("select _pte_get_snapshot_()");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	@row = $result->fetchrow;
	$Snapshot = $row[0];

	if (InitPrepare($conn, $server) != 0)
	{
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$childs = keys %Mtables;
	if ($childs <= 0)
	{
		print STDERR "No tables\n" unless ($quiet);
		$conn->exec("ROLLBACK");
		return(0);
	}

	my $havedeal = 0;

	#
	# Start one child for each table to replicate
	#
	printf STDERR "Starting $childs childs\n" if $debug;
	push @oconn, $conn;
	$childs = RunChildsForPrepare($childs, $cinfo, @oconn);
	if ($childs <= 0)
	{
		$conn->exec("ROLLBACK");
		return(-1);
	}

	# Wait for childs to make their works
	while ($childs > 0)
	{
		my $pid = waitpid(-1, 0);
		next unless exists $Childs{$pid};
		delete $Childs{$pid};
		if ($?)		# errors
		{
			printf $outf "-- ERROR\n" if $havedeal;
			kill SIGTERM, keys %Childs;
			print STDERR "Child $pid failed\n" unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}
		($childs)--;

		# Get result of this child work
		unless (open INP, "< _rserv_pte_$pid")
		{
			printf STDERR "No data from child $pid\n" if $debug;
			next;
		}
		if (! $havedeal)
		{
			my $syncid = GetSYNCID($conn, $outf);
			if ($syncid < 0)
			{
				kill SIGTERM, keys %Childs;
				$conn->exec("ROLLBACK");
				return($syncid);
			}
			$havedeal = 1;
		}
		my $count = 0;
		while (<INP>)
		{
			print $outf $_;
			($count)++;
		}
		close(INP);
		unlink "_rserv_pte_$pid";
		printf STDERR "Child $pid produced $count data lines\n" if $debug;
	}

	unless ($havedeal)
	{
		$conn->exec("ROLLBACK");
		printf $outf "-- OK\n";
		return(0);
	}

	# Remember this snapshot info
	$result = $conn->exec("select _rserv_sync_($server)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		printf $outf "-- ERROR\n";
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("COMMIT");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		printf $outf "-- ERROR\n";
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	printf $outf "-- OK\n";

	return(1);

}

sub RunChildsForPrepare
{
	my ($childs, $cinfo, @oconn) = @_;
	my @tables = keys %Mtables;
	my $started = 0;

	while ($childs > 0)
	{
		return($started) unless $#tables >= 0;
		my $taboid = pop @tables;
		my $pid = fork();
		if ($pid == 0)		# Child
		{
			#
			# Have to close parent' connection(s) socket(s)
			# to do not close parent' connection(s) on exit
			#
			my $conn;
			foreach $conn (@oconn)
			{
				my $sock = $conn->socket;
				unless (open OLD, "<&=$sock")
				{
					print STDERR "Child $$: failed to close parent connection ($!)\n" unless ($quiet);
					exit(1);
				}
				close OLD;
			}

			ChildPrepare($cinfo, $taboid);
			exit(0);
		}
		elsif ($pid > 0)	# Parent
		{
			($childs)--;
			($started)++;
			$Childs{$pid} = $taboid;
			delete $Mtables{$taboid};
			next;
		}
		else
		{
			print STDERR "Cannot start child: $!\n" unless ($quiet);
			return(-1);
		}
	}

	return($started);
}

sub ChildPrepareError
{
	my $pid = $$;
	unlink "_rserv_pte_${pid}";
	unlink "_rserv_pte_${pid}_tmp";
	exit(1);
}

sub ChildPrepare
{
	my ($cinfo, $taboid) = @_;
	my $pid = $$;
	my $outf = new IO::File;

	$SIG{TERM} = \&ChildPrepareError;

	open $outf, ">_rserv_pte_${pid}_tmp" or ChildPrepareError();

	my $conn = Pg::connectdb($cinfo);

	# Optimizer hints
	$conn->exec("set enable_seqscan to off");
	$conn->exec("set enable_indexscan to on");

	my $result = $conn->exec("BEGIN");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		ChildPrepareError();
	}
	$result = $conn->exec("set transaction isolation level serializable");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		ChildPrepareError();
	}

	$result = $conn->exec("select _pte_set_snapshot_('$Snapshot')");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		ChildPrepareError();
	}

	my $gensyncid = 0;	# do NOT generate syncid in child!
	my $gotdata = PrepareTable($conn, $outf, $gensyncid, $taboid);
	if ($gotdata < 0)
	{
		$conn->exec("ROLLBACK");
		ChildPrepareError();
	}

	close $outf;
	$conn->exec("ROLLBACK");

	if ($gotdata)
	{
		if (rename("_rserv_pte_${pid}_tmp", "_rserv_pte_${pid}") == 0)
		{
			print STDERR "Child $$: rename failed ($!)\n" unless ($quiet);
			ChildPrepareError();
		}
	}
	else
	{
		unlink "_rserv_pte_${pid}_tmp";
	}

	exit(0);
}

#
# Non-parallel snapshot generation
#
sub PrepareSnapshot
{
	my ($conn, $outf, $server) = @_; # (@_[0], @_[1], @_[2]);

	# Optimizer hints
	$conn->exec("set enable_seqscan to off");
	$conn->exec("set enable_indexscan to on");

	my $result = $conn->exec("BEGIN");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	$result = $conn->exec("set transaction isolation level serializable");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	if (InitPrepare($conn, $server) != 0)
	{
		$conn->exec("ROLLBACK");
		return(-1);
	}

	my $havedeal = 0;

	foreach $taboid (keys %Mtables)
	{
		# Generate syncid only ONCE
		my $gensyncid = ($havedeal == 0) ? 1 : 0;
		my $gotdata = PrepareTable($conn, $outf, $gensyncid, $taboid);
		if ($gotdata < 0)
		{
			$conn->exec("ROLLBACK");
			return(-1);
		}
		$havedeal = 1 if $gotdata;
	}

	unless ($havedeal)
	{
		$conn->exec("ROLLBACK");
		printf $outf "-- OK\n";
		return(0);
	}

	# Remember this snapshot info
	$result = $conn->exec("select _rserv_sync_($server)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		printf $outf "-- ERROR\n";
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("COMMIT");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		printf $outf "-- ERROR\n";
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	printf $outf "-- OK\n";

	return(1);

}

sub InitPrepare
{
	my ($conn, $server) = @_;
	my @row;
	my $sql;

	%Mtables = ();
	@UStable = ();
	%Childs = ();
	$Sinfo = '';
	$Snapshot = '';
	$ActiveLogTable = 0;
	$UseOldLogTable = 1;
	@UStable = ();

	#
	# And the Magic begins -:)
	#
	my $result = $conn->exec("select _rserv_lock_altid_(0)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	$result = $conn->exec("select last_value::int4 FROM _rserv_active_log_id_");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}
	@row = $result->fetchrow;
	$ActiveLogTable = $row[0];

	$result = $conn->exec("select last_value::int4 FROM _rserv_old_log_status_");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}
	@row = $result->fetchrow;
	$UseOldLogTable = ($row[0] == 2) ? 1 : 0;

	if ($UseOldLogTable)
	{
		$sql = sprintf "LOCK TABLE _RSERV_LOG_%d_ IN ROW SHARE MODE",
			($ActiveLogTable == 1) ? 2 : 1;
		$result = $conn->exec($sql);
		if ($result->resultStatus ne PGRES_COMMAND_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			return(-1);
		}
	}

	$result = $conn->exec("select _rserv_unlock_altid_(0)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	#
	# End of Magic -:)
	#

	# MAP oid --> tabname, keyname, keytype
	$result = $conn->exec(
		"select pgc.oid, pgc.relname, pga.attname, pgt.typname " . 
		"from _RSERV_TABLES_ rt, pg_class pgc, " . 
		"pg_attribute pga, pg_type pgt " . 
		"where pgc.oid = rt.reloid and pga.attrelid = rt.reloid " .
		"and pga.attnum = rt.key and pgt.oid = pga.atttypid");

	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	while (@row = $result->fetchrow)
	{
	#	printf "$row[0], $row[1], $row[2], $row[3]\n";
		push @{$Mtables{$row[0]}}, $row[1], $row[2], $row[3];
	}

	# Read last succeeded sync
	$sql = "select syncid, synctime, minid, maxid, active from _RSERV_SYNC_ " . 
		"where server = $server and syncid = (select max(syncid) from " . 
		"_RSERV_SYNC_ where server = $server and status > 0)";

	printf STDERR "$sql\n" if $debug;

	$result = $conn->exec($sql);
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	@row = $result->fetchrow;
	if ($row[3] ne '')	# sync info
	{
		$Sinfo = "and l.logid >= $row[2] and (l.logid >= $row[3]";
		$Sinfo .= " or l.logid in ($row[4])" if $row[4] ne '';
		$Sinfo .= ")";
	}

	# Read info about snapshots generated after last succeeded sync
	$sql = "select syncid, synctime, minid, maxid, active " . 
			"from _RSERV_SYNC_ where server = $server ";
	$sql .= "and syncid > $row[0] " if $row[0] ne '';
	$sql .= "order by minid";

	printf STDERR "$sql\n" if $debug;

	$result = $conn->exec($sql);
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	while (@row = $result->fetchrow)
	{
		push @UStable, $row[0], $row[2], $row[3], {};
		my $aref = $UStable[$#UStable - 1];
		my @active = split(/[ ]+,[ ]+/, $row[4]);
		foreach (@active)
		{
			$aref{$_} = 1;
		}
	}

	return(0);
}

sub PrepareTable
{
	my ($conn, $outf, $gensyncid, $taboid) = @_;
	my ($tabname, $tabkey, $keytype) = @{$Mtables{$taboid}};
	my ($CTable, $OTable) = (0, 0);

	if ($ActiveLogTable == 1)
	{
		$CTable = "_RSERV_LOG_1_";
		$OTable = "_RSERV_LOG_2_";
	}
	elsif ($ActiveLogTable == 2)
	{
		$CTable = "_RSERV_LOG_2_";
		$OTable = "_RSERV_LOG_1_";
	}

	my $sql = 
		"declare c cursor for " . 
		"select l.logid, l.deleted, l.key, _$tabname.* " . 
		"from $CTable l LEFT JOIN $tabname _$tabname " . 
		"ON l.key::$keytype = _$tabname.${tabkey} " . 
		"WHERE l.reloid = $taboid $Sinfo";

	printf STDERR "$sql\n" if $debug;

	my $result = $conn->exec($sql);
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	my $gotdata = ReadData($conn, $outf, $gensyncid, $tabname);
	if ($gotdata < 0)
	{
		return($gotdata);
	}

	if ($UseOldLogTable)
	{
		$sql = 
			"declare c cursor for " . 
			"select l.logid, l.deleted, l.key, _$tabname.* " . 
			"from $OTable l LEFT JOIN $tabname _$tabname " . 
			"ON l.key::$keytype = _$tabname.${tabkey} " . 
			"WHERE l.reloid = $taboid $Sinfo " .
			"and not exists (" . 
			"select lc.logid from $CTable lc " . 
			"where lc.reloid = $taboid and lc.key = l.key" . 
			")";

		printf STDERR "$sql\n" if $debug;

		$result = $conn->exec($sql);
		if ($result->resultStatus ne PGRES_COMMAND_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			return(-1);
		}

		$gensyncid = 0 if $gotdata > 0;
		$result = ReadData($conn, $outf, $gensyncid, $tabname);
		if ($result < 0)
		{
			return($result);
		}
		$gotdata += $result;
	}

	return($gotdata);
}

sub ReadData
{
	my ($conn, $outf, $gensyncid, $tabname) = @_;
	my ($syncid, $result, $ref, $i);

	my @ops = (
		[0, '', "-- DELETE $tabname\n"],
		[0, '', "-- UPDATE $tabname\n"],
		[0, '', "-- INSERT $tabname\n"]
	);
	my $gotdata = 0;
	for ( ; ; )
	{
		$result = $conn->exec('fetch 1000 in c');
		if ($result->resultStatus ne PGRES_TUPLES_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			return(-1);
		}

		last if $result->ntuples <= 0;

		my @row;
		while (@row = $result->fetchrow)
		{
			#
			# Find to what SyncID this change belongs
			#
			$syncid = 'T';							# this snapshot
			for ($i = 0; $i < $#UStable; $i += 4)
			{
				if ($row[0] < $UStable[$i + 1])		# LT than minid?
				{
					$syncid = $UStable[$i];			# belongs to this
					last;
				}
				if ($row[0] >= $UStable[$i + 2])	# GE than maxid?
				{
					next;							# belongs to the next
				}
				my $aref = $UStable[$i + 3];		# "active" hash
				if (exists $aref{$row[0]})			# was active?
				{
					next;							# belongs to the next
				}
				# (xid > minid) AND (xid < maxid) AND was committed
				$syncid = $UStable[$i];
				last;
			}

			if ($row[1] > 0)		# deleted
			{
				$ref = $ops[0];
				$$ref[1] .= "$syncid	";
				$$ref[1] .= OutputValue($row[2]);
				$$ref[1] .= "\n";
				($$ref[0])++;
			}
			else
			{
				if ($row[1] == 0)	# update
				{
					$ref = $ops[1];
				}
				else				# insert
				{
					$ref = $ops[2];
				}

				$$ref[1] .= $syncid;

				$i = ($tabkey eq 'oid') ? 2 : 3;
				for ( ; $i <= $#row; $i++)
				{
					$$ref[1] .= "	";
					$$ref[1] .= OutputValue($row[$i]);
				}
				$$ref[1] .= "\n";
				($$ref[0])++;
			}
		}

		# Output buffers with > 1000 records
		for ($i = 0; $i < 3; $i++)
		{
			$ref = $ops[$i];
			next if $$ref[0] < 1000;

			$gotdata = 1;
			if ($gensyncid)		# if requested to generate syncid...
			{
				$syncid = GetSYNCID($conn, $outf);
				return($syncid) if $syncid < 0;
				$gensyncid = 0;
			}
			print $outf $$ref[2];
			print $outf $$ref[1];
			print $outf "\\.\n";
			$$ref[1] = '';
			$$ref[0] = 0;
		}
	}

	# Output unempty buffers
	for ($i = 0; $i < 3; $i++)
	{
		$ref = $ops[$i];
		next if $$ref[0] <= 0;
		$gotdata = 1;
		if ($gensyncid)		# if requested to generate syncid...
		{
			$syncid = GetSYNCID($conn, $outf);
			return($syncid) if $syncid < 0;
			$gensyncid = 0;
		}
		print $outf $$ref[2];
		print $outf $$ref[1];
		print $outf "\\.\n";
	}

	$result = $conn->exec('close c');
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	return($gotdata);
}

sub OutputValue
{
	my ($val) = @_; # @_[0];

	return("\\N") unless defined $val;

	$val =~ s/\\/\\\\/g;
	$val =~ s/	/\\011/g;
	$val =~ s/\n/\\012/g;
	$val =~ s/\'/\\047/g;

	return($val);
}

# Get syncid for new snapshot
sub GetSYNCID
{
	my ($conn, $outf) = @_; # (@_[0], @_[1]);

	my $result = $conn->exec("select nextval('_rserv_sync_seq_')");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	my @row = $result->fetchrow;

	printf $outf "-- SYNCID $row[0]\n";
	return($row[0]);
}


sub CleanLog
{
	my ($conn, $tout) = @_;
	my @row;
	my $tm;

	$tout = 60 unless $tout > 0;

	# Optimizer hints
	$conn->exec("set enable_seqscan to off");
	$conn->exec("set enable_indexscan to on");

	my $result = $conn->exec("BEGIN");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("select _rserv_lock_altid_(1)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("select last_value::int4 FROM _rserv_active_log_id_");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	@row = $result->fetchrow;
	my $alt = $row[0];
	if ($alt != 1 && $alt != 2)
	{
		print STDERR "Invalid ActiveLogTableId $alt\n" unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	my $olt = ($alt == 1) ? 2 : 1;

	$result = $conn->exec("select last_value::int4 FROM _rserv_old_log_status_");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	@row = $result->fetchrow;
	my $useolt = $row[0];
	if ($useolt != 0 && $useolt != 1 && $useolt != 2)
	{
		print STDERR "Invalid UseOldLogTable $useolt\n" unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	if ($useolt == 0)	# Old log is clean - switch logs
	{
		# First - flag to use "new old" log
		$result = $conn->exec("select setval('_rserv_old_log_status_', 2)");
		if ($result->resultStatus ne PGRES_TUPLES_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}
		# Now - change active log
		$result = $conn->exec("select setval('_rserv_active_log_id_', $olt)");
		if ($result->resultStatus ne PGRES_TUPLES_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}
		$alt = $olt;				# new active log
		$olt = ($alt == 1) ? 2 : 1;
		printf STDERR "%s - CleanLog: log $alt is active now\n",
			$tm = localtime unless ($quiet);
	}
	#
	# Else - do not switch logs and cleanup old log
	#

	$result = $conn->exec("select _rserv_unlock_altid_(1)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	#
	# Make sure that no triggers use old log table.
	# Note that triggers always place ROW EXCLUSIVE lock
	# on active table while holding share "altid" lock,
	# so *we wait for triggers* here and do not block
	# any triggers while waiting. "Prepare" uses ROW SHARE
	# lock on old log - so no conflict at this point.
	#
	$result = $conn->exec("LOCK TABLE _RSERV_LOG_${olt}_ IN SHARE MODE");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	#
	# Got EXCLUSIVE lock ==> all ROW EXCLUSIVE LOCKs from triggers
	# released, ie triggers now use new active table only
	#

	$result = $conn->exec("COMMIT");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	#
	# If old log has status 1 then just truncate it and return.
	#
	if ($useolt == 1)
	{
		$result = $conn->exec("TRUNCATE TABLE _RSERV_LOG_${olt}_");
		if ($result->resultStatus ne PGRES_COMMAND_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}

		$result = $conn->exec("BEGIN");
		if ($result->resultStatus ne PGRES_COMMAND_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}
		$result = $conn->exec("select _rserv_lock_altid_(1)");
		if ($result->resultStatus ne PGRES_TUPLES_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}
		$result = $conn->exec("select setval('_rserv_old_log_status_', 0)");
		if ($result->resultStatus ne PGRES_TUPLES_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}
		$result = $conn->exec("select _rserv_unlock_altid_(1)");
		if ($result->resultStatus ne PGRES_TUPLES_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}
		$result = $conn->exec("COMMIT");
		if ($result->resultStatus ne PGRES_COMMAND_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}
		printf STDERR "%s - CleanLog: log $olt truncated; active log ($alt) is not changed\n",
			$tm = localtime unless ($quiet);
		return(0);
	}

	#
	# Now wait as long as there are unreplicated records in old log
	#

	for ( ; ; )
	{
		my %tables = ();
		my %sinfo = ();

		$result = $conn->exec("BEGIN");
		if ($result->resultStatus ne PGRES_COMMAND_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}
		$result = $conn->exec("set transaction isolation level serializable");
		if ($result->resultStatus ne PGRES_COMMAND_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}
		$result = $conn->exec(
			"select pgc.oid " . 
			"from _RSERV_TABLES_ rt, pg_class pgc " . 
			"where pgc.oid = rt.reloid");
		if ($result->resultStatus ne PGRES_TUPLES_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}

		while (@row = $result->fetchrow)
		{
			$tables{$row[0]} = $row[0];
		}

		my $sql = "select server from _rserv_servers_";
		$result = $conn->exec($sql);
		if ($result->resultStatus ne PGRES_TUPLES_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}

		while (@row = $result->fetchrow)
		{
			$sinfo{$row[0]} = '';
		}

		$sql = "select s.server, s.minid, s.maxid, s.active " . 
			"from _RSERV_SYNC_ s " . 
			"where s.syncid = (select max(s2.syncid) from " . 
			"_RSERV_SYNC_ s2 where s2.server = s.server and s2.status > 0)";
		printf STDERR "$sql\n" if $debug;
		$result = $conn->exec($sql);
		if ($result->resultStatus ne PGRES_TUPLES_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-1);
		}

		while (@row = $result->fetchrow)
		{
			my $s = "and l.logid >= $row[1] and (l.logid >= $row[2]";
			$s .= " or l.logid in ($row[3])" if $row[3] ne '';
			$s .= ")";
			$sinfo{$row[0]} = $s;
		}

		$sql = keys %sinfo;
		$sinfo{0} = '' unless $sql > 0;

		#
		# For each server and for each table
		#
		my ($server, $taboid);
		my $found = 0;
		foreach $server (keys %sinfo)
		{
			foreach $taboid (keys %tables)
			{
				$sql = "select l.logid from _RSERV_LOG_${olt}_ l " . 
						"WHERE l.reloid = $taboid ";
				$sql .= $sinfo{$server};
				$sql .= " LIMIT 1";
				printf STDERR "$sql\n" if $debug;

				$result = $conn->exec($sql);
				if ($result->resultStatus ne PGRES_TUPLES_OK)
				{
					print STDERR $conn->errorMessage unless ($quiet);
					$conn->exec("ROLLBACK");
					return(-1);
				}
				if ($result->ntuples > 0)
				{
					$found = 1;
					last;
				}
			}
			last if $found > 0;
		}
		$conn->exec("ROLLBACK");

		last if $found == 0;

		printf STDERR "%s - CleanLog: log $olt is not replicated yet - wait $tout sec\n",
			$tm = localtime unless ($quiet);
		sleep($tout);
	}

	#
	# Ok - all records from _RSERV_LOG_${olt}_ are replicated.
	# Now advise "Prepare" to ignore old log table in queries
	# but mark old log as clean only after truncate.
	#

	$result = $conn->exec("BEGIN");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("select _rserv_lock_altid_(1)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("select setval('_rserv_old_log_status_', 1)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("select _rserv_unlock_altid_(1)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("COMMIT");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	#
	# Now we can just truncate old log table. Truncate will
	# grab AccessExclusive lock and so will wait for "Prepare"
	# if "Prepare" decided to read from old log table before
	# we changed old log status. This is because of "Prepare"
	# grab SHARE ROW lock on old log table while holding
	# "altid" lock and so *we wait for "Prepare"*, which is not
	# bad and *do not block "Prepare"*, which is very good.
	#

	$result = $conn->exec("TRUNCATE TABLE _RSERV_LOG_${olt}_");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("BEGIN");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("select _rserv_lock_altid_(1)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("select setval('_rserv_old_log_status_', 0)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("select _rserv_unlock_altid_(1)");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("COMMIT");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	printf STDERR "%s - CleanLog: log $olt is clean now\n",
		$tm = localtime unless ($quiet);

	return(1);
}

sub ApplySnapshot
{
	my ($conn, $inpf) = @_; # (@_[0], @_[1]);

	# Optimizer hints
	$conn->exec("set enable_seqscan to off");
	$conn->exec("set enable_indexscan to on");

	my $result = $conn->exec("BEGIN");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("SET CONSTRAINTS ALL DEFERRED");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	# MAP name --> oid, keyname, keynum
	my $sql = "select pgc.oid, pgc.relname, pga.attname, rt.key" . 
		" from _RSERV_SLAVE_TABLES_ rt, pg_class pgc, pg_attribute pga" . 
			" where pgc.oid = rt.reloid and pga.attrelid = rt.reloid" .
				" and pga.attnum = rt.key";
	$result = $conn->exec($sql);
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	while (@row = $result->fetchrow)
	{
	#	printf "	%s	%s\n", $row[1], $row[0];
		push @{$Stables{$row[1]}}, $row[0], $row[2], $row[3];
	}

	my $ok = 0;
	my $syncid = '';
	my $lastsid = '';
	while(<$inpf>)
	{
		$_ =~ s/\n//;
		my ($cmt, $cmd, $prm) = split (/[ 	]+/, $_, 3);
		if ($cmt ne '--')
		{
			printf STDERR "Invalid format\n" unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-2);
		}
		if ($cmd eq 'DELETE')
		{
			if ($syncid eq '')
			{
				printf STDERR "Sync ID unspecified\n" unless ($quiet);
				$conn->exec("ROLLBACK");
				return(-2);
			}
			$result = DoDelete($conn, $inpf, $prm, $lastsid);
			if ($result)
			{
				$conn->exec("ROLLBACK");
				return($result);
			}
		}
		elsif ($cmd eq 'UPDATE')
		{
			if ($syncid eq '')
			{
				printf STDERR "Sync ID unspecified\n" unless ($quiet);
				$conn->exec("ROLLBACK");
				return(-2);
			}
			$result = DoUpdate($conn, $inpf, $prm, $lastsid);
			if ($result)
			{
				$conn->exec("ROLLBACK");
				return($result);
			}
		}
		elsif ($cmd eq 'INSERT')
		{
			if ($syncid eq '')
			{
				printf STDERR "Sync ID unspecified\n" unless ($quiet);
				$conn->exec("ROLLBACK");
				return(-2);
			}
			$result = DoInsert($conn, $inpf, $prm, $lastsid);
			if ($result)
			{
				$conn->exec("ROLLBACK");
				return($result);
			}
		}
		elsif ($cmd eq 'SYNCID')
		{
			if ($syncid ne '')
			{
				printf STDERR "Second Sync ID ?!\n" unless ($quiet);
				$conn->exec("ROLLBACK");
				return(-2);
			}
			if ($prm !~ /^\d+$/)
			{
				printf STDERR "Invalid Sync ID $prm\n" unless ($quiet);
				$conn->exec("ROLLBACK");
				return(-2);
			}
			$syncid = $prm;

			printf STDERR "Sync ID $syncid\n" unless ($quiet);

			$result = $conn->exec("select syncid, synctime from " . 
								  "_RSERV_SLAVE_SYNC_ where syncid = " . 
								  "(select max(syncid) from _RSERV_SLAVE_SYNC_)");
			if ($result->resultStatus ne PGRES_TUPLES_OK)
			{
				print STDERR $conn->errorMessage unless ($quiet);
				$conn->exec("ROLLBACK");
				return(-1);
			}
			my @row = $result->fetchrow;
			if (! defined $row[0])
			{
				$lastsid = 'A';			# get data from ANY snapshot

				$result = $conn->exec("insert into" .
									  " _RSERV_SLAVE_SYNC_(syncid, synctime) values ($syncid, now())");
			}
			elsif ($row[0] >= $prm)
			{
				printf STDERR "Sync-ed to ID $row[0] ($row[1])\n" unless ($quiet);
				$conn->exec("ROLLBACK");
				return(0);
			}
			else
			{
				$lastsid = $row[0];

				$result = $conn->exec("update _RSERV_SLAVE_SYNC_" .
									  " set syncid = $syncid, synctime = now()");
			}
			if ($result->resultStatus ne PGRES_COMMAND_OK)
			{
				print STDERR $conn->errorMessage unless ($quiet);
				$conn->exec("ROLLBACK");
				return(-1);
			}
		}
		elsif ($cmd eq 'OK')
		{
			$ok = 1;
			last;
		}
		elsif ($cmd eq 'ERROR')
		{
			printf STDERR "ERROR signaled\n" unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-2);
		}
		else
		{
			printf STDERR "Unknown command $cmd\n" unless ($quiet);
			$conn->exec("ROLLBACK");
			return(-2);
		}
	}

	if (! $ok)
	{
		printf STDERR "No OK flag in input\n" unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-2);
	}

	$result = $conn->exec("COMMIT");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	return(1);
}

sub DoDelete
{
	my ($conn, $inpf, $tabname, $lastsid) = @_;
	my $rowsid;
	my $ok = 0;

	while(<$inpf>)
	{
		if ($_ !~ /\n$/)
		{
			printf STDERR "Invalid format\n" unless ($quiet);
			return(-2);
		}
		my $key = $_;
		$key =~ s/\n//;
		if ($key eq '\.')
		{
			$ok = 1;
			last;
		}

		($rowsid, $key) = split(/	/, $key, 2);
		next if $lastsid ne 'A' && $rowsid ne 'T' && $lastsid >= $rowsid;

		my $sql = "delete from $tabname where $Stables{$tabname}->[1] = '$key'";

		printf STDERR "$sql\n" if $debug;

		my $result = $conn->exec($sql);
		if ($result->resultStatus ne PGRES_COMMAND_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			return(-1);
		}
	}

	if (! $ok)
	{
		printf STDERR "No end of input in DELETE section\n" unless ($quiet);
		return(-2);
	}

	return(0);
}

sub DoUpdate
{
	my ($conn, $inpf, $tabname, $lastsid) = @_;
	my $oidkey = ($Stables{$tabname}->[2] < 0) ? 1 : 0;
	my $rowsid;

	my @CopyBuf = ();
	my $CBufLen = 0;
	my $CBufMax = 16 * 1024 * 1024;	# max size of buf for copy

	my $sql = "select attnum, attname from pg_attribute" .
		" where attrelid = $Stables{$tabname}->[0] and attnum > 0";

	my $result = $conn->exec($sql);
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	my @anames = ();
	while (@row = $result->fetchrow)
	{
		$anames[$row[0]] = $row[1];
	}

	my $istring;
	my $ok = 0;
	while(<$inpf>)
	{
		if ($_ !~ /\n$/)
		{
			printf STDERR "Invalid format\n" unless ($quiet);
			return(-2);
		}
		$istring = $_;
		$istring =~ s/\n//;
		if ($istring eq '\.')
		{
			$ok = 1;
			last;
		}

		($rowsid, $istring) = split(/	/, $istring, 2);
		next if $lastsid ne 'A' && $rowsid ne 'T' && $lastsid >= $rowsid;

		my @vals = split(/	/, $istring);
		if ($oidkey)
		{
			if ($vals[0] !~ /^\d+$/ || $vals[0] <= 0)
			{
				printf STDERR "Invalid OID\n" unless ($quiet);
				return(-2);
			}
			$oidkey = $vals[0];
		}
		else
		{
			unshift @vals, '';
		}

		$sql = "update $tabname set ";
		my $ocnt = 0;
		for (my $i = 1; $i <= $#anames; $i++)
		{
			if ($vals[$i] eq '\N')
			{
				if ($i == $Stables{$tabname}->[2])
				{
					printf STDERR "NULL key\n" unless ($quiet);
					return(-2);
				}
				$vals[$i] = 'null';
			}
			else
			{
				$vals[$i] = "'" . $vals[$i] . "'";
				next if $i == $Stables{$tabname}->[2];
			}
			$ocnt++;
			$sql .= ', ' if $ocnt > 1;
			$sql .= "$anames[$i] = $vals[$i]";
		}
		if ($oidkey)
		{
			$sql .= " where $Stables{$tabname}->[1] = $oidkey";
		}
		else
		{
			$sql .= " where $Stables{$tabname}->[1] = $vals[$Stables{$tabname}->[2]]";
		}

		printf STDERR "$sql\n" if $debug;

		$result = $conn->exec($sql);
		if ($result->resultStatus ne PGRES_COMMAND_OK)
		{
			print STDERR $conn->errorMessage unless ($quiet);
			return(-1);
		}
		next if $result->cmdTuples == 1;	# updated

		if ($result->cmdTuples > 1)
		{
			printf STDERR "Duplicate keys\n" unless ($quiet);
			return(-2);
		}

		# no key - copy
		push @CopyBuf, "$istring\n";
		$CBufLen += length($istring);

		if ($CBufLen >= $CBufMax)
		{
			$result = DoCopy($conn, $tabname, $oidkey, \@CopyBuf);
			return($result) if $result;
			@CopyBuf = ();
			$CBufLen = 0;
		}
	}

	if (! $ok)
	{
		printf STDERR "No end of input in UPDATE section\n" unless ($quiet);
		return(-2);
	}

	if ($CBufLen)
	{
		$result = DoCopy($conn, $tabname, $oidkey, \@CopyBuf);
		return($result) if $result;
	}

	return(0);
}

sub DoInsert
{
	my ($conn, $inpf, $tabname, $lastsid) = @_;
	my $oidkey = ($Stables{$tabname}->[2] < 0) ? 1 : 0;
	my $rowsid;

	my @CopyBuf = ();
	my $CBufLen = 0;
	my $CBufMax = 16 * 1024 * 1024;	# max size of buf for copy

	my $istring;
	my $ok = 0;
	while(<$inpf>)
	{
		if ($_ !~ /\n$/)
		{
			printf STDERR "Invalid format\n" unless ($quiet);
			return(-2);
		}
		$istring = $_;
		if ($istring eq "\\.\n")
		{
			$ok = 1;
			last;
		}

		($rowsid, $istring) = split(/	/, $istring, 2);
		next if $lastsid ne 'A' && $rowsid ne 'T' && $lastsid >= $rowsid;

		push @CopyBuf, $istring;
		$CBufLen += length($istring);

		if ($CBufLen >= $CBufMax)
		{
			$result = DoCopy($conn, $tabname, $oidkey, \@CopyBuf);
			return($result) if $result;
			@CopyBuf = ();
			$CBufLen = 0;
		}
	}

	if (! $ok)
	{
		printf STDERR "No end of input in INSERT section\n" unless ($quiet);
		return(-2);
	}

	if ($CBufLen)
	{
		$result = DoCopy($conn, $tabname, $oidkey, \@CopyBuf);
		return($result) if $result;
	}

	return(0);
}


sub DoCopy
{
	my ($conn, $tabname, $withoids, $CBuf) = @_;

	my $sql = "COPY $tabname " . (($withoids) ? "WITH OIDS " : '') . 
"FROM STDIN";
	my $result = $conn->exec($sql);
	if ($result->resultStatus ne PGRES_COPY_IN)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	foreach $str (@{$CBuf})
	{
		$conn->putline($str);
	}

	$conn->putline("\\.\n");

	if ($conn->endcopy)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}

	return(0);

}


#
# Returns last SyncID applied on Slave
#
sub GetSyncID
{
	my ($conn) = @_; # (@_[0]);
	
	my $result = $conn->exec("select max(syncid) from _RSERV_SLAVE_SYNC_");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		return(-1);
	}
	my @row = $result->fetchrow;
	return(undef) unless defined $row[0];	# null
	return($row[0]);
}

#
# Updates _RSERV_SYNC_ on Master with Slave SyncID
#
sub SyncSyncID
{
	my ($conn, $server, $syncid) = @_; # (@_[0], @_[1], @_[2]);
	
	my $result = $conn->exec("BEGIN");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("select synctime, status from _RSERV_SYNC_" .
						  " where server = $server and syncid = $syncid" .
						  " for update");
	if ($result->resultStatus ne PGRES_TUPLES_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	my @row = $result->fetchrow;
	if (! defined $row[0])
	{
		printf STDERR "No SyncID $syncid found for server $server\n" unless ($quiet);
		$conn->exec("ROLLBACK");
		return(0);
	}
	if ($row[1] > 0)
	{
		printf STDERR "SyncID $syncid for server $server already updated\n" unless ($quiet);
		$conn->exec("ROLLBACK");
		return(0);
	}
	$result = $conn->exec("update _RSERV_SYNC_" .
						  " set synctime = now(), status = 1" .
						  " where server = $server and syncid = $syncid");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}
	$result = $conn->exec("delete from _RSERV_SYNC_" .
						  " where server = $server and syncid < $syncid");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	$result = $conn->exec("COMMIT");
	if ($result->resultStatus ne PGRES_COMMAND_OK)
	{
		print STDERR $conn->errorMessage unless ($quiet);
		$conn->exec("ROLLBACK");
		return(-1);
	}

	return(1);
}

1;
