#!/usr/bin/perl
#------------------------------------------------------------------------------
#
# pg_restorebinary - Restore a PostgreSQL database using binary format. The
#                    database must have been exported using pg_dumpbinary.
#
# This program is open source, licensed under the PostgreSQL license.
# For license terms, see the LICENSE file.
#
# Author: Gilles Darold
# Copyright: (C) 2019-2025 LzLabs, GmbH
#------------------------------------------------------------------------------
use strict;

use Getopt::Long  qw(:config bundling no_ignore_case_always);
use POSIX qw(locale_h sys_wait_h _exit);
use Time::HiRes qw/usleep/;
use File::Spec;
use File::Temp qw/ tempfile /;

my $VERSION = '2.20';
my $PROGRAM = 'pg_restorebinary';

my $DBNAME = '';
my $DBUSER = '';
my $DBHOST = '';
my $DBPORT = 5432;

my @INC_SCHEMA = ();
my @EXC_SCHEMA = ();
my @INC_TABLE = ();
my @EXC_TABLE = ();
my $PGRESTORE = 'pg_restore';
my $PSQL      = 'psql';
my $PG_OPT    = '';
my $PG_FILTER = '';
my $JOBS      = 1;
my $VER       = 0;
my $INFO      = 0;
my $HELP      = 0;
my $DATAONLY  = 0;
my $TRUNCATE  = 0;
my @EXCLUDED_EXTENSION   = ();
my $IF_NOT_EXISTS_SCHEMA = 0;
my $OUTFILE     = '';
my $DUMP_CREATE = 0;
my $CREATEDB    = 0;
my $VERBOSE     = 0;

my $DISABLE_TRIGGERS = 0;

my $TMP_DIR   = File::Spec->tmpdir() || '/tmp';

my $interrupt = 0;
my $child_count = 0;
my %RUNNING_PIDS= ();

my $parent_pid = $$;

$ENV{PGAPPNAME} = 'pg_restorebinary';

####
# Method used to fork as many child as wanted,
# must be declared at top if the program.
####
sub spawn
{
	my $coderef = shift;

	unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE')
	{
		print "usage: spawn CODEREF";
		exit 0;
	}

	my $pid;
	if (!defined($pid = fork))
	{
		print STDERR "Error: cannot fork: $!\n";
		return;
	}
	elsif ($pid)
	{
		$RUNNING_PIDS{$pid} = $pid;
		return; # the parent
	}

	# the child -- go spawn
	$< = $>;
	$( = $); # suid progs only

	exit &$coderef();
}


# Child reports error
sub child_error
{
	my $sig = shift;

	$interrupt = 1;

	print STDERR "An error occurs from a child process, aborting...\n";
	if ($^O !~ /MSWin32|dos/i) {
		1 while wait != -1;
		$SIG{INT} = \&wait_child;
		$SIG{TERM} = \&wait_child;
		$SIG{USR1} = \&child_error;
	}
	_exit(1);
}

# With multiprocess we need to wait for all children
sub wait_child
{
	my $sig = shift;

	$interrupt = 1;

	print STDERR "Received terminating signal ($sig).\n";
	if ($^O !~ /MSWin32|dos/i) {
		1 while wait != -1;
		$SIG{INT} = \&wait_child;
		$SIG{TERM} = \&wait_child;
	}
	_exit(0);
}
$SIG{INT} = \&wait_child;
$SIG{TERM} = \&wait_child;
$SIG{USR1} = \&child_error;

$| = 1;

GetOptions(
	"a|data-only!"       => \$DATAONLY,
	"C|create!"          => \$CREATEDB,
	"d|database=s"       => \$DBNAME,
	"E|exclude-ext=s"    => \@EXCLUDED_EXTENSION,
	"f|file=s"           => \$OUTFILE,
	"h|host=s"           => \$DBHOST,
	"i|info!"            => \$INFO,
	"j|jobs=s"           => \$JOBS,
	"n|schema=s"         => \@INC_SCHEMA,
	"N|exclude-schema=s" => \@EXC_SCHEMA,
	"p|port=i"           => \$DBPORT,
	"t|table=s"          => \@INC_TABLE,
	"T|exclude-table=s"  => \@EXC_TABLE,
	"u|user=s"           => \$DBUSER,
	"v|version!"         => \$VER,
	"V|verbose!"         => \$VERBOSE,
	"help!"              => \$HELP,
	"disable-triggers!"  => \$DISABLE_TRIGGERS,
	"truncate!"          => \$TRUNCATE,
	"schema-exists!"     => \$IF_NOT_EXISTS_SCHEMA,
	"dump-create!"       => \$DUMP_CREATE,
);

if ($VER)
{
	print "$PROGRAM Version: v$VERSION.\n";
	exit 0;
}

&usage() if ($HELP);

if (!$DBNAME && !$INFO && !$OUTFILE && !$DUMP_CREATE)
{
	&usage("ERROR: you must specify a database to restore, see -d option\n", 1);
}

if ($DATAONLY && ($DUMP_CREATE || $CREATEDB))
{
	&usage("ERROR: you the -a data only option is not compatible with -C and --dump-create options\n", 1);
}

# Set pg_dump/psql option
if ($DBHOST)
{
	$PG_OPT .= " -h $DBHOST";
}
if ($DBPORT)
{
	$PG_OPT .= " -p $DBPORT";
}
if ($DBUSER)
{
	$PG_OPT .= " -U $DBUSER";
}

# Set schema/table filter options
&set_filter_option();

# Store input directory name
my $INDIR = $ARGV[0] || 0;

# Change directory to writable one for pg_restore
if (! -w ".")
{
	# Force absolute path for input directory
	if ($INDIR !~ /^\//) {
		die "ERROR: the input directory must be an absolute path.\n";
	}
        chdir $TMP_DIR;
}

# We need to retrieve the version of pg_restore about
# Check input directory
if (!-d $INDIR)
{
	&usage("ERROR: you must specify a dump directory as input.\n", 1);
}
else
{
	# check that this is a dump done with pg_dumpbinary
	if (!-e "$INDIR/pre-data.dmp" or !-e "$INDIR/pre-data.dmp")
	{
		die "ERROR: the input directory does not look to be generated by pg_dumpbinary.\n";
	}
}

# option -f - that is required with PG >= v12
my $pgrestore_version = eval_command("$PGRESTORE --version | awk '{print \$3}'");
chomp($pgrestore_version);

# Look at backup information
if ($INFO)
{
	print "Look at backup information\n" if ($VERBOSE);
	my @info = `$PGRESTORE -l $INDIR/pre-data.dmp | grep "^;"`;
	if ($?) {
		die "ERROR: pg_restore error to obtain dump information.\n";
	}
	chomp(@info);
	foreach my $l (@info)
	{
		print "$l\n" if ($l =~ /Archive|dbname:|Dumped from database/);
	}

	opendir(DIR, "$INDIR") || die "FATAL: can't opendir $INDIR: $!";
	my @list = grep { /^data-.*.bin(?:.gz)?$/} readdir(DIR);
        closedir(DIR);
	print ";     Number of tables to retore: ", scalar @list, "\n";
	exit 0;
}

# Show DATABASE related DDL statements
if ($DUMP_CREATE)
{
	print "Show DATABASE related DDL statements\n" if ($VERBOSE);
	`$PGRESTORE -C -l $INDIR/pre-data.dmp | grep "DATABASE" > /tmp/createdb_pg_dumpbinary-$$.lst`;
	if ($?) {
		die "ERROR: pg_restore error to obtain dump information.\n";
	}
	my @info = `$PGRESTORE -C $INDIR/pre-data.dmp -f - -L /tmp/createdb_pg_dumpbinary-$$.lst | grep -Ev "^(SET|\-\-|SELECT|.connect|\$)"`;
	chomp(@info);
	foreach my $l (@info) {
		print "$l\n";
	}

	exit 0;
}


# Restoring database pre-data section without attaching partition
# it will be done at import of post data section.
if (!$DATAONLY)
{
	print "Restoring database pre-data section without attaching partition.\n" if ($VERBOSE);
	my $output = "-d $DBNAME";
	my $creadb = '';
	$creadb = '-C' if ($CREATEDB);
	if ($OUTFILE) {
		$output = "-f - > $OUTFILE";
		print "Restoring pre data section into file $OUTFILE\n";
	} else {
		print "Restoring pre data section into database $DBNAME\n";
	}

	if (!$IF_NOT_EXISTS_SCHEMA && $#EXCLUDED_EXTENSION < 0)
	{
		`$PGRESTORE $creadb $PG_OPT $PG_FILTER $INDIR/pre-data.dmp $output`;
		if ($?) {
			die "ERROR: pg_restore error to restore pre-data section.\n";
		}
	}
	else
	{
		my $pgr_opt = '';
		$pgr_opt = '-f -' if ($pgrestore_version >= 12);
		my $schema_change = '';
		if ($IF_NOT_EXISTS_SCHEMA) {
			# Apply the IF NOT EXISTS clause
			$schema_change = "| sed 's/^CREATE SCHEMA /CREATE SCHEMA IF NOT EXISTS /'";
		}
		my $noextension = '';
		if ($#EXCLUDED_EXTENSION >= 0) {
			$noextension = '| grep -vE "(' . join('|', @EXCLUDED_EXTENSION) . ')"';
		}
		# we have to dump the list of objects to manipulate it
		`$PGRESTORE $creadb $PG_OPT $PG_FILTER $pgr_opt $INDIR/pre-data.dmp $schema_change $noextension > tmp_restore.sql`;
		if ($?) {
			die "ERROR: pg_restore error to restore pre-data section with filters.\n";
		}
		# Now restore the schema generated with previous filters
		`$PSQL $PG_OPT -v ON_ERROR_STOP=1 -f tmp_restore.sql $output`;
		if ($?) {
			die "ERROR: psql fail to restore filtered pre-data section.\n";
		}
	}
	unlink('tmp_pre-data.sql');
}

# Get the list of sequences
my %sequences = get_sequence_values();

# Retrieve the list of partition table
my %partitions = get_partition_list();

# Retrieve list of schema.tables to restore.
my %tbl_list = get_table_list();

print "Distributing all tables equally between all processes.\n" if ($JOBS and $VERBOSE);
my %distributed_table = ();
my $proc = 1;
foreach my $s (sort keys %tbl_list)
{
	foreach my $t (sort keys %{ $tbl_list{$s} })
	{
		my $sch = $s;
		my $tbl = $t;
		$sch =~ s/"//g;
		$tbl =~ s/"//g;
		next if (!-e "$INDIR/data-$sch.$tbl.bin.gz" && !-e "$INDIR/data-$sch.$tbl.bin");

		push(@{ $distributed_table{$proc} }, "\\o");
		push(@{ $distributed_table{$proc} }, qq{\\echo Restoring data to table $s.$t});
		# first of all get the table's column namesfrom the origin table
		my @collist = get_column_list($s, $t);
		my $target_list = '';
		$target_list = "(" . join(',', @collist) . ")" if ($#collist >= 0);
		if ($DISABLE_TRIGGERS)
		{
			push(@{ $distributed_table{$proc} }, qq{ALTER TABLE $s.$t DISABLE TRIGGER ALL;});
		}
		if ($TRUNCATE)
		{
			push(@{ $distributed_table{$proc} }, qq{TRUNCATE $s.$t;});
		}
		if (-e "$INDIR/data-$sch.$tbl.bin.gz")
		{
			$sch =  quotemeta($sch);
			$tbl =  quotemeta($tbl);
			push(@{ $distributed_table{$proc} }, qq{\\copy $s.$t $target_list FROM PROGRAM 'gunzip -c "$INDIR/data-$sch.$tbl.bin.gz"' WITH (FORMAT binary);});
		}
		elsif (-e "$INDIR/data-$sch.$tbl.bin")
		{
			$sch =  quotemeta($sch);
			$tbl =  quotemeta($tbl);
			push(@{ $distributed_table{$proc} }, qq{\\copy $s.$t $target_list FROM '$INDIR/data-$sch.$tbl.bin' WITH (FORMAT binary);});
		}
		if ($DISABLE_TRIGGERS)
		{
			push(@{ $distributed_table{$proc} }, qq{ALTER TABLE $s.$t ENABLE TRIGGER ALL;});
		}
		$proc++;
		$proc = 1 if ($proc > $JOBS);
	}
}

# Use the -j processes to restore tables data
print "Restoring tables data\n" if ($VERBOSE);
foreach my $p (sort keys %distributed_table)
{

	if (!$OUTFILE) {
		spawn sub
		{
			my ($fh, $filename) = tempfile('pg_dumpbinXXXX', SUFFIX => '.tmp', DIR => $TMP_DIR, UNLINK => 1 );
			if (defined $fh)
			{
				map { print $fh "$_\n"; } @{ $distributed_table{$p} };
				close($fh);
				&eval_command("$PSQL $PG_OPT -v ON_ERROR_STOP=1 -d $DBNAME -f $filename");
			}
		};
	}
	else
	{
		open(my $fh, '>>', "$OUTFILE") or die "FATAL: can not write to file $OUTFILE, $!\n";
		map { print $fh "$_\n"; } @{ $distributed_table{$p} };
		close($fh);
	}
}

# Wait for all child processes to localdie
while (scalar keys %RUNNING_PIDS > 0)
{
	my $kid = waitpid(-1, WNOHANG);
	if ($kid > 0) {
		delete $RUNNING_PIDS{$kid};
	}
	usleep(50000);
}

exit(1) if ($interrupt);

print "Restoring sequences values.\n" if ($VERBOSE);
# Restore the sequences values
if (scalar keys %sequences)
{
	if (!$OUTFILE)
	{
		my ($fh, $filename) = tempfile('pg_dumpbinXXXX', SUFFIX => '.tmp', DIR => $TMP_DIR, UNLINK => 1 );
		foreach my $s (sort keys %sequences)
		{ 
			foreach my $n (sort keys %{ $sequences{$s} })
			{ 
				print $fh "SELECT pg_catalog.setval('$sequences{$s}{$n}{fqdn}', $sequences{$s}{$n}{value}, true);\n";
			}
		}
		close($fh);
		# Import file to set sequences values
		&eval_command("$PSQL $PG_OPT -v ON_ERROR_STOP=1 -d $DBNAME -f $filename");
	}
	else
	{
		open(my $fh, '>>', "$OUTFILE") or die "FATAL: can not write to file $OUTFILE, $!\n";
		foreach my $s (sort keys %sequences)
		{ 
			foreach my $n (sort keys %{ $sequences{$s} })
			{ 
				print $fh "SELECT pg_catalog.setval('$sequences{$s}{$n}{fqdn}', $sequences{$s}{$n}{value}, true);\n";
			}
		}
		close($fh);
	}
}

# Dump database post-data section
if (!$DATAONLY)
{
	my $output = "-d $DBNAME";
	if ($OUTFILE) {
		$output = "-f - >> $OUTFILE";
	}
	print "Restoring post data section.\n" if ($VERBOSE);
	`$PGRESTORE $PG_OPT $PG_FILTER -j $JOBS $INDIR/post-data.dmp $output`;
	if ($?) {
		die "ERROR: pg_restore error for post-data section.\n";
	}
}

#----------------------------------------------------------------------------------

####
# Show program usage
####
sub usage
{
	my ($msg, $exit_code) = @_;

	$exit_code ||= 0;

	print qq{
Program used to restore a PostgreSQL binary dump done with pg_dumpbinary.
It can not be used to restore other PostgreSQL dump.

usage: pg_restorebinary [options] [-d dbname | -f outfile.sql] backup_dir

  backup_dir   directory where backup files to restore will be read.
               It must be a directory created by pg_dumpbinary.
options:

  -a, --data-only              restore only the data, no schema.
  -C, --create                 create the database before restoring into it.
  -d, --database DBNAME        database to restore, it must exists.
  -E, --exclude-ext EXTNAME    name of an extension to not restore, it can
                               be used multiple time.
  -f, --file FILENAME          specify output file for generated script. No
                               restore is performed, the resulting DDL and
			       commands are written to the file.
  -h, --host HOSTNAME          database server host or socket directory.
  -i, --info                   print information about the dump and exit.
  -j, --job NUM                use this many parallel jobs to restore.
  -n, --schema SCHEMA          restore the named schema(s) only.
  -N, --exclude-schema SCHEMA  do NOT restore the named schema(s).
  -p, --port PORT              database server port number, default: 5432.
  -t, --table TABLE            restore named relation.
  -T, --exclude-table TABLE    do NOT restore the named table.
  -u, --user NAME              connect as specified database user.
  -v, --version                show program version.
  -V, --verbose                show progress information.
  --help                       show usage.
  --disable-triggers           disable triggers during data restore.
  --truncate                   truncate the table before importing the data.
  --schema-exists              add an IF NOT EXISTS clause to CREATE SCHEMA.
  --dump-create                print to stdout the CREATE and ALTER DATABASE
                               statements.

$msg
};
	exit $exit_code;
}

####
# Execute a system command and check its code result.
# Exit with the code returned in case of error.
# With success it returns the command output.
####
sub eval_command
{
	my $cmd = shift;

	my @result = `$cmd`;

	my $exit_val = $? >> 8;
	if ($exit_val)
	{
		print STDERR "ERROR running command: $cmd\n";
		kill USR1 => $parent_pid; 
		exit 1;
	}

	return wantarray ? @result : $result[0];
}

####
# Return a hash of array corresponding to the
# list of files to restore per schema and table
####
sub get_table_list
{
	my %tb_lst = ();

	my $pgr_opt = '';
	$pgr_opt = '-f -' if ($pgrestore_version >= 12);
	my @list = `$PGRESTORE $pgr_opt $INDIR/pre-data.dmp | grep -E "CREATE (UNLOGGED|TABLE) "`;
	chomp(@list);

	foreach my $l (@list)
	{
		if ($l =~ /^CREATE (?:UNLOGGED )?TABLE ([^\s]+) \($/) {
			my @inf = split(/\./, $1);
			my $sch = $inf[0];
			$sch =~ s/"//g;
			my $tb = $inf[1];
			$tb =~ s/"//g;
			# Exclude / include some schemas only?
			next if ($#INC_SCHEMA >= 0 and !grep(/^["]*\Q$sch\E["]*$/, @INC_SCHEMA));
			next if ($#EXC_SCHEMA >= 0 and grep(/^["]*\Q$sch\E["]*$/, @EXC_SCHEMA));
			# Exclude / include some tables only?
			next if ($#INC_TABLE >= 0 and !grep(/^["]*\Q$tb\E["]*$/, @INC_TABLE));
			next if ($#EXC_TABLE >= 0 and grep(/^["]*\Q$tb\E["]*$/, @EXC_TABLE));
			$tb_lst{$inf[0]}{$inf[1]} = $l;
		}
	}

	# Read list of extensions tables
	if (-e "$INDIR/extensions-tables.lst")
	{
		open(my $ifh, '<', "$INDIR/extensions-tables.lst") or die "FATAL: can not read file $INDIR/extensions-tables.lst, $!\n";
		while (my $l = <$ifh>)
		{
			chomp($l);
			$l =~ s/^(.*)\|(.*)/$2/;
			my $extens = $1;
			my @inf = split(/\./, $2);
			my $sch = $inf[0];
			$sch =~ s/"//g;
			my $tb = $inf[1];
			$tb =~ s/"//g;
			next if exists ($tb_lst{$inf[0]}{$inf[1]});
			next if exists ($tb_lst{$inf[0]}{"\"$inf[1]\""});
			next if exists ($tb_lst{"\"$inf[0]\""}{$inf[1]});
			next if exists ($tb_lst{"\"$inf[0]\""}{"\"$inf[1]\""});

			# Exclude / include some schemas only?
			next if ($#INC_SCHEMA >= 0 and !grep(/^["]*\Q$sch\E["]*$/, @INC_SCHEMA));
			next if ($#EXC_SCHEMA >= 0 and grep(/^["]*\Q$sch\E["]*$/, @EXC_SCHEMA));
			# Exclude / include some tables only?
			next if ($#INC_TABLE >= 0 and !grep(/^["]*\Q$tb\E["]*$/, @INC_TABLE));
			next if ($#EXC_TABLE >= 0 and grep(/^["]*\Q$tb\E["]*$/, @EXC_TABLE));

			$tb_lst{$inf[0]}{$inf[1]} = $l;
		}
		close($ifh);
	}
	else
	{
		opendir(DIR, "$INDIR") || die "FATAL: can't opendir $INDIR: $!";
		my @datafiles = grep { /^data-.*.bin(?:.gz)?$/} readdir(DIR);
		closedir(DIR);
		chomp(@datafiles);
		map { s/^data-//; s/\.bin(?:\.gz)?$//; } @datafiles;
		foreach my $l (@datafiles)
		{
			my @inf = split(/\./, $l);
			my $sch = $inf[0];
			my $tb = $inf[1];
			$inf[0] = "\"$inf[0]\"" if ($inf[0] =~ /[^a-z0-9_\$]/ || $inf[0] =~ /^\d/);
			$inf[1] = "\"$inf[1]\"" if ($inf[1] =~ /[^a-z0-9_\$]/ || $inf[1] =~ /^\d/);
			next if exists ($tb_lst{$inf[0]}{$inf[1]});
			next if exists ($tb_lst{$inf[0]}{"\"$inf[1]\""});
			next if exists ($tb_lst{"\"$inf[0]\""}{$inf[1]});
			next if exists ($tb_lst{"\"$inf[0]\""}{"\"$inf[1]\""});
			# Exclude / include some schemas only?
			next if ($#INC_SCHEMA >= 0 and !grep(/^["]*\Q$sch\E["]*$/, @INC_SCHEMA));
			next if ($#EXC_SCHEMA >= 0 and grep(/^["]*\Q$sch\E["]*$/, @EXC_SCHEMA));
			# Exclude / include some tables only?
			next if ($#INC_TABLE >= 0 and !grep(/^["]*\Q$tb\E["]*$/, @INC_TABLE));
			next if ($#EXC_TABLE >= 0 and grep(/^["]*\Q$tb\E["]*$/, @EXC_TABLE));
			$tb_lst{$inf[0]}{$inf[1]} = $l;
		}
	}

	return %tb_lst;
}

# Get the list of partitions to import
sub get_partition_list
{
	my %part_list = ();

	my $pgr_opt = '';
	$pgr_opt = '-f -' if ($pgrestore_version >= 12);
	my @list = `$PGRESTORE $pgr_opt $INDIR/pre-data.dmp | grep "ALTER TABLE ONLY .* ATTACH PARTITION "`;
	chomp(@list);

	foreach my $l (@list)
	{
		# ALTER TABLE ONLY schema.table ATTACH PARTITION schema.partition
		if ($l =~ /ALTER TABLE ONLY ["]*([^"\.]+)["]*\.["]*([^"\s]+)["]* ATTACH PARTITION ["]*([^"\.]+)["]*\.["]*([^"\s]+)["]* /)
		{
			push(@{$part_list{$1}{$2}}, $3 . '.' . $4)
		}
	}

	return %part_list
}

####
# Set schema/table option to use with pg_dump
####
sub set_filter_option
{

	if ($#INC_SCHEMA >= 0)
	{
		foreach my $s (@INC_SCHEMA)
		{
			$PG_FILTER .= " -n '$s'";
		}
	}

	if ($#EXC_SCHEMA >= 0)
	{
		foreach my $s (@EXC_SCHEMA)
		{
			$PG_FILTER .= " -N '$s'";
		}
	}

	if ($#INC_TABLE >= 0)
	{
		foreach my $s (@INC_TABLE)
		{
			$PG_FILTER .= " -t '$s'";
		}
	}

	if ($#EXC_TABLE >= 0)
	{
		foreach my $s (@EXC_TABLE)
		{
			$PG_FILTER .= " -T '$s'";
		}
	}
}

sub get_sequence_values
{
	my %seq = ();
	my %list_dump_seq = ();

	return %seq if (!-e "$INDIR/data-sequences.lst");

	# Look at sequences values to restore
	open(my $sfh, '<', "$INDIR/data-sequences.lst") or die "FATAL: can not read file $INDIR/data-sequences.lst, $!\n";
	while (my $l = <$sfh>)
	{
		chomp($l);
		# Line format: schema.sequence_name.value
		if ($l =~ s/^([^\.]+)\.([^\.]+)\.(\d*)$/$1\.$2/)
		{
			my $schema = $1;
			my $seqname = $2;
			my $value = $3;
			$schema =~ s/"//g;
			$seqname =~ s/"//g;
			# With filter on tables no sequences are imported, only tables
			next if ($#INC_TABLE >= 0 or $#EXC_TABLE >= 0);
			# Exclude / include some schemas only?
			next if ($#INC_SCHEMA >= 0 and !grep(/^\Q$schema\E$/, @INC_SCHEMA));
			next if ($#EXC_SCHEMA >= 0 and grep(/^\Q$schema\E$/, @EXC_SCHEMA));
			$seq{$schema}{$seqname}{value} = $value;
			$seq{$schema}{$seqname}{fqdn}  = $l;
		}
	} 
	close($sfh);

	return %seq;
}

sub get_column_list
{
	my ($schema, $table) = @_;

	my @info = `$PGRESTORE -n '$schema' -t '$table' -f - $INDIR/pre-data.dmp | grep -v -- "^--" | grep -v "^\$"`;
	if ($?) {
		die "ERROR: pg_restore error to obtain dump information for table structure.\n";
	}
	chomp(@info);

	# Take the list of columns targets from meta file
	my @colnames = ();
	my $file = quotemeta($schema) . '.' . quotemeta($table);
	if (-e "$INDIR/meta-$file.txt")
	{
		open(my $lfh, '<', "$INDIR/meta-$file.txt") or die "FATAL: can not read file $INDIR/meta-$file.txt, $!\n";
		@colnames = <$lfh>;
		chomp(@colnames);
		close($lfh);
	}
	# Read information from pre-data dump file
	else
	{
		my $found = 0;
		foreach my $l (@info)
		{
			$found = 1, next if ($l =~ /^CREATE TABLE/);
			last if ($found && $l =~ /^\);$/);
			if ($found && $l =~ /^\s+([^\s]+)\s+.*/) {
				my $col = $1;
				push(@colnames, $col) if ($col !~ /^(PRIMARY|CONSTRAINT|UNIQUE|CHECK|EXCLUDE|FOREIGN|DEFERRABLE|NOT|INITIALLY|MATCH|ON|REFERENCES)$/);
			}
		}
	}

	return @colnames;
}

